version 3.0
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
_test_files
|
||||
node_modules
|
||||
package-lock.json
|
4
LICENSE
|
@ -1,9 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017-2023 Marc Robledo
|
||||
Copyright (c) 2017-2024 Marc Robledo
|
||||
|
||||
This project incorporates components from Octicons
|
||||
(https://github.com/primer/octicons/) Copyright (c) 2023 GitHub Inc.,
|
||||
(https://github.com/primer/octicons/) Copyright (c) 2024 GitHub Inc.,
|
||||
also released under MIT license.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
35
README.md
|
@ -1,5 +1,5 @@
|
|||
# Rom Patcher JS
|
||||
A ROM patcher made in HTML5.
|
||||
A ROM patcher made in Javascript.
|
||||
|
||||
**Features:**
|
||||
* Supported formats:
|
||||
|
@ -16,10 +16,35 @@ A ROM patcher made in HTML5.
|
|||
* can remove headers before patching
|
||||
* unzips files automatically
|
||||
* made in Vanilla JS
|
||||
* can be run in any modern web browser, including mobile
|
||||
* can be run in any modern web browser (including mobile) and Node.js
|
||||
* can be customized and embeded into your website for a custom patcher
|
||||
|
||||
|
||||
|
||||
## Embedding Rom Patcher JS in your site
|
||||
Modders and hackers can embed Rom Patcher JS in their websites to provide an online ROM patcher for their patches, allowing users to patch ROMs without downloading any files.<br/>
|
||||
|
||||
- File [`index_template.html`](https://github.com/marcrobledo/RomPatcher.js/blob/master/index_template.html) includes a simple working example
|
||||
- Read [the wiki](https://github.com/marcrobledo/RomPatcher.js/wiki) for more detailed instructions
|
||||
|
||||
|
||||
|
||||
## Using Rom Patcher JS in Node CLI
|
||||
Install dependencies:
|
||||
> npm install
|
||||
|
||||
Patch a ROM:
|
||||
> node index.js patch "my_rom.bin" "my_patch.ips"
|
||||
|
||||
Create a patch:
|
||||
> node index.js create "original_rom.bin" modified_rom.bin"
|
||||
|
||||
Show all options:
|
||||
> node index.js patch --help<br/>
|
||||
> node index.js create --help
|
||||
|
||||
|
||||
|
||||
## Known sites that use Rom Patcher JS
|
||||
* [Romhacking.net](https://www.romhacking.net/)
|
||||
* [Smash Remix](https://smash64.online/remix/)
|
||||
|
@ -27,3 +52,9 @@ A ROM patcher made in HTML5.
|
|||
* [Rocket Edition](https://rocket-edition.com/download/)
|
||||
* [SnapCameraPreservation](https://snapchatreverse.jaku.tv/snap/)
|
||||
* [Pokemon Clover](https://poclo.net/download)
|
||||
|
||||
|
||||
|
||||
## Resources used
|
||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/) by Gildas Lormeau
|
||||
* [Octicons](https://primer.style/octicons/) by GitHub Inc.
|
||||
|
|
|
@ -1,54 +1,53 @@
|
|||
/*
|
||||
Cache Service Worker template by mrc 2019
|
||||
mostly based in:
|
||||
https://github.com/GoogleChrome/samples/blob/gh-pages/service-worker/basic/service-worker.js
|
||||
https://github.com/chriscoyier/Simple-Offline-Site/blob/master/js/service-worker.js
|
||||
https://gist.github.com/kosamari/7c5d1e8449b2fbc97d372675f16b566e
|
||||
Cache Service Worker for Rom Patcher JS by Marc Robledo
|
||||
https://github.com/marcrobledo/RomPatcher.js
|
||||
|
||||
Note for GitHub Pages:
|
||||
there can be an unexpected behaviour (cache not updating) when site is accessed from
|
||||
https://user.github.io/repo/ (without index.html) in some browsers (Firefox)
|
||||
use absolute paths if hosted in GitHub Pages in order to avoid it
|
||||
also invoke sw with an absolute path:
|
||||
navigator.serviceWorker.register('/repo/_cache_service_worker.js', {scope: '/repo/'})
|
||||
Used to cache the webapp files for offline use
|
||||
*/
|
||||
|
||||
var PRECACHE_ID='rom-patcher-js';
|
||||
var PRECACHE_VERSION='v291';
|
||||
var PRECACHE_URLS=[
|
||||
'/RomPatcher.js/','/RomPatcher.js/index.html',
|
||||
var PRECACHE_ID = 'rom-patcher-js';
|
||||
var PRECACHE_VERSION = 'v30beta1';
|
||||
var PRECACHE_URLS = [
|
||||
'/RomPatcher.js/', '/RomPatcher.js/index.html',
|
||||
'/RomPatcher.js/manifest.json',
|
||||
'/RomPatcher.js/style/app_icon_16.png',
|
||||
'/RomPatcher.js/style/app_icon_114.png',
|
||||
'/RomPatcher.js/style/app_icon_144.png',
|
||||
'/RomPatcher.js/style/app_icon_192.png',
|
||||
'/RomPatcher.js/style/app_icon_maskable.png',
|
||||
'/RomPatcher.js/style/logo.png',
|
||||
'/RomPatcher.js/style/RomPatcher.css',
|
||||
'/RomPatcher.js/style/icon_close.svg',
|
||||
'/RomPatcher.js/style/icon_github.svg',
|
||||
'/RomPatcher.js/style/icon_heart.svg',
|
||||
'/RomPatcher.js/style/icon_settings.svg',
|
||||
'/RomPatcher.js/js/RomPatcher.js',
|
||||
'/RomPatcher.js/js/locale.js',
|
||||
'/RomPatcher.js/js/worker_apply.js',
|
||||
'/RomPatcher.js/js/worker_create.js',
|
||||
'/RomPatcher.js/js/worker_crc.js',
|
||||
'/RomPatcher.js/js/MarcFile.js',
|
||||
'/RomPatcher.js/js/crc.js',
|
||||
'/RomPatcher.js/js/zip.js/zip.js',
|
||||
'/RomPatcher.js/js/zip.js/z-worker.js',
|
||||
'/RomPatcher.js/js/zip.js/inflate.js',
|
||||
'/RomPatcher.js/js/formats/ips.js',
|
||||
'/RomPatcher.js/js/formats/ups.js',
|
||||
'/RomPatcher.js/js/formats/aps_n64.js',
|
||||
'/RomPatcher.js/js/formats/aps_gba.js',
|
||||
'/RomPatcher.js/js/formats/bps.js',
|
||||
'/RomPatcher.js/js/formats/rup.js',
|
||||
'/RomPatcher.js/js/formats/ppf.js',
|
||||
'/RomPatcher.js/js/formats/pmsr.js',
|
||||
'/RomPatcher.js/js/formats/vcdiff.js',
|
||||
'/RomPatcher.js/js/formats/zip.js'
|
||||
/* Rom Patcher JS core (code) */
|
||||
'/RomPatcher.js/rom-patcher-js/RomPatcher.js',
|
||||
'/RomPatcher.js/rom-patcher-js/RomPatcher.webapp.js',
|
||||
'/RomPatcher.js/rom-patcher-js/RomPatcher.webworker.apply.js',
|
||||
'/RomPatcher.js/rom-patcher-js/RomPatcher.webworker.create.js',
|
||||
'/RomPatcher.js/rom-patcher-js/RomPatcher.webworker.crc.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/BinFile.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/HashCalculator.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.ips.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.bps.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.ups.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.aps_n64.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.aps_gba.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.rup.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.ppf.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.pmsr.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/RomPatcher.format.vcdiff.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/zip.js/z-worker.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/zip.js/zip.min.js',
|
||||
'/RomPatcher.js/rom-patcher-js/modules/zip.js/inflate.js',
|
||||
/* Rom Patcher JS core (web assets) */
|
||||
'/RomPatcher.js/rom-patcher-js/assets/icon_alert_orange.svg',
|
||||
'/RomPatcher.js/rom-patcher-js/assets/icon_check_circle_green.svg',
|
||||
'/RomPatcher.js/rom-patcher-js/assets/icon_upload.svg',
|
||||
'/RomPatcher.js/rom-patcher-js/assets/icon_x_circle_red.svg',
|
||||
/* webapp assets */
|
||||
'/RomPatcher.js/webapp/webapp.js',
|
||||
'/RomPatcher.js/webapp/style.css',
|
||||
'/RomPatcher.js/webapp/app_icon_16.png',
|
||||
'/RomPatcher.js/webapp/app_icon_114.png',
|
||||
'/RomPatcher.js/webapp/app_icon_144.png',
|
||||
'/RomPatcher.js/webapp/app_icon_192.png',
|
||||
'/RomPatcher.js/webapp/app_icon_maskable.png',
|
||||
'/RomPatcher.js/webapp/logo.png',
|
||||
'/RomPatcher.js/webapp/icon_close.svg',
|
||||
'/RomPatcher.js/webapp/icon_github.svg',
|
||||
'/RomPatcher.js/webapp/icon_heart.svg',
|
||||
'/RomPatcher.js/webapp/icon_settings.svg'
|
||||
];
|
||||
|
||||
|
||||
|
@ -56,7 +55,7 @@ var PRECACHE_URLS=[
|
|||
// install event (fired when sw is first installed): opens a new cache
|
||||
self.addEventListener('install', evt => {
|
||||
evt.waitUntil(
|
||||
caches.open('precache-'+PRECACHE_ID+'-'+PRECACHE_VERSION)
|
||||
caches.open('precache-' + PRECACHE_ID + '-' + PRECACHE_VERSION)
|
||||
.then(cache => cache.addAll(PRECACHE_URLS))
|
||||
.then(self.skipWaiting())
|
||||
);
|
||||
|
@ -67,10 +66,10 @@ self.addEventListener('install', evt => {
|
|||
self.addEventListener('activate', evt => {
|
||||
evt.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return cacheNames.filter(cacheName => (cacheName.startsWith('precache-'+PRECACHE_ID+'-') && !cacheName.endsWith('-'+PRECACHE_VERSION)));
|
||||
return cacheNames.filter(cacheName => (cacheName.startsWith('precache-' + PRECACHE_ID + '-') && !cacheName.endsWith('-' + PRECACHE_VERSION)));
|
||||
}).then(cachesToDelete => {
|
||||
return Promise.all(cachesToDelete.map(cacheToDelete => {
|
||||
console.log('delete '+cacheToDelete);
|
||||
console.log('Delete cache: ' + cacheToDelete);
|
||||
return caches.delete(cacheToDelete);
|
||||
}));
|
||||
}).then(() => self.clients.claim())
|
||||
|
@ -80,12 +79,12 @@ self.addEventListener('activate', evt => {
|
|||
|
||||
// fetch event (fired when requesting a resource): returns cached resource when possible
|
||||
self.addEventListener('fetch', evt => {
|
||||
if(evt.request.url.startsWith(self.location.origin)){ //skip cross-origin requests
|
||||
if (evt.request.url.startsWith(self.location.origin)) { //skip cross-origin requests
|
||||
evt.respondWith(
|
||||
caches.match(evt.request).then(cachedResource => {
|
||||
if (cachedResource) {
|
||||
return cachedResource;
|
||||
}else{
|
||||
} else {
|
||||
return fetch(evt.request);
|
||||
}
|
||||
})
|
||||
|
|
318
index.html
|
@ -7,14 +7,14 @@
|
|||
<meta name="keywords" content="ips,ups,aps,bps,rup,ninja,ppf,xdelta,patcher,online,html5,web,rom,patch,hack,translation"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
|
||||
<link rel="manifest" href="./manifest.json"/>
|
||||
<link rel="shortcut icon" href="./style/app_icon_16.png" type="image/png" sizes="16x16"/>
|
||||
<link rel="shortcut icon" href="./style/app_icon_192.png" type="image/png" sizes="192x192"/>
|
||||
<link rel="shortcut icon" href="./webapp/app_icon_16.png" type="image/png" sizes="16x16"/>
|
||||
<link rel="shortcut icon" href="./webapp/app_icon_192.png" type="image/png" sizes="192x192"/>
|
||||
<!-- iOS icons -->
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./style/app_icon_114.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="./style/app_icon_114.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./style/app_icon_144.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./style/app_icon_144.png" />
|
||||
<link rel="apple-touch-icon" href="./style/app_icon_192.png" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./webapp/app_icon_114.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="./webapp/app_icon_114.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./webapp/app_icon_144.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./webapp/app_icon_144.png" />
|
||||
<link rel="apple-touch-icon" href="./webapp/app_icon_192.png" />
|
||||
|
||||
<!-- cache -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
|
@ -28,147 +28,132 @@
|
|||
<meta property="og:title" content="Rom Patcher JS">
|
||||
<meta name="twitter:title" content="Rom Patcher JS">
|
||||
<meta name="twitter:description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, PPF and xdelta.">
|
||||
<meta property="og:image" content="https://www.marcrobledo.com/RomPatcher.js/style/thumbnail.jpg">
|
||||
<meta name="twitter:image" content="https://www.marcrobledo.com/RomPatcher.js/style/thumbnail.jpg">
|
||||
<meta property="og:image" content="https://www.marcrobledo.com/RomPatcher.js/card_thumbnail.jpg">
|
||||
<meta name="twitter:image" content="https://www.marcrobledo.com/RomPatcher.js/card_thumbnail.jpg">
|
||||
<meta name="twitter:card" content="photo">
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="./style/RomPatcher.css" media="all"/>
|
||||
<script type="text/javascript" src="./js/locale.js"></script>
|
||||
<script type="text/javascript" src="./js/RomPatcher.js"></script>
|
||||
<script type="text/javascript" src="./js/MarcFile.js"></script>
|
||||
<script type="text/javascript" src="./js/crc.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/zip.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/ips.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/ups.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/aps_n64.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/aps_gba.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/bps.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/rup.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/ppf.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/pmsr.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/vcdiff.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="./webapp/style.css" media="all"/>
|
||||
|
||||
<script type="text/javascript" src="./js/zip.js/zip.js"></script>
|
||||
<!-- <script type="text/javascript" src="js/libunrar/rpc.js"></script> -->
|
||||
|
||||
<script type="text/javascript"><!--
|
||||
// CUSTOM PATCHER EXAMPLE
|
||||
// uncomment this to build your own custom exclusive patcher for your hacks/translations
|
||||
// user will only need to provide the ROM file, as patches will be fetched from your server!
|
||||
// a crc (or various crcs) can be provided for source files, allowing old formats like IPS to have validation!
|
||||
/*var CUSTOM_PATCHER=[
|
||||
{ //example: uncompressed file
|
||||
file:'./_example/SML2DXv181.ips',
|
||||
name:'Super Mario Land 2 DX v1.8.1 (USA/Europe)',
|
||||
crc:0xd5ec24e4
|
||||
},{ //example: uncompressed file
|
||||
file:'./_example/SML2DXv181_jap.ips',
|
||||
name:'Super Mario Land 2 DX v1.8.1 (Japan)',
|
||||
crc:0xa715daf5
|
||||
},{ //example: compressed file
|
||||
file:'./_example/SML2DXv181.zip',
|
||||
name:'Super Mario Land 2 DX v1.8.1 (World)',
|
||||
crc:[0xd5ec24e4,0xa715daf5]
|
||||
},{ //example: compressed file containing several patches, separate patches provided (recommended)
|
||||
file:'./_example/SML2DXv181.zip',
|
||||
patches:[{
|
||||
file:'SML2DXv181.ips',
|
||||
name:'Super Mario Land 2 DX v1.8.1 (USA/Europe)',
|
||||
crc:0xd5ec24e4
|
||||
},{
|
||||
file:'SML2DXv181_jap.ips',
|
||||
name:'Super Mario Land 2 DX v1.8.1 (Japan)',
|
||||
crc:0xa715daf5
|
||||
}]
|
||||
}
|
||||
];*/
|
||||
--></script>
|
||||
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/BinFile.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/HashCalculator.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.ips.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.ups.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.aps_n64.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.aps_gba.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.bps.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.rup.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.ppf.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.pmsr.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.vcdiff.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/zip.js/zip.min.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/RomPatcher.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/RomPatcher.webapp.js"></script>
|
||||
<script type="text/javascript" src="./webapp/webapp.js"></script>
|
||||
</head>
|
||||
<body><div id="column">
|
||||
|
||||
|
||||
<!-- HEADER -->
|
||||
<header><img src="./style/logo.png" /><h1>Rom Patcher JS</h1></header>
|
||||
<header><img src="./webapp/logo.png" loading="lazy" /><h1>Rom Patcher JS</h1></header>
|
||||
|
||||
<!-- APP -->
|
||||
<div id="wrapper">
|
||||
<div id="switch-container"><span id="switch-create-button" class="button-outer"><span data-localize="creator_mode">Creator mode</span> <span id="switch-create" class="switch disabled"></span></span></div>
|
||||
<div id="switch-container"><span id="switch-create-button" class="btn-transparent"><span data-localize="yes">Creator mode</span> <span id="switch-create" class="switch disabled"></span></span></div>
|
||||
|
||||
<div id="tab0" class="tab">
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right"><label for="input-file-rom" data-localize="rom_file">ROM file:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-rom" class="enabled" />
|
||||
<div class="tab">
|
||||
<div id="rom-patcher-container">
|
||||
<div class="row m-b" id="rom-patcher-row-file-rom">
|
||||
<div class="text-right"><label for="rom-patcher-input-file-rom" data-localize="yes">ROM file:</label></div>
|
||||
<div class="rom-patcher-container-input">
|
||||
<input type="file" id="rom-patcher-input-file-rom" class="w100 empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b" id="rom-info">
|
||||
<div class="leftcol text-right">CRC32:</div><div class="rightcol"><span id="crc32"></span></div>
|
||||
<div class="leftcol text-right">MD5:</div><div class="rightcol"><span id="md5"></span></div>
|
||||
<div class="leftcol text-right">SHA-1:</div><div class="rightcol"><span id="sha1"></span></div>
|
||||
</div>
|
||||
<div class="row m-b hide" id="row-removeheader">
|
||||
<div class="leftcol text-right"></div>
|
||||
<div class="rightcol">
|
||||
<input type="checkbox" id="checkbox-removeheader" /> <label for="checkbox-removeheader" data-localize="remove_header">Remove header before patching</label>
|
||||
<div class="row m-b" id="rom-patcher-row-alter-header">
|
||||
<div></div>
|
||||
<div>
|
||||
<label><input type="checkbox" id="rom-patcher-checkbox-alter-header" class="styled" /> <span id="rom-patcher-span-alter-header"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b hide" id="row-addheader">
|
||||
<div class="leftcol text-right"></div>
|
||||
<div class="rightcol">
|
||||
<input type="checkbox" id="checkbox-addheader" /> <label for="checkbox-addheader" data-localize="add_header">Add temporary header</label> <small>(<label id="headersize" for="checkbox-addheader"></label>)</small>
|
||||
<div class="m-b text-selectable text-mono" id="rom-patcher-rom-info">
|
||||
<div class="row">
|
||||
<div class="text-right">CRC32:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-crc32"></span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-right">MD5:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-md5"></span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-right">SHA-1:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-sha1"></span></div>
|
||||
</div>
|
||||
<div class="row" id="rom-patcher-row-info-rom">
|
||||
<div class="text-right">ROM:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-rom-info"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b" id="rom-patcher-row-file-patch">
|
||||
<div class="text-right"><label for="rom-patcher-input-file-patch" data-localize="yes">Patch file:</label></div>
|
||||
<div class="rom-patcher-container-input">
|
||||
<input type="file" id="rom-patcher-input-file-patch" class="w100 empty" accept=".ips,.ups,.bps,.aps,.rup,.ppf,.mod,.xdelta,.vcdiff,.zip" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b" id="rom-patcher-row-patch-description">
|
||||
<div class="text-right text-mono" data-localize="yes">Description:</div>
|
||||
<div class="text-truncate" id="rom-patcher-patch-description"></div>
|
||||
</div>
|
||||
<div class="row m-b text-selectable text-mono" id="rom-patcher-row-patch-requirements">
|
||||
<div class="text-right text-mono" id="rom-patcher-patch-requirements-type">ROM requirements:</div>
|
||||
<div class="text-truncate" id="rom-patcher-patch-requirements-value"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons text-center">
|
||||
<div id="rom-patcher-row-error-message" class="m-b"><span id="rom-patcher-error-message"></span></div>
|
||||
<button id="rom-patcher-button-apply" data-localize="yes" disabled>Apply patch</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b" id="row-file-patch">
|
||||
<div class="leftcol text-right"><label for="input-file-patch" data-localize="patch_file">Patch file:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-patch" accept=".ips,.ups,.bps,.aps,.rup,.ppf,.mod,.xdelta,.vcdiff,.zip"/>
|
||||
<div id="patch-builder-container" style="display:none">
|
||||
<div class="row m-b">
|
||||
<div class="text-right"><label for="patch-builder-input-file-original" data-localize="yes" >Original ROM:</label></div>
|
||||
<div>
|
||||
<input type="file" id="patch-builder-input-file-original" class="w100 empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b hide" id="row-expected-source-info"></div>
|
||||
|
||||
<div class="buttons">
|
||||
<span id="message-apply" class="message"></span>
|
||||
<button id="button-apply" data-localize="apply_patch" class="disabled" disabled>Apply patch</button>
|
||||
<div class="row m-b">
|
||||
<div class="text-right"><label for="patch-builder-input-file-modified" data-localize="yes">Modified ROM:</label></div>
|
||||
<div>
|
||||
<input type="file" id="patch-builder-input-file-modified" class="w100 empty" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="text-right"><label for="patch-builder-select-patch-type" data-localize="yes">Patch type:</label></div>
|
||||
<div>
|
||||
<select id="patch-builder-select-patch-type">
|
||||
<option value="ips">IPS</option>
|
||||
<option value="bps">BPS</option>
|
||||
<option value="ppf">PPF</option>
|
||||
<option value="ups">UPS</option>
|
||||
<option value="aps">APS</option>
|
||||
<option value="rup">RUP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons text-center">
|
||||
<div id="patch-builder-row-error-message" class="m-b"><span id="patch-builder-error-message"></span></div>
|
||||
<button id="patch-builder-button-create" disabled data-localize="yes">Create patch</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="tab1" class="tab">
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right"><label for="input-file-rom1" data-localize="original_rom" >Original ROM:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-rom1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right"><label for="input-file-rom2" data-localize="modified_rom">Modified ROM:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-rom2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right" data-localize="patch_type">Patch type:</div>
|
||||
<div class="rightcol">
|
||||
<select id="select-patch-type">
|
||||
<option value="ips">IPS</option>
|
||||
<option value="bps">BPS</option>
|
||||
<option value="ppf">PPF</option>
|
||||
<option value="ups">UPS</option>
|
||||
<option value="aps">APS</option>
|
||||
<option value="rup">RUP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<span id="message-create" class="message"></span>
|
||||
<button id="button-create" class="disabled" disabled data-localize="create_patch">Create patch</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -178,64 +163,59 @@
|
|||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div>
|
||||
<button id="button-settings" class="button-outer"><img src="style/icon_settings.svg" class="icon settings" /> <span data-localize="settings">Settings</span></button>
|
||||
<button id="button-settings" class="btn-transparent"><img src="./webapp/icon_settings.svg" loading="lazy" class="icon settings" /> <span data-localize="yes">Settings</span></button>
|
||||
</div>
|
||||
|
||||
Rom Patcher JS <small>v2.9.1</small> by <a href="/">Marc Robledo</a>
|
||||
Rom Patcher JS <small>v3.0 <a style="color:rgb(255, 197, 7)" href="legacy/" rel="nofollow">Beta 1</a></small> by <a href="/">Marc Robledo</a>
|
||||
<br />
|
||||
<img src="style/icon_github.svg" class="icon github" /> <a href="https://github.com/marcrobledo/RomPatcher.js/" target="_blank">See on GitHub</a>
|
||||
<img src="style/icon_heart.svg" class="icon heart" /> <a href="https://www.paypal.me/marcrobledo/5" target="_blank" rel="nofollow">Donate</a>
|
||||
<img src="./webapp/icon_github.svg" loading="lazy" class="icon github" /> <a href="https://github.com/marcrobledo/RomPatcher.js/" target="_blank">See on GitHub</a>
|
||||
<img src="./webapp/icon_heart.svg" loading="lazy" class="icon heart" /> <a href="https://www.paypal.me/marcrobledo/5" target="_blank" rel="nofollow">Donate</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- SETTINGS DIALOG -->
|
||||
<div id="dialog-backdrop">
|
||||
<div id="zip-dialog" class="dialog">
|
||||
<div id="zip-dialog-message" class="text-center"></div>
|
||||
<ul id="zip-dialog-file-list"></ul>
|
||||
<dialog id="dialog-settings" class="rom-patcher-dialog">
|
||||
<div class="text-right m-b"><img id="dialog-settings-button-close" src="./webapp/icon_close.svg" loading="lazy" /></div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div><label for="settings-language">Language</label></div>
|
||||
<div class="text-right">
|
||||
<select id="settings-language" class="enabled">
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="nl">Nederlands</option>
|
||||
<option value="sv">Svenska</option>
|
||||
<option value="ca">Català</option>
|
||||
<option value="ca-va">Valencià</option>
|
||||
<option value="pt-br">Português Brasileiro</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
<option value="zh-tw">中文(正體)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-dialog" class="dialog">
|
||||
<div class="text-right m-b"><img id="settings-close-dialog" src="style/icon_close.svg" /></div>
|
||||
<div class="row row-lg m-b">
|
||||
<div><label data-localize="yes" for="settings-output-suffix">Use patch name for output</label></div>
|
||||
<div class="text-right"><input type="checkbox" id="settings-output-suffix" class="styled" /></div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol"><label for="select-language">Language</label></div>
|
||||
<div class="rightcol text-right">
|
||||
<select id="select-language" class="enabled w100">
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="nl">Nederlands</option>
|
||||
<option value="sv">Svenska</option>
|
||||
<option value="ca">Català</option>
|
||||
<option value="ca-va">Valencià</option>
|
||||
<option value="pt-br">Português Brasileiro</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
<option value="zh-tw">中文(正體)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-lg m-b">
|
||||
<div><label data-localize="yes" for="settings-fix-checksum">Fix ROM checksum</label></div>
|
||||
<div class="text-right"><input type="checkbox" id="settings-fix-checksum" class="styled" /></div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol-lg"><label data-localize="alternate_output_name">Use patch name for output</label></div>
|
||||
<div class="rightcol-lg text-right"><span id="switch-output-name" class="switch disabled"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol-lg"><label data-localize="fix_checksum">Fix ROM checksum</label></div>
|
||||
<div class="rightcol-lg text-right"><span id="switch-fix-checksum" class="switch disabled"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="leftcol-lg"><label data-localize="light_theme">Light theme</label></div>
|
||||
<div class="rightcol-lg text-right"><span id="switch-theme" class="switch disabled"></span></div>
|
||||
</div>
|
||||
<div class="row row-lg">
|
||||
<div><label data-localize="yes" for="settings-light-theme">Light theme</label></div>
|
||||
<div class="text-right"><input type="checkbox" id="settings-light-theme" class="styled" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body></html>
|
||||
|
|
73
index.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
#! /usr/bin/env node
|
||||
|
||||
/*
|
||||
CLI implementation for Rom Patcher JS
|
||||
https://github.com/marcrobledo/RomPatcher.js
|
||||
by Marc Robledo, released under MIT license: https://github.com/marcrobledo/RomPatcher.js/blob/master/LICENSE
|
||||
|
||||
Usage:
|
||||
Install needed dependencies:
|
||||
> npm install
|
||||
|
||||
Patch a ROM:
|
||||
> node index.js patch "my_rom.bin" "my_patch.ips"
|
||||
|
||||
Create a patch from two ROMs:
|
||||
> node index.js create "original_rom.bin" "modified_rom.bin"
|
||||
|
||||
For more options:
|
||||
> node index.js patch --help
|
||||
> node index.js create --help
|
||||
*/
|
||||
|
||||
|
||||
const chalk=require('chalk');
|
||||
const { program } = require('commander')
|
||||
const RomPatcher = require('./app/RomPatcher');
|
||||
|
||||
|
||||
program
|
||||
.command('patch')
|
||||
.description('patches a ROM')
|
||||
.argument('<rom_file>','the ROM file that will be patched')
|
||||
.argument('<patch_file>', 'the patch to apply')
|
||||
.option('-v, --validate-checksum','should validate checksum')
|
||||
.option('-h1, --add-header','adds a temporary header to the provided ROM for patches that require headered ROMs')
|
||||
.option('-h0, --remove-header','removes ROM header temporarily for patches that require headerless ROMs')
|
||||
.option('-f, --fix-checksum','fixes any known ROM header checksum if possible')
|
||||
.option('-s, --output-suffix','add a (patched) suffix to output ROM file name')
|
||||
.action(function(romPath, patchPath, options) {
|
||||
try{
|
||||
const romFile=new BinFile(romPath);
|
||||
const patchFile=new BinFile(patchPath);
|
||||
|
||||
const patch=RomPatcher.parsePatchFile(patchFile);
|
||||
if(!patch)
|
||||
throw new Error('Invalid patch file');
|
||||
|
||||
const patchedRom=RomPatcher.applyPatch(romFile, patch, options);
|
||||
patchedRom.save();
|
||||
console.log(chalk.green('successfully saved to ' + patchedRom.fileName));
|
||||
}catch(err){
|
||||
console.log(chalk.bgRed('error: ' + err.message));
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.description('creates a patch based on two ROMs')
|
||||
.argument('<original_rom_file>', 'the original ROM')
|
||||
.argument('<modified_rom_file>','the modified ROM')
|
||||
.option('-f, --format','patch format (allowed values: ips [default], bps, ppf, ups, aps, rup)')
|
||||
.action(function(originalRomPath, modifiedRomPath, options) {
|
||||
try{
|
||||
const originalFile=new BinFile(originalRomPath);
|
||||
const modifiedFile=new BinFile(modifiedRomPath);
|
||||
|
||||
const patch=RomPatcher.createPatch(originalFile, modifiedFile, options.format);
|
||||
}catch(err){
|
||||
console.log(chalk.bgBlue('Error: ' + err.message));
|
||||
}
|
||||
});
|
||||
|
||||
program.parse()
|
126
index_template.html
Normal file
|
@ -0,0 +1,126 @@
|
|||
<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
|
||||
<head>
|
||||
<title>Rom Patcher JS - Custom patcher template</title>
|
||||
<meta http-equiv="content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
||||
|
||||
|
||||
|
||||
<!-- Rom Patcher JS needed CSS/JS files -->
|
||||
<link type="text/css" rel="stylesheet" href="./rom-patcher-js/style.css" media="all" />
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/BinFile.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/HashCalculator.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.ips.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.ups.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.aps_n64.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.aps_gba.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.bps.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.rup.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.ppf.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.pmsr.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/RomPatcher.format.vcdiff.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/modules/zip.js/zip.min.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/RomPatcher.js"></script>
|
||||
<script type="text/javascript" src="./rom-patcher-js/RomPatcher.webapp.js"></script>
|
||||
|
||||
<!-- Rom Patcher JS initizlization -->
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function (evt) {
|
||||
try {
|
||||
RomPatcherWeb.initialize({
|
||||
language: 'en', //default: en
|
||||
}, {
|
||||
file: './my_patch.zip', //zip containing patches
|
||||
name: 'Game (English v1.0)',
|
||||
description: 'English fan translation for Game',
|
||||
outputName: 'Game (English v1.0)', //patched ROM name
|
||||
});
|
||||
} catch (err) {
|
||||
document.getElementById('rom-patcher-container').innerHTML = err.message;
|
||||
document.getElementById('rom-patcher-container').style.color = 'red';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="font: 15px 'Open Sans', sans-serif; background-color: #31343a; color: #e4e4e6;">
|
||||
<header style="text-align: center">
|
||||
<h1>Rom Patcher JS</h1>
|
||||
<p>
|
||||
This is a template that shows off Rom Patcher JS embeding capabilities. You can use this template to embed
|
||||
Rom Patcher JS in your website.<br />
|
||||
Take a look at the <a
|
||||
href="https://github.com/marcrobledo/RomPatcher.js/blob/master/custom_patcher_template.html"
|
||||
target="_blank">sourcecode</a> to see how it's done.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- Rom Patcher JS container -->
|
||||
<!--
|
||||
The following elements are required for Rom Patcher JS to work:
|
||||
#rom-patcher-input-file-rom
|
||||
#rom-patcher-input-file-patch
|
||||
#rom-patcher-button-apply
|
||||
The rest of elements are informative and can be removed, though it's recommended to keep them for a better user experience.
|
||||
-->
|
||||
<div id="rom-patcher-container">
|
||||
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-file-rom">
|
||||
<div class="text-right"><label for="rom-patcher-input-file-rom" data-localize="yes">ROM file:</label></div>
|
||||
<div class="rom-patcher-container-input">
|
||||
<input type="file" id="rom-patcher-input-file-rom" class="empty" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="margin-bottom text-selectable text-mono text-muted" id="rom-patcher-rom-info">
|
||||
<div class="rom-patcher-row">
|
||||
<div class="text-right">CRC32:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-crc32"></span></div>
|
||||
</div>
|
||||
<div class="rom-patcher-row">
|
||||
<div class="text-right">MD5:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-md5"></span></div>
|
||||
</div>
|
||||
<div class="rom-patcher-row">
|
||||
<div class="text-right">SHA-1:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-sha1"></span></div>
|
||||
</div>
|
||||
<div class="rom-patcher-row" id="rom-patcher-row-info-rom">
|
||||
<div class="text-right">ROM:</div>
|
||||
<div class="text-truncate"><span id="rom-patcher-span-rom-info"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-file-patch">
|
||||
<div class="text-right"><label for="rom-patcher-input-file-patch" data-localize="yes">Patch file:</label>
|
||||
</div>
|
||||
<div class="rom-patcher-container-input">
|
||||
<input type="file" id="rom-patcher-input-file-patch" class="empty"
|
||||
accept=".ips,.ups,.bps,.aps,.rup,.ppf,.mod,.xdelta,.vcdiff,.zip" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-description">
|
||||
<div class="text-right text-mono text-muted" data-localize="yes">Description:</div>
|
||||
<div class="text-truncate" id="rom-patcher-patch-description"></div>
|
||||
</div>
|
||||
<div class="rom-patcher-row margin-bottom text-selectable text-mono text-muted"
|
||||
id="rom-patcher-row-patch-requirements">
|
||||
<div class="text-right text-mono" id="rom-patcher-patch-requirements-type">ROM requirements:</div>
|
||||
<div class="text-truncate" id="rom-patcher-patch-requirements-value"></div>
|
||||
</div>
|
||||
|
||||
<div class="text-center" id="rom-patcher-row-apply">
|
||||
<div id="rom-patcher-row-error-message" class="margin-bottom"><span id="rom-patcher-error-message"></span>
|
||||
</div>
|
||||
<button id="rom-patcher-button-apply" data-localize="yes" disabled>Apply patch</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rom-patcher-powered" class="text-center">
|
||||
<a href="https://github.com/marcrobledo/RomPatcher.js" target="_blank"><img
|
||||
src="rom-patcher-js/assets/powered_by_rom_patcher_js.png" loading="lazy" />Powered by Rom Patcher JS</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
0
legacy/.nojekyll
Normal file
25
legacy/LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017-2023 Marc Robledo
|
||||
|
||||
This project incorporates components from Octicons
|
||||
(https://github.com/primer/octicons/) Copyright (c) 2023 GitHub Inc.,
|
||||
also released under MIT license.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
2
legacy/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Rom Patcher JS - Legacy version
|
||||
This is the legacy 2.9.1 Rom Patcher JS, kept for compatibility purposes.
|
207
legacy/index.html
Normal file
|
@ -0,0 +1,207 @@
|
|||
<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<title>Rom Patcher JS</title>
|
||||
<meta http-equiv="content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, PPF and xdelta."/>
|
||||
<meta name="keywords" content="ips,ups,aps,bps,rup,ninja,ppf,xdelta,patcher,online,html5,web,rom,patch,hack,translation"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
|
||||
<link rel="manifest" href="./manifest.json"/>
|
||||
<link rel="shortcut icon" href="./style/app_icon_16.png" type="image/png" sizes="16x16"/>
|
||||
<link rel="shortcut icon" href="./style/app_icon_192.png" type="image/png" sizes="192x192"/>
|
||||
<!-- iOS icons -->
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./style/app_icon_114.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="./style/app_icon_114.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./style/app_icon_144.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./style/app_icon_144.png" />
|
||||
<link rel="apple-touch-icon" href="./style/app_icon_192.png" />
|
||||
|
||||
<!-- cache -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
|
||||
<!-- social network metatags -->
|
||||
<meta name="twitter:site" content="@marc_robledo">
|
||||
<meta name="twitter:creator" content="@marc_robledo">
|
||||
<meta name="twitter:domain" content="marcrobledo.com">
|
||||
<meta property="og:title" content="Rom Patcher JS">
|
||||
<meta name="twitter:title" content="Rom Patcher JS">
|
||||
<meta name="twitter:description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, PPF and xdelta.">
|
||||
<meta property="og:image" content="https://www.marcrobledo.com/RomPatcher.js/style/thumbnail.jpg">
|
||||
<meta name="twitter:image" content="https://www.marcrobledo.com/RomPatcher.js/style/thumbnail.jpg">
|
||||
<meta name="twitter:card" content="photo">
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="./style/RomPatcher.css" media="all"/>
|
||||
<script type="text/javascript" src="./js/locale.js"></script>
|
||||
<script type="text/javascript" src="./js/RomPatcher.js"></script>
|
||||
<script type="text/javascript" src="./js/MarcFile.js"></script>
|
||||
<script type="text/javascript" src="./js/crc.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/zip.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/ips.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/ups.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/aps_n64.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/aps_gba.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/bps.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/rup.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/ppf.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/pmsr.js"></script>
|
||||
<script type="text/javascript" src="./js/formats/vcdiff.js"></script>
|
||||
|
||||
<script type="text/javascript" src="./js/zip.js/zip.js"></script>
|
||||
</head>
|
||||
<body><div id="column">
|
||||
|
||||
|
||||
<!-- HEADER -->
|
||||
<header><img src="./style/logo.png" /><h1>Rom Patcher JS</h1></header>
|
||||
|
||||
<!-- APP -->
|
||||
<div id="wrapper">
|
||||
<div id="switch-container"><span id="switch-create-button" class="button-outer"><span data-localize="creator_mode">Creator mode</span> <span id="switch-create" class="switch disabled"></span></span></div>
|
||||
|
||||
<div id="tab0" class="tab">
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right"><label for="input-file-rom" data-localize="rom_file">ROM file:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-rom" class="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b" id="rom-info">
|
||||
<div class="leftcol text-right">CRC32:</div><div class="rightcol"><span id="crc32"></span></div>
|
||||
<div class="leftcol text-right">MD5:</div><div class="rightcol"><span id="md5"></span></div>
|
||||
<div class="leftcol text-right">SHA-1:</div><div class="rightcol"><span id="sha1"></span></div>
|
||||
</div>
|
||||
<div class="row m-b hide" id="row-removeheader">
|
||||
<div class="leftcol text-right"></div>
|
||||
<div class="rightcol">
|
||||
<input type="checkbox" id="checkbox-removeheader" /> <label for="checkbox-removeheader" data-localize="remove_header">Remove header before patching</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b hide" id="row-addheader">
|
||||
<div class="leftcol text-right"></div>
|
||||
<div class="rightcol">
|
||||
<input type="checkbox" id="checkbox-addheader" /> <label for="checkbox-addheader" data-localize="add_header">Add temporary header</label> <small>(<label id="headersize" for="checkbox-addheader"></label>)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b" id="row-file-patch">
|
||||
<div class="leftcol text-right"><label for="input-file-patch" data-localize="patch_file">Patch file:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-patch" accept=".ips,.ups,.bps,.aps,.rup,.ppf,.mod,.xdelta,.vcdiff,.zip"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b hide" id="row-expected-source-info"></div>
|
||||
|
||||
<div class="buttons">
|
||||
<span id="message-apply" class="message"></span>
|
||||
<button id="button-apply" data-localize="apply_patch" class="disabled" disabled>Apply patch</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="tab1" class="tab">
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right"><label for="input-file-rom1" data-localize="original_rom" >Original ROM:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-rom1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right"><label for="input-file-rom2" data-localize="modified_rom">Modified ROM:</label></div>
|
||||
<div class="rightcol">
|
||||
<input type="file" id="input-file-rom2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol text-right" data-localize="patch_type">Patch type:</div>
|
||||
<div class="rightcol">
|
||||
<select id="select-patch-type">
|
||||
<option value="ips">IPS</option>
|
||||
<option value="bps">BPS</option>
|
||||
<option value="ppf">PPF</option>
|
||||
<option value="ups">UPS</option>
|
||||
<option value="aps">APS</option>
|
||||
<option value="rup">RUP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<span id="message-create" class="message"></span>
|
||||
<button id="button-create" class="disabled" disabled data-localize="create_patch">Create patch</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div>
|
||||
<button id="button-settings" class="button-outer"><img src="style/icon_settings.svg" class="icon settings" /> <span data-localize="settings">Settings</span></button>
|
||||
</div>
|
||||
|
||||
Rom Patcher JS <small>v2.9.1 <a style="color:rgb(255, 197, 7)" href="../" rel="nofollow">Legacy</a></small> by <a href="/">Marc Robledo</a>
|
||||
<br />
|
||||
<img src="style/icon_github.svg" class="icon github" /> <a href="https://github.com/marcrobledo/RomPatcher.js/" target="_blank">See on GitHub</a>
|
||||
<img src="style/icon_heart.svg" class="icon heart" /> <a href="https://www.paypal.me/marcrobledo/5" target="_blank" rel="nofollow">Donate</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- SETTINGS DIALOG -->
|
||||
<div id="dialog-backdrop">
|
||||
<div id="zip-dialog" class="dialog">
|
||||
<div id="zip-dialog-message" class="text-center"></div>
|
||||
<ul id="zip-dialog-file-list"></ul>
|
||||
</div>
|
||||
|
||||
<div id="settings-dialog" class="dialog">
|
||||
<div class="text-right m-b"><img id="settings-close-dialog" src="style/icon_close.svg" /></div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol"><label for="select-language">Language</label></div>
|
||||
<div class="rightcol text-right">
|
||||
<select id="select-language" class="enabled w100">
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="nl">Nederlands</option>
|
||||
<option value="sv">Svenska</option>
|
||||
<option value="ca">Català</option>
|
||||
<option value="ca-va">Valencià</option>
|
||||
<option value="pt-br">Português Brasileiro</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
<option value="zh-tw">中文(正體)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol-lg"><label data-localize="alternate_output_name">Use patch name for output</label></div>
|
||||
<div class="rightcol-lg text-right"><span id="switch-output-name" class="switch disabled"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="row m-b">
|
||||
<div class="leftcol-lg"><label data-localize="fix_checksum">Fix ROM checksum</label></div>
|
||||
<div class="rightcol-lg text-right"><span id="switch-fix-checksum" class="switch disabled"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="leftcol-lg"><label data-localize="light_theme">Light theme</label></div>
|
||||
<div class="rightcol-lg text-right"><span id="switch-theme" class="switch disabled"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
|
@ -12,20 +12,22 @@ const HEADERS_INFO=[
|
|||
|
||||
|
||||
/* service worker */
|
||||
/*
|
||||
const FORCE_HTTPS=true;
|
||||
if(FORCE_HTTPS && location.protocol==='http:')
|
||||
location.href=window.location.href.replace('http:','https:');
|
||||
else if(location.protocol==='https:' && 'serviceWorker' in navigator && window.location.hostname==='www.marcrobledo.com')
|
||||
navigator.serviceWorker.register('/RomPatcher.js/_cache_service_worker.js', {scope: '/RomPatcher.js/'});
|
||||
|
||||
*/
|
||||
|
||||
|
||||
var romFile, patchFile, patch, romFile1, romFile2, tempFile, headerSize, oldHeader;
|
||||
|
||||
var CAN_USE_WEB_WORKERS=true;
|
||||
var WEBWORKERS_PATH='./js/';
|
||||
var webWorkerApply,webWorkerCreate,webWorkerCrc;
|
||||
try{
|
||||
webWorkerApply=new Worker('./js/worker_apply.js');
|
||||
webWorkerApply=new Worker(WEBWORKERS_PATH + 'worker_apply.js');
|
||||
webWorkerApply.onmessage = event => { // listen for events from the worker
|
||||
//retrieve arraybuffers back from webworker
|
||||
if(!el('checkbox-removeheader').checked && !el('checkbox-addheader').checked){ //when adding/removing header we don't need the arraybuffer back since we made a copy previously
|
||||
|
@ -51,7 +53,7 @@ try{
|
|||
|
||||
|
||||
|
||||
webWorkerCreate=new Worker('./js/worker_create.js');
|
||||
webWorkerCreate=new Worker(WEBWORKERS_PATH + 'worker_create.js');
|
||||
webWorkerCreate.onmessage = event => { // listen for events from the worker
|
||||
var newPatchFile=new MarcFile(event.data.patchFileU8Array);
|
||||
newPatchFile.fileName=romFile2.fileName.replace(/\.[^\.]+$/,'')+'.'+el('select-patch-type').value;
|
||||
|
@ -67,7 +69,7 @@ try{
|
|||
|
||||
|
||||
|
||||
webWorkerCrc=new Worker('./js/worker_crc.js');
|
||||
webWorkerCrc=new Worker(WEBWORKERS_PATH + 'worker_crc.js');
|
||||
webWorkerCrc.onmessage = event => { // listen for events from the worker
|
||||
//console.log('received_crc');
|
||||
el('crc32').innerHTML=padZeroes(event.data.crc32, 4);
|
||||
|
@ -257,6 +259,8 @@ var UI={
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
var APP_SETTINGS_ID='rom-patcher-js-settings-legacy';
|
||||
var AppSettings={
|
||||
langCode:(typeof navigator.userLanguage==='string')? navigator.userLanguage.substr(0,2) : 'en',
|
||||
outputFileNameMatch:false,
|
||||
|
@ -264,9 +268,9 @@ var AppSettings={
|
|||
lightTheme:false,
|
||||
|
||||
load:function(){
|
||||
if(typeof localStorage!=='undefined' && localStorage.getItem('rompatcher-js-settings')){
|
||||
if(typeof localStorage!=='undefined' && localStorage.getItem(APP_SETTINGS_ID)){
|
||||
try{
|
||||
var loadedSettings=JSON.parse(localStorage.getItem('rompatcher-js-settings'));
|
||||
var loadedSettings=JSON.parse(localStorage.getItem(APP_SETTINGS_ID));
|
||||
|
||||
if(typeof loadedSettings.langCode==='string' && typeof LOCALIZATION[loadedSettings.langCode]){
|
||||
this.langCode=loadedSettings.langCode;
|
||||
|
@ -293,7 +297,7 @@ var AppSettings={
|
|||
|
||||
save:function(){
|
||||
if(typeof localStorage!=='undefined')
|
||||
localStorage.setItem('rompatcher-js-settings', JSON.stringify(this));
|
||||
localStorage.setItem(APP_SETTINGS_ID, JSON.stringify(this));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -302,12 +306,12 @@ addEvent(window,'load',function(){
|
|||
/* zip-js web worker */
|
||||
if(CAN_USE_WEB_WORKERS){
|
||||
zip.useWebWorkers=true;
|
||||
zip.workerScriptsPath='./js/zip.js/';
|
||||
zip.workerScriptsPath=WEBWORKERS_PATH + 'zip.js/';
|
||||
}else{
|
||||
zip.useWebWorkers=false;
|
||||
|
||||
var script=document.createElement('script');
|
||||
script.src='./js/zip.js/inflate.js';
|
||||
script.src=WEBWORKERS_PATH + 'zip.js/inflate.js';
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
}
|
||||
|
32
legacy/manifest.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"short_name":"Rom Patcher JS",
|
||||
"name":"Rom Patcher JS",
|
||||
"icons":[
|
||||
{
|
||||
"src": "style/app_icon_114.png",
|
||||
"sizes": "114x114",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},{
|
||||
"src": "style/app_icon_144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},{
|
||||
"src": "style/app_icon_192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},{
|
||||
"src": "style/app_icon_maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "index.html",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"theme_color": "#31343a",
|
||||
"background_color": "#31343a"
|
||||
}
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 202 B After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 206 B |
Before Width: | Height: | Size: 760 B After Width: | Height: | Size: 760 B |
Before Width: | Height: | Size: 729 B After Width: | Height: | Size: 729 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
@ -3,28 +3,28 @@
|
|||
"name":"Rom Patcher JS",
|
||||
"icons":[
|
||||
{
|
||||
"src": "style/app_icon_114.png",
|
||||
"src": "webapp/app_icon_114.png",
|
||||
"sizes": "114x114",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},{
|
||||
"src": "style/app_icon_144.png",
|
||||
"src": "webapp/app_icon_144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},{
|
||||
"src": "style/app_icon_192.png",
|
||||
"src": "webapp/app_icon_192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},{
|
||||
"src": "style/app_icon_maskable.png",
|
||||
"src": "webapp/app_icon_maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "index.html",
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"theme_color": "#31343a",
|
||||
|
|
19
package.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"//" : [
|
||||
"not using type:module (for now) to ensure compatibility with old both browsers and Node",
|
||||
"latest chalk version works only as a ES6 module, so we force the usage of the last CommonJS compatible version"
|
||||
],
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"commander": "^11.0.0"
|
||||
},
|
||||
"name": "rom-patcher",
|
||||
"description": "A ROM patcher made in Javascript.",
|
||||
"version": "3.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node test.js"
|
||||
},
|
||||
"author": "Marc Robledo",
|
||||
"license": "MIT"
|
||||
}
|
405
rom-patcher-js/RomPatcher.js
Normal file
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* Rom Patcher JS core
|
||||
* A ROM patcher/builder made in JavaScript, can be implemented as a webapp or a Node.JS CLI tool
|
||||
* By Marc Robledo https://www.marcrobledo.com
|
||||
* Sourcecode: https://github.com/marcrobledo/RomPatcher.js
|
||||
* License:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2016-2024 Marc Robledo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
const RomPatcher = (function () {
|
||||
const TOO_BIG_ROM_SIZE = 67108863;
|
||||
|
||||
const HEADERS_INFO = [
|
||||
{ extensions: ['nes'], size: 16, romSizeMultiple: 1024, name: 'iNES' }, /* https://www.nesdev.org/wiki/INES */
|
||||
{ extensions: ['fds'], size: 16, romSizeMultiple: 65500, name: 'fwNES' }, /* https://www.nesdev.org/wiki/FDS_file_format */
|
||||
{ extensions: ['lnx'], size: 64, romSizeMultiple: 1024, name: 'LNX' },
|
||||
{ extensions: ['sfc', 'smc', 'swc', 'fig'], size: 512, romSizeMultiple: 262144, name: 'SNES copier' },
|
||||
];
|
||||
|
||||
const GAME_BOY_NINTENDO_LOGO = [
|
||||
0xce, 0xed, 0x66, 0x66, 0xcc, 0x0d, 0x00, 0x0b, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0c, 0x00, 0x0d,
|
||||
0x00, 0x08, 0x11, 0x1f, 0x88, 0x89, 0x00, 0x0e, 0xdc, 0xcc, 0x6e, 0xe6, 0xdd, 0xdd, 0xd9, 0x99
|
||||
];
|
||||
|
||||
|
||||
|
||||
const _getRomSystem = function (binFile) {
|
||||
/* to-do: add more systems */
|
||||
const extension = binFile.getExtension().trim();
|
||||
if (binFile.fileSize > 0x0200 && binFile.fileSize % 4 === 0) {
|
||||
if ((extension === 'gb' || extension === 'gbc') && binFile.fileSize % 0x4000 === 0) {
|
||||
binFile.seek(0x0104);
|
||||
var valid = true;
|
||||
for (var i = 0; i < GAME_BOY_NINTENDO_LOGO.length && valid; i++) {
|
||||
if (GAME_BOY_NINTENDO_LOGO[i] !== binFile.readU8())
|
||||
valid = false;
|
||||
}
|
||||
if (valid)
|
||||
return 'gb';
|
||||
} else if (extension === 'md' || extension === 'bin') {
|
||||
binFile.seek(0x0100);
|
||||
if (/SEGA (GENESIS|MEGA DR)/.test(binFile.readString(12)))
|
||||
return 'smd';
|
||||
} else if (extension === 'z64' && binFile.fileSize >= 0x400000) {
|
||||
return 'n64'
|
||||
}
|
||||
} else if (extension === 'fds' && binFile.fileSize % 65500 === 0) {
|
||||
return 'fds'
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const _getRomAdditionalChecksum = function (binFile) {
|
||||
/* to-do: add more systems */
|
||||
const romSystem = _getRomSystem(binFile);
|
||||
if (romSystem === 'n64') {
|
||||
binFile.seek(0x3c);
|
||||
const cartId = binFile.readString(3);
|
||||
|
||||
binFile.seek(0x10);
|
||||
const crc = binFile.readBytes(8).reduce(function (hex, b) {
|
||||
if (b < 16)
|
||||
return hex + '0' + b.toString(16);
|
||||
else
|
||||
return hex + b.toString(16);
|
||||
}, '');
|
||||
return cartId + ' (' + crc + ')';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
parsePatchFile: function (patchFile) {
|
||||
if (!(patchFile instanceof BinFile))
|
||||
throw new Error('Patch file is not an instance of BinFile');
|
||||
|
||||
patchFile.littleEndian = false;
|
||||
patchFile.seek(0);
|
||||
|
||||
var header = patchFile.readString(6);
|
||||
var patch = null;
|
||||
if (header.startsWith(IPS.MAGIC)) {
|
||||
patch = IPS.fromFile(patchFile);
|
||||
} else if (header.startsWith(UPS.MAGIC)) {
|
||||
patch = UPS.fromFile(patchFile);
|
||||
} else if (header.startsWith(APS.MAGIC)) {
|
||||
patch = APS.fromFile(patchFile);
|
||||
} else if (header.startsWith(APSGBA.MAGIC)) {
|
||||
patch = APSGBA.fromFile(patchFile);
|
||||
} else if (header.startsWith(BPS.MAGIC)) {
|
||||
patch = BPS.fromFile(patchFile);
|
||||
} else if (header.startsWith(RUP.MAGIC)) {
|
||||
patch = RUP.fromFile(patchFile);
|
||||
} else if (header.startsWith(PPF.MAGIC)) {
|
||||
patch = PPF.fromFile(patchFile);
|
||||
} else if (header.startsWith(PMSR.MAGIC)) {
|
||||
patch = PMSR.fromFile(patchFile);
|
||||
} else if (header.startsWith(VCDIFF.MAGIC)) {
|
||||
patch = VCDIFF.fromFile(patchFile);
|
||||
}
|
||||
|
||||
if (patch)
|
||||
patch._originalPatchFile = patchFile;
|
||||
|
||||
return patch;
|
||||
},
|
||||
|
||||
validateRom: function (romFile, patch, skipHeaderSize) {
|
||||
if (!(romFile instanceof BinFile))
|
||||
throw new Error('ROM file is not an instance of BinFile');
|
||||
else if (typeof patch !== 'object')
|
||||
throw new Error('Unknown patch format');
|
||||
|
||||
|
||||
if (typeof skipHeaderSize !== 'number' || skipHeaderSize < 0)
|
||||
skipHeaderSize = 0;
|
||||
|
||||
if (
|
||||
typeof patch.validateSource === 'function' && !patch.validateSource(romFile, skipHeaderSize)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
applyPatch: function (romFile, patch, optionsParam) {
|
||||
if (!(romFile instanceof BinFile))
|
||||
throw new Error('ROM file is not an instance of BinFile');
|
||||
else if (typeof patch !== 'object')
|
||||
throw new Error('Unknown patch format');
|
||||
|
||||
|
||||
const options = {
|
||||
requireValidation: false,
|
||||
removeHeader: false,
|
||||
addHeader: false,
|
||||
fixChecksum: false,
|
||||
outputSuffix: true
|
||||
};
|
||||
if (typeof optionsParam === 'object') {
|
||||
if (typeof optionsParam.requireValidation !== 'undefined')
|
||||
options.requireValidation = !!optionsParam.requireValidation;
|
||||
if (typeof optionsParam.removeHeader !== 'undefined')
|
||||
options.removeHeader = !!optionsParam.removeHeader;
|
||||
if (typeof optionsParam.addHeader !== 'undefined')
|
||||
options.addHeader = !!optionsParam.addHeader;
|
||||
if (typeof optionsParam.fixChecksum !== 'undefined')
|
||||
options.fixChecksum = !!optionsParam.fixChecksum;
|
||||
if (typeof optionsParam.outputSuffix !== 'undefined')
|
||||
options.outputSuffix = !!optionsParam.outputSuffix;
|
||||
}
|
||||
|
||||
var extractedHeader = false;
|
||||
var fakeHeaderSize = 0;
|
||||
if (options.removeHeader) {
|
||||
const headerInfo = RomPatcher.isRomHeadered(romFile);
|
||||
if (headerInfo) {
|
||||
const splitData = RomPatcher.removeHeader(romFile);
|
||||
extractedHeader = splitData.header;
|
||||
romFile = splitData.rom;
|
||||
}
|
||||
} else if (options.addHeader) {
|
||||
const headerInfo = RomPatcher.canRomGetHeader(romFile);
|
||||
if (headerInfo) {
|
||||
fakeHeaderSize = headerInfo.fileSize;
|
||||
romFile = RomPatcher.addFakeHeader(romFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.requireValidation && !RomPatcher.validateRom(romFile, patch)) {
|
||||
throw new Error('Invalid input ROM checksum');
|
||||
}
|
||||
|
||||
var patchedRom = patch.apply(romFile);
|
||||
if (extractedHeader) {
|
||||
/* reinsert header */
|
||||
if (options.fixChecksum)
|
||||
RomPatcher.fixRomHeaderChecksum(patchedRom);
|
||||
|
||||
const patchedRomWithHeader = new BinFile(extractedHeader.fileSize + patchedRom.fileSize);
|
||||
patchedRomWithHeader.fileName = patchedRom.fileName;
|
||||
patchedRomWithHeader.fileType = patchedRom.fileType;
|
||||
extractedHeader.copyTo(patchedRomWithHeader, 0, extractedHeader.fileSize);
|
||||
patchedRom.copyTo(patchedRomWithHeader, 0, patchedRom.fileSize, extractedHeader.fileSize);
|
||||
|
||||
patchedRom = patchedRomWithHeader;
|
||||
} else if (fakeHeaderSize) {
|
||||
/* remove fake header */
|
||||
const patchedRomWithoutFakeHeader = patchedRom.slice(fakeHeaderSize);
|
||||
|
||||
if (options.fixChecksum)
|
||||
RomPatcher.fixRomHeaderChecksum(patchedRomWithoutFakeHeader);
|
||||
|
||||
patchedRom = patchedRomWithoutFakeHeader;
|
||||
|
||||
} else if (options.fixChecksum) {
|
||||
RomPatcher.fixRomHeaderChecksum(patchedRom);
|
||||
}
|
||||
|
||||
if (options.outputSuffix) {
|
||||
patchedRom.fileName = romFile.fileName.replace(/\.([^\.]*?)$/, ' (patched).$1');
|
||||
} else if (patch._originalPatchFile) {
|
||||
patchedRom.fileName = patch._originalPatchFile.fileName.replace(/\.\w+$/i, (/\.\w+$/i.test(romFile.fileName) ? romFile.fileName.match(/\.\w+$/i)[0] : ''));
|
||||
} else {
|
||||
patchedRom.fileName = romFile.fileName;
|
||||
}
|
||||
|
||||
return patchedRom;
|
||||
},
|
||||
|
||||
createPatch: function (originalFile, modifiedFile, format) {
|
||||
if (!(originalFile instanceof BinFile))
|
||||
throw new Error('Original ROM file is not an instance of BinFile');
|
||||
else if (!(modifiedFile instanceof BinFile))
|
||||
throw new Error('Modified ROM file is not an instance of BinFile');
|
||||
|
||||
if (typeof format === 'string')
|
||||
format = format.trim().toLowerCase();
|
||||
else if (typeof format === 'undefined')
|
||||
format = 'ips';
|
||||
|
||||
var patch;
|
||||
if (format === 'ips') {
|
||||
patch = IPS.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'bps') {
|
||||
patch = BPS.buildFromRoms(originalFile, modifiedFile, (originalFile.fileSize <= 4194304));
|
||||
} else if (format === 'ppf') {
|
||||
patch = PPF.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'ups') {
|
||||
patch = UPS.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'aps') {
|
||||
patch = APS.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'rup') {
|
||||
patch = RUP.buildFromRoms(originalFile, modifiedFile);
|
||||
} else {
|
||||
throw new Error('Invalid patch format');
|
||||
}
|
||||
|
||||
if (
|
||||
!(format === 'ppf' && originalFile.fileSize > modifiedFile.fileSize) && //skip verification if PPF and PPF+modified size>original size
|
||||
modifiedFile.hashCRC32() !== patch.apply(originalFile).hashCRC32()
|
||||
) {
|
||||
//throw new Error('Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.');
|
||||
}
|
||||
return patch;
|
||||
},
|
||||
|
||||
|
||||
/* check if ROM can inject a fake header (for patches that require a headered ROM) */
|
||||
canRomGetHeader: function (romFile) {
|
||||
if (romFile.fileSize <= 0x600000) {
|
||||
const compatibleHeader = HEADERS_INFO.find(headerInfo => headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 && romFile.fileSize % headerInfo.romSizeMultiple === 0);
|
||||
if (compatibleHeader) {
|
||||
return {
|
||||
name: compatibleHeader.name,
|
||||
size: compatibleHeader.size
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* check if ROM has a known header */
|
||||
isRomHeadered: function (romFile) {
|
||||
if (romFile.fileSize <= 0x600200 && romFile.fileSize % 1024 !== 0) {
|
||||
const compatibleHeader = HEADERS_INFO.find(headerInfo => headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 && (romFile.fileSize - headerInfo.size) % headerInfo.romSizeMultiple === 0);
|
||||
if (compatibleHeader) {
|
||||
return {
|
||||
name: compatibleHeader.name,
|
||||
size: compatibleHeader.size
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* remove ROM header */
|
||||
removeHeader: function (romFile) {
|
||||
const headerInfo = RomPatcher.isRomHeadered(romFile);
|
||||
if (headerInfo) {
|
||||
return {
|
||||
header: romFile.slice(0, headerInfo.size),
|
||||
rom: romFile.slice(headerInfo.size)
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* add fake ROM header */
|
||||
addFakeHeader: function (romFile) {
|
||||
const headerInfo = RomPatcher.canRomGetHeader(romFile);
|
||||
if (headerInfo) {
|
||||
const romWithFakeHeader = new BinFile(headerInfo.size + romFile.fileSize);
|
||||
romWithFakeHeader.fileName = romFile.fileName;
|
||||
romWithFakeHeader.fileType = romFile.fileType;
|
||||
romFile.copyTo(romWithFakeHeader, 0, romFile.fileSize, headerInfo.size);
|
||||
|
||||
//add a correct FDS header
|
||||
if (_getRomSystem(romWithFakeHeader) === 'fds') {
|
||||
romWithFakeHeader.seek(0);
|
||||
romWithFakeHeader.writeBytes([0x46, 0x44, 0x53, 0x1a, romFile.fileSize / 65500]);
|
||||
}
|
||||
|
||||
romWithFakeHeader.fakeHeader = true;
|
||||
|
||||
return romWithFakeHeader;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* get ROM internal checksum, if possible */
|
||||
fixRomHeaderChecksum: function (romFile) {
|
||||
const romSystem = _getRomSystem(romFile);
|
||||
|
||||
if (romSystem === 'gb') {
|
||||
/* get current checksum */
|
||||
romFile.seek(0x014d);
|
||||
const currentChecksum = romFile.readU8();
|
||||
|
||||
/* calculate checksum */
|
||||
var newChecksum = 0x00;
|
||||
romFile.seek(0x0134);
|
||||
for (var i = 0; i <= 0x18; i++) {
|
||||
newChecksum = ((newChecksum - romFile.readU8() - 1) >>> 0) & 0xff;
|
||||
}
|
||||
|
||||
/* fix checksum */
|
||||
if (currentChecksum !== newChecksum) {
|
||||
console.log('fixed Game Boy checksum');
|
||||
romFile.seek(0x014d);
|
||||
romFile.writeU8(newChecksum);
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if (romSystem === 'smd') {
|
||||
/* get current checksum */
|
||||
romFile.seek(0x018e);
|
||||
const currentChecksum = romFile.readU16();
|
||||
|
||||
/* calculate checksum */
|
||||
var newChecksum = 0x0000;
|
||||
romFile.seek(0x0200);
|
||||
while (!romFile.isEOF()) {
|
||||
newChecksum = ((newChecksum + romFile.readU16()) >>> 0) & 0xffff;
|
||||
}
|
||||
|
||||
/* fix checksum */
|
||||
if (currentChecksum !== newChecksum) {
|
||||
console.log('fixed Megadrive/Genesis checksum');
|
||||
romFile.seek(0x018e);
|
||||
romFile.writeU16(newChecksum);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/* get ROM additional checksum info, if possible */
|
||||
getRomAdditionalChecksum: function (romFile) {
|
||||
return _getRomAdditionalChecksum(romFile);
|
||||
},
|
||||
|
||||
|
||||
/* check if ROM is too big */
|
||||
isRomTooBig: function (romFile) {
|
||||
return romFile && romFile.fileSize > TOO_BIG_ROM_SIZE;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = RomPatcher;
|
||||
|
||||
IPS = require('./modules/RomPatcher.format.ips');
|
||||
UPS = require('./modules/RomPatcher.format.ups');
|
||||
APS = require('./modules/RomPatcher.format.aps_n64');
|
||||
APSGBA = require('./modules/RomPatcher.format.aps_gba');
|
||||
BPS = require('./modules/RomPatcher.format.bps');
|
||||
RUP = require('./modules/RomPatcher.format.rup');
|
||||
PPF = require('./modules/RomPatcher.format.ppf');
|
||||
PMSR = require('./modules/RomPatcher.format.pmsr');
|
||||
VCDIFF = require('./modules/RomPatcher.format.vcdiff');
|
||||
}
|
1762
rom-patcher-js/RomPatcher.webapp.js
Normal file
82
rom-patcher-js/RomPatcher.webworker.apply.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
self.importScripts(
|
||||
'./RomPatcher.js',
|
||||
'./modules/BinFile.js',
|
||||
'./modules/HashCalculator.js',
|
||||
'./modules/RomPatcher.format.ips.js',
|
||||
'./modules/RomPatcher.format.aps_n64.js',
|
||||
'./modules/RomPatcher.format.aps_gba.js',
|
||||
'./modules/RomPatcher.format.ups.js',
|
||||
'./modules/RomPatcher.format.bps.js',
|
||||
'./modules/RomPatcher.format.rup.js',
|
||||
'./modules/RomPatcher.format.ppf.js',
|
||||
'./modules/RomPatcher.format.pmsr.js',
|
||||
'./modules/RomPatcher.format.vcdiff.js'
|
||||
);
|
||||
|
||||
|
||||
self.onmessage = event => { // listen for messages from the main thread
|
||||
const romFile=new BinFile(event.data.romFileU8Array);
|
||||
romFile.fileName=event.data.romFileName;
|
||||
//romFile.fileType.event.data.romFileType;
|
||||
const patchFile=new BinFile(event.data.patchFileU8Array);
|
||||
patchFile.fileName=event.data.patchFileName;
|
||||
|
||||
const patch=RomPatcher.parsePatchFile(patchFile);
|
||||
|
||||
var errorMessage=false;
|
||||
|
||||
|
||||
var patchedRom;
|
||||
if(patch){
|
||||
try{
|
||||
patchedRom=RomPatcher.applyPatch(romFile, patch, event.data.options);
|
||||
}catch(evt){
|
||||
errorMessage=evt.message;
|
||||
}
|
||||
}else{
|
||||
errorMessage='Invalid patch file';
|
||||
}
|
||||
|
||||
//console.log('postMessage');
|
||||
if(patchedRom){
|
||||
/* set custom output name if embeded patch */
|
||||
const patchExtraInfo=event.data.patchExtraInfo;
|
||||
if(patchExtraInfo){
|
||||
if(typeof patchExtraInfo.outputName === 'string')
|
||||
patchedRom.setName(patchExtraInfo.outputName);
|
||||
if(typeof patchExtraInfo.outputExtension === 'string')
|
||||
patchedRom.setExtension(patchExtraInfo.outputExtension);
|
||||
}
|
||||
|
||||
self.postMessage(
|
||||
{
|
||||
success: !!errorMessage,
|
||||
romFileU8Array:event.data.romFileU8Array,
|
||||
patchFileU8Array:event.data.patchFileU8Array,
|
||||
patchedRomU8Array:patchedRom._u8array,
|
||||
patchedRomFileName:patchedRom.fileName,
|
||||
errorMessage:errorMessage
|
||||
},
|
||||
[
|
||||
event.data.romFileU8Array.buffer,
|
||||
event.data.patchFileU8Array.buffer,
|
||||
patchedRom._u8array.buffer
|
||||
]
|
||||
);
|
||||
}else{
|
||||
self.postMessage(
|
||||
{
|
||||
success: false,
|
||||
romFileU8Array:event.data.romFileU8Array,
|
||||
patchFileU8Array:event.data.patchFileU8Array,
|
||||
errorMessage:errorMessage
|
||||
},
|
||||
[
|
||||
event.data.romFileU8Array.buffer,
|
||||
event.data.patchFileU8Array.buffer
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
26
rom-patcher-js/RomPatcher.webworker.crc.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
self.importScripts(
|
||||
'./RomPatcher.js',
|
||||
'./modules/BinFile.js',
|
||||
'./modules/HashCalculator.js'
|
||||
);
|
||||
|
||||
|
||||
self.onmessage = event => { // listen for messages from the main thread
|
||||
const binFile=new BinFile(event.data.u8array);
|
||||
binFile.fileName=event.data.fileName;
|
||||
const startOffset=typeof event.data.checksumStartOffset==='number'? event.data.checksumStartOffset : 0;
|
||||
|
||||
self.postMessage(
|
||||
{
|
||||
action: event.data.action,
|
||||
crc32:binFile.hashCRC32(startOffset),
|
||||
md5:binFile.hashMD5(startOffset),
|
||||
checksumStartOffset: startOffset,
|
||||
rom:RomPatcher.getRomAdditionalChecksum(binFile),
|
||||
u8array:event.data.u8array
|
||||
},
|
||||
[event.data.u8array.buffer]
|
||||
);
|
||||
};
|
36
rom-patcher-js/RomPatcher.webworker.create.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
self.importScripts(
|
||||
'./RomPatcher.js',
|
||||
'./modules/BinFile.js',
|
||||
'./modules/HashCalculator.js',
|
||||
'./modules/RomPatcher.format.ips.js',
|
||||
'./modules/RomPatcher.format.aps_n64.js',
|
||||
'./modules/RomPatcher.format.ups.js',
|
||||
'./modules/RomPatcher.format.bps.js',
|
||||
'./modules/RomPatcher.format.rup.js',
|
||||
'./modules/RomPatcher.format.ppf.js'
|
||||
);
|
||||
|
||||
|
||||
self.onmessage = event => { // listen for messages from the main thread
|
||||
const originalFile=new BinFile(event.data.originalRomU8Array);
|
||||
const modifiedFile=new BinFile(event.data.modifiedRomU8Array);
|
||||
const format=event.data.format;
|
||||
|
||||
const patch=RomPatcher.createPatch(originalFile, modifiedFile, format);
|
||||
const patchFile=patch.export('my_patch');
|
||||
|
||||
self.postMessage(
|
||||
{
|
||||
originalRomU8Array:event.data.originalRomU8Array,
|
||||
modifiedRomU8Array:event.data.modifiedRomU8Array,
|
||||
patchFileU8Array:patchFile._u8array
|
||||
},
|
||||
[
|
||||
event.data.originalRomU8Array.buffer,
|
||||
event.data.modifiedRomU8Array.buffer,
|
||||
patchFile._u8array.buffer
|
||||
]
|
||||
);
|
||||
};
|
1
rom-patcher-js/assets/icon_alert_orange.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#ff7800" d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>
|
After Width: | Height: | Size: 424 B |
1
rom-patcher-js/assets/icon_check_circle_green.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#080" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm1.5 0a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm10.28-1.72-4.5 4.5a.75.75 0 0 1-1.06 0l-2-2a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l1.47 1.47 3.97-3.97a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"></path></svg>
|
After Width: | Height: | Size: 364 B |
1
rom-patcher-js/assets/icon_upload.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path><path d="M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z"></path></svg>
|
After Width: | Height: | Size: 439 B |
1
rom-patcher-js/assets/icon_x_circle_red.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#f00" d="M2.344 2.343h-.001a8 8 0 0 1 11.314 11.314A8.002 8.002 0 0 1 .234 10.089a8 8 0 0 1 2.11-7.746Zm1.06 10.253a6.5 6.5 0 1 0 9.108-9.275 6.5 6.5 0 0 0-9.108 9.275ZM6.03 4.97 8 6.94l1.97-1.97a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l1.97 1.97a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-1.97 1.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L6.94 8 4.97 6.03a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018Z"></path></svg>
|
After Width: | Height: | Size: 557 B |
BIN
rom-patcher-js/assets/powered_by_rom_patcher_js.png
Normal file
After Width: | Height: | Size: 799 B |
475
rom-patcher-js/modules/BinFile.js
Normal file
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
* BinFile.js (last update: 2024-02-27)
|
||||
* by Marc Robledo, https://www.marcrobledo.com
|
||||
*
|
||||
* a JS class for reading/writing sequentially binary data from/to a file
|
||||
* that allows much more manipulation than simple DataView
|
||||
* compatible with both browsers and Node.js
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2014-2024 Marc Robledo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
function BinFile(source, onLoad) {
|
||||
this.littleEndian = false;
|
||||
this.offset = 0;
|
||||
this._lastRead = null;
|
||||
this._offsetsStack = [];
|
||||
|
||||
|
||||
if (
|
||||
BinFile.RUNTIME_ENVIROMENT === 'browser' && (
|
||||
source instanceof File ||
|
||||
source instanceof FileList ||
|
||||
(source instanceof HTMLElement && source.tagName === 'INPUT' && source.type === 'file')
|
||||
)
|
||||
) {
|
||||
if (source instanceof HTMLElement)
|
||||
source = source.files;
|
||||
if (source instanceof FileList)
|
||||
source = source[0];
|
||||
|
||||
this.fileName = source.name;
|
||||
this.fileType = source.type;
|
||||
this.fileSize = source.size;
|
||||
|
||||
if (typeof window.FileReader !== 'function')
|
||||
throw new Error('Incompatible browser');
|
||||
|
||||
this._fileReader = new FileReader();
|
||||
this._fileReader.addEventListener('load', function () {
|
||||
this.binFile._u8array = new Uint8Array(this.result);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this.binFile);
|
||||
}, false);
|
||||
|
||||
|
||||
this._fileReader.binFile = this;
|
||||
|
||||
this._fileReader.readAsArrayBuffer(source);
|
||||
|
||||
|
||||
|
||||
} else if (BinFile.RUNTIME_ENVIROMENT === 'node' && typeof source === 'string') {
|
||||
if (!nodeFs.existsSync(source))
|
||||
throw new Error(source + ' does not exist');
|
||||
|
||||
const arrayBuffer = nodeFs.readFileSync(source);
|
||||
|
||||
this.fileName = nodePath.basename(source);
|
||||
this.fileType = nodeFs.statSync(source).type;
|
||||
this.fileSize = arrayBuffer.byteLength;
|
||||
|
||||
this._u8array = new Uint8Array(arrayBuffer);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (source instanceof BinFile) { /* if source is another BinFile, clone it */
|
||||
this.fileName = source.fileName;
|
||||
this.fileType = source.fileType;
|
||||
this.fileSize = source.fileSize;
|
||||
|
||||
this._u8array = new Uint8Array(source._u8array.buffer.slice());
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
this.fileName = 'file.bin';
|
||||
this.fileType = 'application/octet-stream';
|
||||
this.fileSize = source.byteLength;
|
||||
|
||||
this._u8array = new Uint8Array(source);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (ArrayBuffer.isView(source)) { /* source is TypedArray */
|
||||
this.fileName = 'file.bin';
|
||||
this.fileType = 'application/octet-stream';
|
||||
this.fileSize = source.buffer.byteLength;
|
||||
|
||||
this._u8array = new Uint8Array(source.buffer);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (typeof source === 'number') { /* source is integer, create new empty file */
|
||||
this.fileName = 'file.bin';
|
||||
this.fileType = 'application/octet-stream';
|
||||
this.fileSize = source;
|
||||
|
||||
this._u8array = new Uint8Array(new ArrayBuffer(source));
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
throw new Error('invalid BinFile source');
|
||||
}
|
||||
}
|
||||
BinFile.RUNTIME_ENVIROMENT = (function () {
|
||||
if (typeof window === 'object' && typeof window.document === 'object')
|
||||
return 'browser';
|
||||
else if (typeof WorkerGlobalScope === 'function' && self instanceof WorkerGlobalScope)
|
||||
return 'webworker';
|
||||
else if (typeof require === 'function' && typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string')
|
||||
return 'node';
|
||||
else
|
||||
return null;
|
||||
}());
|
||||
BinFile.DEVICE_LITTLE_ENDIAN = (function () { /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness */
|
||||
var buffer = new ArrayBuffer(2);
|
||||
new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
|
||||
// Int16Array uses the platform's endianness.
|
||||
return new Int16Array(buffer)[0] === 256;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
BinFile.prototype.push = function () {
|
||||
this._offsetsStack.push(this.offset);
|
||||
}
|
||||
BinFile.prototype.pop = function () {
|
||||
this.seek(this._offsetsStack.pop());
|
||||
}
|
||||
BinFile.prototype.seek = function (offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
BinFile.prototype.skip = function (nBytes) {
|
||||
this.offset += nBytes;
|
||||
}
|
||||
BinFile.prototype.isEOF = function () {
|
||||
return !(this.offset < this.fileSize)
|
||||
}
|
||||
BinFile.prototype.slice = function (offset, len, doNotClone) {
|
||||
if (typeof offset !== 'number' || offset < 0)
|
||||
offset = 0;
|
||||
else if (offset >= this.fileSize)
|
||||
throw new Error('out of bounds slicing');
|
||||
else
|
||||
offset = Math.floor(offset);
|
||||
|
||||
if (typeof len !== 'number' || offset < 0 || (offset + len) >= this.fileSize.length)
|
||||
len = this.fileSize - offset;
|
||||
else if (len === 0)
|
||||
throw new Error('zero length provided for slicing');
|
||||
else
|
||||
offset = Math.floor(offset);
|
||||
|
||||
if (offset === 0 && len === this.fileSize && doNotClone)
|
||||
return this;
|
||||
|
||||
|
||||
var newFile = new BinFile(this._u8array.buffer.slice(offset, offset + len));
|
||||
newFile.fileName = this.fileName;
|
||||
newFile.fileType = this.fileType;
|
||||
newFile.littleEndian = this.littleEndian;
|
||||
return newFile;
|
||||
}
|
||||
BinFile.prototype.prependBytes = function (bytes) {
|
||||
var newFile = new BinFile(this.fileSize + bytes.length);
|
||||
newFile.seek(0);
|
||||
newFile.writeBytes(bytes);
|
||||
this.copyTo(newFile, 0, this.fileSize, bytes.length);
|
||||
|
||||
this.fileSize = newFile.fileSize;
|
||||
this._u8array = newFile._u8array;
|
||||
return this;
|
||||
}
|
||||
BinFile.prototype.removeLeadingBytes = function (nBytes) {
|
||||
this.seek(0);
|
||||
var oldData = this.readBytes(nBytes);
|
||||
var newFile = this.slice(nBytes.length);
|
||||
|
||||
this.fileSize = newFile.fileSize;
|
||||
this._u8array = newFile._u8array;
|
||||
return oldData;
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.copyTo = function (target, offsetSource, len, offsetTarget) {
|
||||
if (!(target instanceof BinFile))
|
||||
throw new Error('target is not a BinFile object');
|
||||
|
||||
if (typeof offsetTarget !== 'number')
|
||||
offsetTarget = offsetSource;
|
||||
|
||||
len = len || (this.fileSize - offsetSource);
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
target._u8array[offsetTarget + i] = this._u8array[offsetSource + i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.save = function () {
|
||||
if (BinFile.RUNTIME_ENVIROMENT === 'browser') {
|
||||
var fileBlob = new Blob([this._u8array], { type: this.fileType });
|
||||
var blobUrl = URL.createObjectURL(fileBlob);
|
||||
var a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = this.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
document.body.removeChild(a);
|
||||
} else if (BinFile.RUNTIME_ENVIROMENT === 'node') {
|
||||
nodeFs.writeFileSync(this.fileName, Buffer.from(this._u8array.buffer));
|
||||
} else {
|
||||
throw new Error('invalid runtime environment, can\'t save file');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.getExtension = function () {
|
||||
var ext = this.fileName ? this.fileName.toLowerCase().match(/\.(\w+)$/) : '';
|
||||
|
||||
return ext ? ext[1] : '';
|
||||
}
|
||||
BinFile.prototype.getName = function () {
|
||||
return this.fileName.replace(new RegExp('\\.' + this.getExtension() + '$', 'i'), '');
|
||||
}
|
||||
BinFile.prototype.setExtension = function (newExtension) {
|
||||
return (this.fileName = this.getName() + '.' + newExtension);
|
||||
}
|
||||
BinFile.prototype.setName = function (newName) {
|
||||
return (this.fileName = newName + '.' + this.getExtension());
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.readU8 = function () {
|
||||
this._lastRead = this._u8array[this.offset++];
|
||||
|
||||
return this._lastRead
|
||||
}
|
||||
BinFile.prototype.readU16 = function () {
|
||||
if (this.littleEndian)
|
||||
this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8);
|
||||
else
|
||||
this._lastRead = (this._u8array[this.offset] << 8) + this._u8array[this.offset + 1];
|
||||
|
||||
this.offset += 2;
|
||||
return this._lastRead >>> 0
|
||||
}
|
||||
BinFile.prototype.readU24 = function () {
|
||||
if (this.littleEndian)
|
||||
this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16);
|
||||
else
|
||||
this._lastRead = (this._u8array[this.offset] << 16) + (this._u8array[this.offset + 1] << 8) + this._u8array[this.offset + 2];
|
||||
|
||||
this.offset += 3;
|
||||
return this._lastRead >>> 0
|
||||
}
|
||||
BinFile.prototype.readU32 = function () {
|
||||
if (this.littleEndian)
|
||||
this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16) + (this._u8array[this.offset + 3] << 24);
|
||||
else
|
||||
this._lastRead = (this._u8array[this.offset] << 24) + (this._u8array[this.offset + 1] << 16) + (this._u8array[this.offset + 2] << 8) + this._u8array[this.offset + 3];
|
||||
|
||||
this.offset += 4;
|
||||
return this._lastRead >>> 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
BinFile.prototype.readBytes = function (len) {
|
||||
this._lastRead = new Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
this._lastRead[i] = this._u8array[this.offset + i];
|
||||
}
|
||||
|
||||
this.offset += len;
|
||||
return this._lastRead
|
||||
}
|
||||
|
||||
BinFile.prototype.readString = function (len) {
|
||||
this._lastRead = '';
|
||||
for (var i = 0; i < len && (this.offset + i) < this.fileSize && this._u8array[this.offset + i] > 0; i++)
|
||||
this._lastRead = this._lastRead + String.fromCharCode(this._u8array[this.offset + i]);
|
||||
|
||||
this.offset += len;
|
||||
return this._lastRead
|
||||
}
|
||||
|
||||
BinFile.prototype.writeU8 = function (u8) {
|
||||
this._u8array[this.offset++] = u8;
|
||||
}
|
||||
BinFile.prototype.writeU16 = function (u16) {
|
||||
if (this.littleEndian) {
|
||||
this._u8array[this.offset] = u16 & 0xff;
|
||||
this._u8array[this.offset + 1] = u16 >> 8;
|
||||
} else {
|
||||
this._u8array[this.offset] = u16 >> 8;
|
||||
this._u8array[this.offset + 1] = u16 & 0xff;
|
||||
}
|
||||
|
||||
this.offset += 2;
|
||||
}
|
||||
BinFile.prototype.writeU24 = function (u24) {
|
||||
if (this.littleEndian) {
|
||||
this._u8array[this.offset] = u24 & 0x0000ff;
|
||||
this._u8array[this.offset + 1] = (u24 & 0x00ff00) >> 8;
|
||||
this._u8array[this.offset + 2] = (u24 & 0xff0000) >> 16;
|
||||
} else {
|
||||
this._u8array[this.offset] = (u24 & 0xff0000) >> 16;
|
||||
this._u8array[this.offset + 1] = (u24 & 0x00ff00) >> 8;
|
||||
this._u8array[this.offset + 2] = u24 & 0x0000ff;
|
||||
}
|
||||
|
||||
this.offset += 3;
|
||||
}
|
||||
BinFile.prototype.writeU32 = function (u32) {
|
||||
if (this.littleEndian) {
|
||||
this._u8array[this.offset] = u32 & 0x000000ff;
|
||||
this._u8array[this.offset + 1] = (u32 & 0x0000ff00) >> 8;
|
||||
this._u8array[this.offset + 2] = (u32 & 0x00ff0000) >> 16;
|
||||
this._u8array[this.offset + 3] = (u32 & 0xff000000) >> 24;
|
||||
} else {
|
||||
this._u8array[this.offset] = (u32 & 0xff000000) >> 24;
|
||||
this._u8array[this.offset + 1] = (u32 & 0x00ff0000) >> 16;
|
||||
this._u8array[this.offset + 2] = (u32 & 0x0000ff00) >> 8;
|
||||
this._u8array[this.offset + 3] = u32 & 0x000000ff;
|
||||
}
|
||||
|
||||
this.offset += 4;
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.writeBytes = function (a) {
|
||||
for (var i = 0; i < a.length; i++)
|
||||
this._u8array[this.offset + i] = a[i]
|
||||
|
||||
this.offset += a.length;
|
||||
}
|
||||
|
||||
BinFile.prototype.writeString = function (str, len) {
|
||||
len = len || str.length;
|
||||
for (var i = 0; i < str.length && i < len; i++)
|
||||
this._u8array[this.offset + i] = str.charCodeAt(i);
|
||||
|
||||
for (; i < len; i++)
|
||||
this._u8array[this.offset + i] = 0x00;
|
||||
|
||||
this.offset += len;
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.swapBytes = function (swapSize, newFile) {
|
||||
if (typeof swapSize !== 'number') {
|
||||
swapSize = 4;
|
||||
}
|
||||
|
||||
if (this.fileSize % swapSize !== 0) {
|
||||
throw new Error('file size is not divisible by ' + swapSize);
|
||||
}
|
||||
|
||||
var swappedFile = new BinFile(this.fileSize);
|
||||
this.seek(0);
|
||||
while (!this.isEOF()) {
|
||||
swappedFile.writeBytes(
|
||||
this.readBytes(swapSize).reverse()
|
||||
);
|
||||
}
|
||||
|
||||
if (newFile) {
|
||||
swappedFile.fileName = this.fileName;
|
||||
swappedFile.fileType = this.fileType;
|
||||
|
||||
return swappedFile;
|
||||
} else {
|
||||
this._u8array = swappedFile._u8array;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
BinFile.prototype.hashSHA1 = async function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.sha1 !== 'function')
|
||||
throw new Error('no Hash object found or missing sha1 function');
|
||||
|
||||
return HashCalculator.sha1(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashMD5 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.md5 !== 'function')
|
||||
throw new Error('no Hash object found or missing md5 function');
|
||||
|
||||
return HashCalculator.md5(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashCRC32 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.crc32 !== 'function')
|
||||
throw new Error('no Hash object found or missing crc32 function');
|
||||
|
||||
return HashCalculator.crc32(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashAdler32 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.adler32 !== 'function')
|
||||
throw new Error('no Hash object found or missing adler32 function');
|
||||
|
||||
return HashCalculator.adler32(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashCRC16 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.crc16 !== 'function')
|
||||
throw new Error('no Hash object found or missing crc16 function');
|
||||
|
||||
return HashCalculator.crc16(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (BinFile.RUNTIME_ENVIROMENT === 'node' && typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = BinFile;
|
||||
HashCalculator = require('./HashCalculator');
|
||||
nodePath = require('path');
|
||||
nodeFs = require('fs');
|
||||
}
|
179
rom-patcher-js/modules/HashCalculator.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* HashCalculator.js (last update: 2021-08-15)
|
||||
* by Marc Robledo, https://www.marcrobledo.com
|
||||
*
|
||||
* data hash calculator (CRC32, MD5, SHA1, ADLER-32, CRC16)
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2016-2021 Marc Robledo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
const HashCalculator = (function () {
|
||||
const HEX_CHR = '0123456789abcdef'.split('');
|
||||
|
||||
/* MD5 helpers */
|
||||
const _add32 = function (a, b) { return (a + b) & 0xffffffff }
|
||||
const _md5cycle = function (x, k) { var a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = _add32(a, x[0]); x[1] = _add32(b, x[1]); x[2] = _add32(c, x[2]); x[3] = _add32(d, x[3]) }
|
||||
const _md5blk = function (d) { var md5blks = [], i; for (i = 0; i < 64; i += 4)md5blks[i >> 2] = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + (d[i + 3] << 24); return md5blks }
|
||||
const _cmn = function (q, a, b, x, s, t) { a = _add32(_add32(a, q), _add32(x, t)); return _add32((a << s) | (a >>> (32 - s)), b) }
|
||||
const ff = function (a, b, c, d, x, s, t) { return _cmn((b & c) | ((~b) & d), a, b, x, s, t) }
|
||||
const gg = function (a, b, c, d, x, s, t) { return _cmn((b & d) | (c & (~d)), a, b, x, s, t) }
|
||||
const hh = function (a, b, c, d, x, s, t) { return _cmn(b ^ c ^ d, a, b, x, s, t) }
|
||||
const ii = function (a, b, c, d, x, s, t) { return _cmn(c ^ (b | (~d)), a, b, x, s, t) }
|
||||
|
||||
/* CRC32 helpers */
|
||||
const CRC32_TABLE = (function () {
|
||||
var c, crcTable = [];
|
||||
for (var n = 0; n < 256; n++) {
|
||||
c = n;
|
||||
for (var k = 0; k < 8; k++)
|
||||
c = ((c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1));
|
||||
crcTable[n] = c;
|
||||
}
|
||||
return crcTable;
|
||||
}());
|
||||
|
||||
/* Adler-32 helpers */
|
||||
const ADLER32_MOD = 0xfff1;
|
||||
|
||||
|
||||
|
||||
|
||||
const generateUint8Array = function (arrayBuffer, offset, len) {
|
||||
if (typeof offset !== 'number' || offset < 0)
|
||||
offset = 0;
|
||||
else if (offset < arrayBuffer.byteLength)
|
||||
offset = Math.floor(offset);
|
||||
else
|
||||
throw new Error('out of bounds slicing');
|
||||
|
||||
if (typeof len !== 'number' || len < 0 || (offset + len) >= arrayBuffer.byteLength.length)
|
||||
len = arrayBuffer.byteLength - offset;
|
||||
else if (len > 0)
|
||||
len = Math.floor(len);
|
||||
else
|
||||
throw new Error('zero length provided for slicing');
|
||||
|
||||
return new Uint8Array(arrayBuffer, offset, len);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
/* SHA-1 using WebCryptoAPI */
|
||||
sha1: async function sha1(arrayBuffer, offset, len) {
|
||||
if(typeof window === 'undefined' || typeof window.crypto === 'undefined')
|
||||
throw new Error('Web Crypto API is not available');
|
||||
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
if (u8array.byteLength !== arrayBuffer.byteLength) {
|
||||
arrayBuffer = arrayBuffer.slice(u8array.byteOffset, u8array.byteOffset + u8array.byteLength);
|
||||
}
|
||||
|
||||
const hash = await window.crypto.subtle.digest('SHA-1', arrayBuffer);
|
||||
|
||||
const bytes = new Uint8Array(hash);
|
||||
let hexString = '';
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
hexString += bytes[i] < 16 ? '0' + bytes[i].toString(16) : bytes[i].toString(16);
|
||||
return hexString;
|
||||
},
|
||||
|
||||
/* MD5 - from Joseph's Myers - http://www.myersdaily.org/joseph/javascript/md5.js */
|
||||
md5: function (arrayBuffer, offset, len) {
|
||||
let u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var n = u8array.byteLength, state = [1732584193, -271733879, -1732584194, 271733878], i;
|
||||
for (i = 64; i <= u8array.byteLength; i += 64)
|
||||
_md5cycle(state, _md5blk(u8array.slice(i - 64, i)));
|
||||
u8array = u8array.slice(i - 64);
|
||||
var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for (i = 0; i < u8array.byteLength; i++)
|
||||
tail[i >> 2] |= u8array[i] << ((i % 4) << 3);
|
||||
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
||||
if (i > 55) {
|
||||
_md5cycle(state, tail);
|
||||
for (i = 0; i < 16; i++)tail[i] = 0;
|
||||
}
|
||||
tail[14] = n * 8;
|
||||
tail[15] = Math.floor(n / 536870912) >>> 0; //if file is bigger than 512Mb*8, value is bigger than 32 bits, so it needs two words to store its length
|
||||
_md5cycle(state, tail);
|
||||
|
||||
for (var i = 0; i < state.length; i++) {
|
||||
var s = '', j = 0;
|
||||
for (; j < 4; j++)
|
||||
s += HEX_CHR[(state[i] >> (j * 8 + 4)) & 0x0f] + HEX_CHR[(state[i] >> (j * 8)) & 0x0f];
|
||||
state[i] = s;
|
||||
}
|
||||
return state.join('')
|
||||
},
|
||||
|
||||
/* CRC32 - from Alex - https://stackoverflow.com/a/18639999 */
|
||||
crc32: function (arrayBuffer, offset, len) {
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var crc = 0 ^ (-1);
|
||||
|
||||
for (var i = 0; i < u8array.byteLength; i++)
|
||||
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ u8array[i]) & 0xff];
|
||||
|
||||
return ((crc ^ (-1)) >>> 0);
|
||||
},
|
||||
|
||||
/* Adler-32 - https://en.wikipedia.org/wiki/Adler-32#Example_implementation */
|
||||
adler32: function (arrayBuffer, offset, len) {
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var a = 1, b = 0;
|
||||
|
||||
for (var i = 0; i < u8array.byteLength; i++) {
|
||||
a = (a + u8array[i]) % ADLER32_MOD;
|
||||
b = (b + a) % ADLER32_MOD;
|
||||
}
|
||||
|
||||
return ((b << 16) | a) >>> 0;
|
||||
},
|
||||
|
||||
/* CRC16/CCITT-FALSE */
|
||||
crc16: function (arrayBuffer, offset, len) {
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var crc = 0xffff;
|
||||
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < u8array.byteLength; i++) {
|
||||
crc ^= u8array[offset++] << 8;
|
||||
for (j = 0; j < 8; ++j) {
|
||||
crc = (crc & 0x8000) >>> 0 ? (crc << 1) ^ 0x1021 : crc << 1;
|
||||
}
|
||||
}
|
||||
|
||||
return crc & 0xffff;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HashCalculator;
|
||||
}
|
114
rom-patcher-js/modules/RomPatcher.format.aps_gba.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
/* APS (GBA) module for Rom Patcher JS v20230331 - Marc Robledo 2017-2023 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(GBA) */
|
||||
|
||||
const APS_GBA_MAGIC='APS1';
|
||||
const APS_GBA_BLOCK_SIZE=0x010000; //64Kb
|
||||
const APS_GBA_RECORD_SIZE=4 + 2 + 2 + APS_GBA_BLOCK_SIZE;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = APSGBA;
|
||||
}
|
||||
function APSGBA(){
|
||||
this.sourceSize=0;
|
||||
this.targetSize=0;
|
||||
this.records=[];
|
||||
}
|
||||
APSGBA.prototype.addRecord=function(offset, sourceCrc16, targetCrc16, xorBytes){
|
||||
this.records.push({
|
||||
offset:offset,
|
||||
sourceCrc16:sourceCrc16,
|
||||
targetCrc16:targetCrc16,
|
||||
xorBytes:xorBytes}
|
||||
);
|
||||
}
|
||||
APSGBA.prototype.toString=function(){
|
||||
var s='Total records: '+this.records.length;
|
||||
s+='\nInput file size: '+this.sourceSize;
|
||||
s+='\nOutput file size: '+this.targetSize;
|
||||
return s
|
||||
}
|
||||
APSGBA.prototype.validateSource=function(sourceFile){
|
||||
if(sourceFile.fileSize!==this.sourceSize)
|
||||
return false;
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
sourceFile.seek(this.records[i].offset);
|
||||
var bytes=sourceFile.readBytes(APS_GBA_BLOCK_SIZE);
|
||||
if(sourceFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE) !== this.records[i].sourceCrc16)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
APSGBA.prototype.export=function(fileName){
|
||||
var patchFileSize=12 + (this.records.length * APS_GBA_RECORD_SIZE);
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.fileName=fileName+'.aps';
|
||||
tempFile.writeString(APS_GBA_MAGIC, APS_GBA_MAGIC.length);
|
||||
tempFile.writeU32(this.sourceSize);
|
||||
tempFile.writeU32(this.targetSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeU32(this.records[i].offset);
|
||||
tempFile.writeU16(this.records[i].sourceCrc16);
|
||||
tempFile.writeU16(this.records[i].targetCrc16);
|
||||
tempFile.writeBytes(this.records[i].xorBytes);
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APSGBA.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
romFile.copyTo(tempFile, 0, romFile.fileSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
romFile.seek(this.records[i].offset);
|
||||
tempFile.seek(this.records[i].offset);
|
||||
for(var j=0; j<APS_GBA_BLOCK_SIZE; j++){
|
||||
tempFile.writeU8(romFile.readU8() ^ this.records[i].xorBytes[j]);
|
||||
}
|
||||
|
||||
if(validate && tempFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE)!==this.records[i].targetCrc16){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APSGBA.MAGIC=APS_GBA_MAGIC;
|
||||
|
||||
APSGBA.fromFile=function(patchFile){
|
||||
patchFile.seek(0);
|
||||
patchFile.littleEndian=true;
|
||||
|
||||
if(
|
||||
patchFile.readString(APS_GBA_MAGIC.length)!==APS_GBA_MAGIC ||
|
||||
patchFile.fileSize < (12 + APS_GBA_RECORD_SIZE) ||
|
||||
(patchFile.fileSize-12)%APS_GBA_RECORD_SIZE!==0
|
||||
)
|
||||
return null;
|
||||
|
||||
var patch=new APSGBA();
|
||||
|
||||
patch.sourceSize=patchFile.readU32();
|
||||
patch.targetSize=patchFile.readU32();
|
||||
|
||||
while(!patchFile.isEOF()){
|
||||
var offset=patchFile.readU32();
|
||||
var sourceCrc16=patchFile.readU16();
|
||||
var targetCrc16=patchFile.readU16();
|
||||
var xorBytes=patchFile.readBytes(APS_GBA_BLOCK_SIZE);
|
||||
|
||||
patch.addRecord(offset, sourceCrc16, targetCrc16, xorBytes);
|
||||
}
|
||||
return patch;
|
||||
}
|
212
rom-patcher-js/modules/RomPatcher.format.aps_n64.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
/* APS (N64) module for Rom Patcher JS v20180930 - Marc Robledo 2017-2018 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(N64) */
|
||||
|
||||
const APS_N64_MAGIC='APS10';
|
||||
const APS_RECORD_RLE=0x0000;
|
||||
const APS_RECORD_SIMPLE=0x01;
|
||||
const APS_N64_MODE=0x01;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = APS;
|
||||
}
|
||||
function APS(){
|
||||
this.records=[];
|
||||
this.headerType=0;
|
||||
this.encodingMethod=0;
|
||||
this.description='no description';
|
||||
|
||||
this.header={};
|
||||
}
|
||||
APS.prototype.addRecord=function(o, d){
|
||||
this.records.push({offset:o, type:APS_RECORD_SIMPLE, data:d})
|
||||
}
|
||||
APS.prototype.addRLERecord=function(o, b, l){
|
||||
this.records.push({offset:o, type:APS_RECORD_RLE, length:l, byte:b})
|
||||
}
|
||||
APS.prototype.toString=function(){
|
||||
var s='Total records: '+this.records.length;
|
||||
s+='\nHeader type: '+this.headerType;
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
s+=' (N64)';
|
||||
}
|
||||
s+='\nEncoding method: '+this.encodingMethod;
|
||||
s+='\nDescription: '+this.description;
|
||||
s+='\nHeader: '+JSON.stringify(this.header);
|
||||
return s
|
||||
}
|
||||
APS.prototype.validateSource=function(sourceFile){
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
sourceFile.seek(0x3c);
|
||||
if(sourceFile.readString(3)!==this.header.cartId)
|
||||
return false;
|
||||
|
||||
sourceFile.seek(0x10);
|
||||
var crc=sourceFile.readBytes(8);
|
||||
for(var i=0; i<8; i++){
|
||||
if(crc[i]!==this.header.crc[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
APS.prototype.getValidationInfo=function(){
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
return this.header.cartId + ' (' + this.header.crc.reduce(function(hex, b){
|
||||
if(b<16)
|
||||
return hex + '0' + b.toString(16);
|
||||
else
|
||||
return hex + b.toString(16);
|
||||
}, '') + ')';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
APS.prototype.export=function(fileName){
|
||||
var patchFileSize=61;
|
||||
if(this.headerType===APS_N64_MODE)
|
||||
patchFileSize+=17;
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===APS_RECORD_RLE)
|
||||
patchFileSize+=7;
|
||||
else
|
||||
patchFileSize+=5+this.records[i].data.length; //offset+length+data
|
||||
}
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.fileName=fileName+'.aps';
|
||||
tempFile.writeString(APS_N64_MAGIC, APS_N64_MAGIC.length);
|
||||
tempFile.writeU8(this.headerType);
|
||||
tempFile.writeU8(this.encodingMethod);
|
||||
tempFile.writeString(this.description, 50);
|
||||
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
tempFile.writeU8(this.header.originalN64Format);
|
||||
tempFile.writeString(this.header.cartId, 3);
|
||||
tempFile.writeBytes(this.header.crc);
|
||||
tempFile.writeBytes(this.header.pad);
|
||||
}
|
||||
tempFile.writeU32(this.header.sizeOutput);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
tempFile.writeU32(rec.offset);
|
||||
if(rec.type===APS_RECORD_RLE){
|
||||
tempFile.writeU8(0x00);
|
||||
tempFile.writeU8(rec.byte);
|
||||
tempFile.writeU8(rec.length);
|
||||
}else{
|
||||
tempFile.writeU8(rec.data.length);
|
||||
tempFile.writeBytes(rec.data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
tempFile=new BinFile(this.header.sizeOutput);
|
||||
romFile.copyTo(tempFile, 0, tempFile.fileSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
if(this.records[i].type===APS_RECORD_RLE){
|
||||
for(var j=0; j<this.records[i].length; j++)
|
||||
tempFile.writeU8(this.records[i].byte);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APS.MAGIC=APS_N64_MAGIC;
|
||||
|
||||
|
||||
|
||||
APS.fromFile=function(patchFile){
|
||||
var patch=new APS();
|
||||
patchFile.littleEndian=true;
|
||||
|
||||
patchFile.seek(5);
|
||||
patch.headerType=patchFile.readU8();
|
||||
patch.encodingMethod=patchFile.readU8();
|
||||
patch.description=patchFile.readString(50);
|
||||
|
||||
var seek;
|
||||
if(patch.headerType===APS_N64_MODE){
|
||||
patch.header.originalN64Format=patchFile.readU8();
|
||||
patch.header.cartId=patchFile.readString(3);
|
||||
patch.header.crc=patchFile.readBytes(8);
|
||||
patch.header.pad=patchFile.readBytes(5);
|
||||
}
|
||||
patch.header.sizeOutput=patchFile.readU32();
|
||||
|
||||
while(!patchFile.isEOF()){
|
||||
var offset=patchFile.readU32();
|
||||
var length=patchFile.readU8();
|
||||
|
||||
if(length===APS_RECORD_RLE)
|
||||
patch.addRLERecord(offset, patchFile.readU8(seek), patchFile.readU8(seek+1));
|
||||
else
|
||||
patch.addRecord(offset, patchFile.readBytes(length));
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
APS.buildFromRoms=function(original, modified){
|
||||
var patch=new APS();
|
||||
|
||||
|
||||
|
||||
if(original.readU32()===0x80371240){ //is N64 ROM
|
||||
patch.headerType=APS_N64_MODE;
|
||||
|
||||
patch.header.originalN64Format=/\.v64$/i.test(original.fileName)?0:1;
|
||||
original.seek(0x3c);
|
||||
patch.header.cartId=original.readString(3);
|
||||
original.seek(0x10);
|
||||
patch.header.crc=original.readBytes(8);
|
||||
patch.header.pad=[0,0,0,0,0];
|
||||
}
|
||||
patch.header.sizeOutput=modified.fileSize;
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var RLERecord=true;
|
||||
var differentBytes=[];
|
||||
var offset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentBytes.length<0xff){
|
||||
differentBytes.push(b2);
|
||||
if(b2!==differentBytes[0])
|
||||
RLERecord=false;
|
||||
|
||||
if(modified.isEOF() || differentBytes.length===0xff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
if(RLERecord && differentBytes.length>2){
|
||||
patch.addRLERecord(offset, differentBytes[0], differentBytes.length);
|
||||
}else{
|
||||
patch.addRecord(offset, differentBytes);
|
||||
}
|
||||
//NO se puede comentar??? why????
|
||||
}
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
463
rom-patcher-js/modules/RomPatcher.format.bps.js
Normal file
|
@ -0,0 +1,463 @@
|
|||
/* BPS module for Rom Patcher JS v20240721 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://www.romhacking.net/documents/746/ */
|
||||
|
||||
const BPS_MAGIC='BPS1';
|
||||
const BPS_ACTION_SOURCE_READ=0;
|
||||
const BPS_ACTION_TARGET_READ=1;
|
||||
const BPS_ACTION_SOURCE_COPY=2;
|
||||
const BPS_ACTION_TARGET_COPY=3;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = BPS;
|
||||
}
|
||||
|
||||
function BPS(){
|
||||
this.sourceSize=0;
|
||||
this.targetSize=0;
|
||||
this.metaData='';
|
||||
this.actions=[];
|
||||
this.sourceChecksum=0;
|
||||
this.targetChecksum=0;
|
||||
this.patchChecksum=0;
|
||||
}
|
||||
BPS.prototype.toString=function(){
|
||||
var s='Source size: '+this.sourceSize;
|
||||
s+='\nTarget size: '+this.targetSize;
|
||||
s+='\nMetadata: '+this.metaData;
|
||||
s+='\n#Actions: '+this.actions.length;
|
||||
return s
|
||||
}
|
||||
BPS.prototype.validateSource=function(romFile,headerSize){return this.sourceChecksum===romFile.hashCRC32(headerSize)}
|
||||
BPS.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':this.sourceChecksum
|
||||
}
|
||||
}
|
||||
BPS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
|
||||
|
||||
//patch
|
||||
var sourceRelativeOffset=0;
|
||||
var targetRelativeOffset=0;
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
|
||||
if(action.type===BPS_ACTION_SOURCE_READ){
|
||||
romFile.copyTo(tempFile, tempFile.offset, action.length);
|
||||
tempFile.skip(action.length);
|
||||
|
||||
}else if(action.type===BPS_ACTION_TARGET_READ){
|
||||
tempFile.writeBytes(action.bytes);
|
||||
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY){
|
||||
sourceRelativeOffset+=action.relativeOffset;
|
||||
var actionLength=action.length;
|
||||
while(actionLength--){
|
||||
tempFile.writeU8(romFile._u8array[sourceRelativeOffset]);
|
||||
sourceRelativeOffset++;
|
||||
}
|
||||
}else if(action.type===BPS_ACTION_TARGET_COPY){
|
||||
targetRelativeOffset+=action.relativeOffset;
|
||||
var actionLength=action.length;
|
||||
while(actionLength--) {
|
||||
tempFile.writeU8(tempFile._u8array[targetRelativeOffset]);
|
||||
targetRelativeOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(validate && this.targetChecksum!==tempFile.hashCRC32()){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
BPS.MAGIC=BPS_MAGIC;
|
||||
|
||||
|
||||
BPS.fromFile=function(file){
|
||||
file.readVLV=BPS_readVLV;
|
||||
|
||||
file.littleEndian=true;
|
||||
var patch=new BPS();
|
||||
|
||||
|
||||
file.seek(4); //skip BPS1
|
||||
|
||||
patch.sourceSize=file.readVLV();
|
||||
patch.targetSize=file.readVLV();
|
||||
|
||||
var metaDataLength=file.readVLV();
|
||||
if(metaDataLength){
|
||||
patch.metaData=file.readString(metaDataLength);
|
||||
}
|
||||
|
||||
|
||||
var endActionsOffset=file.fileSize-12;
|
||||
while(file.offset<endActionsOffset){
|
||||
var data=file.readVLV();
|
||||
var action={type: data & 3, length: (data >> 2)+1};
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
action.bytes=file.readBytes(action.length);
|
||||
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
var relativeOffset=file.readVLV();
|
||||
action.relativeOffset=(relativeOffset & 1? -1 : +1) * (relativeOffset >> 1)
|
||||
}
|
||||
|
||||
patch.actions.push(action);
|
||||
}
|
||||
|
||||
//file.seek(endActionsOffset);
|
||||
patch.sourceChecksum=file.readU32();
|
||||
patch.targetChecksum=file.readU32();
|
||||
patch.patchChecksum=file.readU32();
|
||||
|
||||
if(patch.patchChecksum!==file.hashCRC32(0, file.fileSize - 4)){
|
||||
throw new Error('Patch checksum mismatch');
|
||||
}
|
||||
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function BPS_readVLV(){
|
||||
var data=0, shift=1;
|
||||
while(true){
|
||||
var x = this.readU8();
|
||||
data += (x & 0x7f) * shift;
|
||||
if(x & 0x80)
|
||||
break;
|
||||
shift <<= 7;
|
||||
data += shift;
|
||||
}
|
||||
|
||||
this._lastRead=data;
|
||||
return data;
|
||||
}
|
||||
function BPS_writeVLV(data){
|
||||
while(true){
|
||||
var x = data & 0x7f;
|
||||
data >>= 7;
|
||||
if(data === 0){
|
||||
this.writeU8(0x80 | x);
|
||||
break;
|
||||
}
|
||||
this.writeU8(x);
|
||||
data--;
|
||||
}
|
||||
}
|
||||
function BPS_getVLVLen(data){
|
||||
var len=0;
|
||||
while(true){
|
||||
var x = data & 0x7f;
|
||||
data >>= 7;
|
||||
if(data === 0){
|
||||
len++;
|
||||
break;
|
||||
}
|
||||
len++;
|
||||
data--;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
BPS.prototype.export=function(fileName){
|
||||
var patchFileSize=BPS_MAGIC.length;
|
||||
patchFileSize+=BPS_getVLVLen(this.sourceSize);
|
||||
patchFileSize+=BPS_getVLVLen(this.targetSize);
|
||||
patchFileSize+=BPS_getVLVLen(this.metaData.length);
|
||||
patchFileSize+=this.metaData.length;
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
patchFileSize+=BPS_getVLVLen(((action.length-1)<<2) + action.type);
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
patchFileSize+=action.length;
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
patchFileSize+=BPS_getVLVLen((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
|
||||
}
|
||||
}
|
||||
patchFileSize+=12;
|
||||
|
||||
var patchFile=new BinFile(patchFileSize);
|
||||
patchFile.fileName=fileName+'.bps';
|
||||
patchFile.littleEndian=true;
|
||||
patchFile.writeVLV=BPS_writeVLV;
|
||||
|
||||
patchFile.writeString(BPS_MAGIC);
|
||||
patchFile.writeVLV(this.sourceSize);
|
||||
patchFile.writeVLV(this.targetSize);
|
||||
patchFile.writeVLV(this.metaData.length);
|
||||
patchFile.writeString(this.metaData, this.metaData.length);
|
||||
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
patchFile.writeVLV(((action.length-1)<<2) + action.type);
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
patchFile.writeBytes(action.bytes);
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
patchFile.writeVLV((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
|
||||
}
|
||||
}
|
||||
patchFile.writeU32(this.sourceChecksum);
|
||||
patchFile.writeU32(this.targetChecksum);
|
||||
patchFile.writeU32(this.patchChecksum);
|
||||
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
function BPS_Node(){
|
||||
this.offset=0;
|
||||
this.next=null;
|
||||
};
|
||||
BPS_Node.prototype.delete=function(){
|
||||
if(this.next)
|
||||
delete this.next;
|
||||
}
|
||||
BPS.buildFromRoms=function(original, modified, deltaMode){
|
||||
var patch=new BPS();
|
||||
patch.sourceSize=original.fileSize;
|
||||
patch.targetSize=modified.fileSize;
|
||||
|
||||
if(deltaMode){
|
||||
patch.actions=createBPSFromFilesDelta(original, modified);
|
||||
}else{
|
||||
patch.actions=createBPSFromFilesLinear(original, modified);
|
||||
}
|
||||
|
||||
var patchFile=patch.export();
|
||||
patch.sourceChecksum=original.hashCRC32();
|
||||
patch.targetChecksum=modified.hashCRC32();
|
||||
patch.patchChecksum=patchFile.hashCRC32(0, patchFile.fileSize - 4);
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/linear.hpp */
|
||||
function createBPSFromFilesLinear(original, modified){
|
||||
var patchActions=[];
|
||||
|
||||
/* references to match original beat code */
|
||||
var sourceData=original._u8array;
|
||||
var targetData=modified._u8array;
|
||||
var sourceSize=original.fileSize;
|
||||
var targetSize=modified.fileSize;
|
||||
var Granularity=1;
|
||||
|
||||
|
||||
|
||||
var targetRelativeOffset=0;
|
||||
var outputOffset=0;
|
||||
var targetReadLength=0;
|
||||
|
||||
function targetReadFlush(){
|
||||
if(targetReadLength){
|
||||
//encode(TargetRead | ((targetReadLength - 1) << 2));
|
||||
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
|
||||
patchActions.push(action);
|
||||
var offset = outputOffset - targetReadLength;
|
||||
while(targetReadLength){
|
||||
//write(targetData[offset++]);
|
||||
action.bytes.push(targetData[offset++]);
|
||||
targetReadLength--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while(outputOffset < targetSize) {
|
||||
var sourceLength = 0;
|
||||
for(var n = 0; outputOffset + n < Math.min(sourceSize, targetSize); n++) {
|
||||
if(sourceData[outputOffset + n] != targetData[outputOffset + n]) break;
|
||||
sourceLength++;
|
||||
}
|
||||
|
||||
var rleLength = 0;
|
||||
for(var n = 1; outputOffset + n < targetSize; n++) {
|
||||
if(targetData[outputOffset] != targetData[outputOffset + n]) break;
|
||||
rleLength++;
|
||||
}
|
||||
|
||||
if(rleLength >= 4) {
|
||||
//write byte to repeat
|
||||
targetReadLength++;
|
||||
outputOffset++;
|
||||
targetReadFlush();
|
||||
|
||||
//copy starting from repetition byte
|
||||
//encode(TargetCopy | ((rleLength - 1) << 2));
|
||||
var relativeOffset = (outputOffset - 1) - targetRelativeOffset;
|
||||
//encode(relativeOffset << 1);
|
||||
patchActions.push({type:BPS_ACTION_TARGET_COPY, length:rleLength, relativeOffset:relativeOffset});
|
||||
outputOffset += rleLength;
|
||||
targetRelativeOffset = outputOffset - 1;
|
||||
} else if(sourceLength >= 4) {
|
||||
targetReadFlush();
|
||||
//encode(SourceRead | ((sourceLength - 1) << 2));
|
||||
patchActions.push({type:BPS_ACTION_SOURCE_READ, length:sourceLength});
|
||||
outputOffset += sourceLength;
|
||||
} else {
|
||||
targetReadLength += Granularity;
|
||||
outputOffset += Granularity;
|
||||
}
|
||||
}
|
||||
|
||||
targetReadFlush();
|
||||
|
||||
|
||||
|
||||
return patchActions;
|
||||
}
|
||||
|
||||
/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/delta.hpp */
|
||||
function createBPSFromFilesDelta(original, modified){
|
||||
var patchActions=[];
|
||||
|
||||
|
||||
/* references to match original beat code */
|
||||
var sourceData=original._u8array;
|
||||
var targetData=modified._u8array;
|
||||
var sourceSize=original.fileSize;
|
||||
var targetSize=modified.fileSize;
|
||||
var Granularity=1;
|
||||
|
||||
|
||||
|
||||
var sourceRelativeOffset=0;
|
||||
var targetRelativeOffset=0;
|
||||
var outputOffset=0;
|
||||
|
||||
|
||||
|
||||
var sourceTree=new Array(65536);
|
||||
var targetTree=new Array(65536);
|
||||
for(var n=0; n<65536; n++){
|
||||
sourceTree[n]=null;
|
||||
targetTree[n]=null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//source tree creation
|
||||
for(var offset=0; offset<sourceSize; offset++) {
|
||||
var symbol = sourceData[offset + 0];
|
||||
//sourceChecksum = crc32_adjust(sourceChecksum, symbol);
|
||||
if(offset < sourceSize - 1)
|
||||
symbol |= sourceData[offset + 1] << 8;
|
||||
var node=new BPS_Node();
|
||||
node.offset=offset;
|
||||
node.next=sourceTree[symbol];
|
||||
sourceTree[symbol] = node;
|
||||
}
|
||||
|
||||
var targetReadLength=0;
|
||||
|
||||
function targetReadFlush(){
|
||||
if(targetReadLength) {
|
||||
//encode(TargetRead | ((targetReadLength - 1) << 2));
|
||||
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
|
||||
patchActions.push(action);
|
||||
var offset = outputOffset - targetReadLength;
|
||||
while(targetReadLength){
|
||||
//write(targetData[offset++]);
|
||||
action.bytes.push(targetData[offset++]);
|
||||
targetReadLength--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while(outputOffset<modified.fileSize){
|
||||
var maxLength = 0, maxOffset = 0, mode = BPS_ACTION_TARGET_READ;
|
||||
|
||||
var symbol = targetData[outputOffset + 0];
|
||||
if(outputOffset < targetSize - 1) symbol |= targetData[outputOffset + 1] << 8;
|
||||
|
||||
{ //source read
|
||||
var length = 0, offset = outputOffset;
|
||||
while(offset < sourceSize && offset < targetSize && sourceData[offset] == targetData[offset]) {
|
||||
length++;
|
||||
offset++;
|
||||
}
|
||||
if(length > maxLength) maxLength = length, mode = BPS_ACTION_SOURCE_READ;
|
||||
}
|
||||
|
||||
{ //source copy
|
||||
var node = sourceTree[symbol];
|
||||
while(node) {
|
||||
var length = 0, x = node.offset, y = outputOffset;
|
||||
while(x < sourceSize && y < targetSize && sourceData[x++] == targetData[y++]) length++;
|
||||
if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_SOURCE_COPY;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
|
||||
{ //target copy
|
||||
var node = targetTree[symbol];
|
||||
while(node) {
|
||||
var length = 0, x = node.offset, y = outputOffset;
|
||||
while(y < targetSize && targetData[x++] == targetData[y++]) length++;
|
||||
if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_TARGET_COPY;
|
||||
node = node.next;
|
||||
}
|
||||
|
||||
//target tree append
|
||||
node = new BPS_Node();
|
||||
node.offset = outputOffset;
|
||||
node.next = targetTree[symbol];
|
||||
targetTree[symbol] = node;
|
||||
}
|
||||
|
||||
{ //target read
|
||||
if(maxLength < 4) {
|
||||
maxLength = Math.min(Granularity, targetSize - outputOffset);
|
||||
mode = BPS_ACTION_TARGET_READ;
|
||||
}
|
||||
}
|
||||
|
||||
if(mode != BPS_ACTION_TARGET_READ) targetReadFlush();
|
||||
|
||||
switch(mode) {
|
||||
case BPS_ACTION_SOURCE_READ:
|
||||
//encode(BPS_ACTION_SOURCE_READ | ((maxLength - 1) << 2));
|
||||
patchActions.push({type:BPS_ACTION_SOURCE_READ, length:maxLength});
|
||||
break;
|
||||
case BPS_ACTION_TARGET_READ:
|
||||
//delay write to group sequential TargetRead commands into one
|
||||
targetReadLength += maxLength;
|
||||
break;
|
||||
case BPS_ACTION_SOURCE_COPY:
|
||||
case BPS_ACTION_TARGET_COPY:
|
||||
//encode(mode | ((maxLength - 1) << 2));
|
||||
var relativeOffset;
|
||||
if(mode == BPS_ACTION_SOURCE_COPY) {
|
||||
relativeOffset = maxOffset - sourceRelativeOffset;
|
||||
sourceRelativeOffset = maxOffset + maxLength;
|
||||
} else {
|
||||
relativeOffset = maxOffset - targetRelativeOffset;
|
||||
targetRelativeOffset = maxOffset + maxLength;
|
||||
}
|
||||
//encode((relativeOffset < 0) | (abs(relativeOffset) << 1));
|
||||
patchActions.push({type:mode, length:maxLength, relativeOffset:relativeOffset});
|
||||
break;
|
||||
}
|
||||
|
||||
outputOffset += maxLength;
|
||||
}
|
||||
|
||||
targetReadFlush();
|
||||
|
||||
|
||||
return patchActions;
|
||||
}
|
235
rom-patcher-js/modules/RomPatcher.format.ips.js
Normal file
|
@ -0,0 +1,235 @@
|
|||
/* IPS module for Rom Patcher JS v20230924 - Marc Robledo 2016-2023 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */
|
||||
|
||||
const IPS_MAGIC='PATCH';
|
||||
const IPS_MAX_ROM_SIZE=0x1000000; //16 megabytes
|
||||
const IPS_RECORD_RLE=0x0000;
|
||||
const IPS_RECORD_SIMPLE=0x01;
|
||||
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = IPS;
|
||||
}
|
||||
|
||||
|
||||
function IPS(){
|
||||
this.records=[];
|
||||
this.truncate=false;
|
||||
}
|
||||
IPS.prototype.addSimpleRecord=function(o, d){
|
||||
this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d})
|
||||
}
|
||||
IPS.prototype.addRLERecord=function(o, l, b){
|
||||
this.records.push({offset:o, type:IPS_RECORD_RLE, length:l, byte:b})
|
||||
}
|
||||
IPS.prototype.toString=function(){
|
||||
nSimpleRecords=0;
|
||||
nRLERecords=0;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===IPS_RECORD_RLE)
|
||||
nRLERecords++;
|
||||
else
|
||||
nSimpleRecords++;
|
||||
}
|
||||
var s='Simple records: '+nSimpleRecords;
|
||||
s+='\nRLE records: '+nRLERecords;
|
||||
s+='\nTotal records: '+this.records.length;
|
||||
if(this.truncate)
|
||||
s+='\nTruncate at: 0x'+this.truncate.toString(16);
|
||||
return s
|
||||
}
|
||||
IPS.prototype.export=function(fileName){
|
||||
var patchFileSize=5; //PATCH string
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===IPS_RECORD_RLE)
|
||||
patchFileSize+=(3+2+2+1); //offset+0x0000+length+RLE byte to be written
|
||||
else
|
||||
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
|
||||
}
|
||||
patchFileSize+=3; //EOF string
|
||||
if(this.truncate)
|
||||
patchFileSize+=3; //truncate
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.fileName=fileName+'.ips';
|
||||
tempFile.writeString(IPS_MAGIC);
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
tempFile.writeU24(rec.offset);
|
||||
if(rec.type===IPS_RECORD_RLE){
|
||||
tempFile.writeU16(0x0000);
|
||||
tempFile.writeU16(rec.length);
|
||||
tempFile.writeU8(rec.byte);
|
||||
}else{
|
||||
tempFile.writeU16(rec.data.length);
|
||||
tempFile.writeBytes(rec.data);
|
||||
}
|
||||
}
|
||||
|
||||
tempFile.writeString('EOF');
|
||||
if(this.truncate)
|
||||
tempFile.writeU24(this.truncate);
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
IPS.prototype.apply=function(romFile){
|
||||
if(this.truncate){
|
||||
if(this.truncate>romFile.fileSize){ //expand (discussed here: https://github.com/marcrobledo/RomPatcher.js/pull/46)
|
||||
tempFile=new BinFile(this.truncate);
|
||||
romFile.copyTo(tempFile, 0, romFile.fileSize, 0);
|
||||
}else{ //truncate
|
||||
tempFile=romFile.slice(0, this.truncate);
|
||||
}
|
||||
}else{
|
||||
//calculate target ROM size, expanding it if any record offset is beyond target ROM size
|
||||
var newFileSize=romFile.fileSize;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
if(rec.type===IPS_RECORD_RLE){
|
||||
if(rec.offset+rec.length>newFileSize){
|
||||
newFileSize=rec.offset+rec.length;
|
||||
}
|
||||
}else{
|
||||
if(rec.offset+rec.data.length>newFileSize){
|
||||
newFileSize=rec.offset+rec.data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newFileSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(newFileSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
romFile.seek(0);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
if(this.records[i].type===IPS_RECORD_RLE){
|
||||
for(var j=0; j<this.records[i].length; j++)
|
||||
tempFile.writeU8(this.records[i].byte);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
IPS.MAGIC=IPS_MAGIC;
|
||||
|
||||
|
||||
IPS.fromFile=function(file){
|
||||
var patchFile=new IPS();
|
||||
file.seek(5);
|
||||
|
||||
while(!file.isEOF()){
|
||||
var offset=file.readU24();
|
||||
|
||||
if(offset===0x454f46){ /* EOF */
|
||||
if(file.isEOF()){
|
||||
break;
|
||||
}else if((file.offset+3)===file.fileSize){
|
||||
patchFile.truncate=file.readU24();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var length=file.readU16();
|
||||
|
||||
if(length===IPS_RECORD_RLE){
|
||||
patchFile.addRLERecord(offset, file.readU16(), file.readU8());
|
||||
}else{
|
||||
patchFile.addSimpleRecord(offset, file.readBytes(length));
|
||||
}
|
||||
}
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
|
||||
IPS.buildFromRoms=function(original, modified){
|
||||
var patch=new IPS();
|
||||
|
||||
if(modified.fileSize<original.fileSize){
|
||||
patch.truncate=modified.fileSize;
|
||||
}
|
||||
|
||||
//solucion: guardar startOffset y endOffset (ir mirando de 6 en 6 hacia atrás)
|
||||
var previousRecord={type:0xdeadbeef,startOffset:0,length:0};
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var RLEmode=true;
|
||||
var differentData=[];
|
||||
var startOffset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentData.length<0xffff){
|
||||
differentData.push(b2);
|
||||
if(b2!==differentData[0])
|
||||
RLEmode=false;
|
||||
|
||||
if(modified.isEOF() || differentData.length===0xffff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
|
||||
//check if this record is near the previous one
|
||||
var distance=startOffset-(previousRecord.offset+previousRecord.length);
|
||||
if(
|
||||
previousRecord.type===IPS_RECORD_SIMPLE &&
|
||||
distance<6 && (previousRecord.length+distance+differentData.length)<0xffff
|
||||
){
|
||||
if(RLEmode && differentData.length>6){
|
||||
// separate a potential RLE record
|
||||
original.seek(startOffset);
|
||||
modified.seek(startOffset);
|
||||
previousRecord={type:0xdeadbeef,startOffset:0,length:0};
|
||||
}else{
|
||||
// merge both records
|
||||
while(distance--){
|
||||
previousRecord.data.push(modified._u8array[previousRecord.offset+previousRecord.length]);
|
||||
previousRecord.length++;
|
||||
}
|
||||
previousRecord.data=previousRecord.data.concat(differentData);
|
||||
previousRecord.length=previousRecord.data.length;
|
||||
}
|
||||
}else{
|
||||
if(startOffset>=IPS_MAX_ROM_SIZE){
|
||||
throw new Error('Files are too big for IPS format');
|
||||
return null;
|
||||
}
|
||||
|
||||
if(RLEmode && differentData.length>2){
|
||||
patch.addRLERecord(startOffset, differentData.length, differentData[0]);
|
||||
}else{
|
||||
patch.addSimpleRecord(startOffset, differentData);
|
||||
}
|
||||
previousRecord=patch.records[patch.records.length-1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if(modified.fileSize>original.fileSize){
|
||||
var lastRecord=patch.records[patch.records.length-1];
|
||||
var lastOffset=lastRecord.offset+lastRecord.length;
|
||||
|
||||
if(lastOffset<modified.fileSize){
|
||||
patch.addSimpleRecord(modified.fileSize-1, [0x00]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return patch
|
||||
}
|
97
rom-patcher-js/modules/RomPatcher.format.pmsr.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/* PMSR (Paper Mario Star Rod) module for Rom Patcher JS v20240721 - Marc Robledo 2020-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://origami64.net/attachment.php?aid=790 (dead link) */
|
||||
|
||||
const PMSR_MAGIC='PMSR';
|
||||
const YAY0_MAGIC='Yay0';
|
||||
const PAPER_MARIO_USA10_CRC32=0xa7f5cd7e;
|
||||
const PAPER_MARIO_USA10_FILE_SIZE=41943040;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = PMSR;
|
||||
}
|
||||
|
||||
function PMSR(){
|
||||
this.targetSize=0;
|
||||
this.records=[];
|
||||
}
|
||||
PMSR.prototype.addRecord=function(offset, data){
|
||||
this.records.push({offset:offset, data:data})
|
||||
}
|
||||
PMSR.prototype.toString=function(){
|
||||
var s='Star Rod patch';
|
||||
s+='\nTarget file size: '+this.targetSize;
|
||||
s+='\n#Records: '+this.records.length;
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
PMSR.prototype.validateSource=function(romFile){
|
||||
return romFile.fileSize===PAPER_MARIO_USA10_FILE_SIZE && romFile.hashCRC32()===PAPER_MARIO_USA10_CRC32;
|
||||
}
|
||||
PMSR.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':PAPER_MARIO_USA10_CRC32
|
||||
}
|
||||
}
|
||||
PMSR.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
console.log('a');
|
||||
if(this.targetSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
|
||||
console.log('b');
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
PMSR.MAGIC=PMSR_MAGIC;
|
||||
|
||||
PMSR.fromFile=function(file){
|
||||
var patch=new PMSR();
|
||||
|
||||
/*file.seek(0);
|
||||
if(file.readString(YAY0_MAGIC.length)===YAY0_MAGIC){
|
||||
file=PMSR.YAY0_decode(file);
|
||||
}*/
|
||||
|
||||
patch.targetSize=PAPER_MARIO_USA10_FILE_SIZE;
|
||||
|
||||
file.seek(4);
|
||||
var nRecords=file.readU32();
|
||||
|
||||
for(var i=0; i<nRecords; i++){
|
||||
var offset=file.readU32();
|
||||
var length=file.readU32();
|
||||
patch.addRecord(offset, file.readBytes(length));
|
||||
|
||||
if((offset+length)>patch.targetSize)
|
||||
patch.targetSize=offset+length;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* to-do */
|
||||
//PMSR.prototype.export=function(fileName){return null}
|
||||
//PMSR.buildFromRoms=function(original, modified){return null}
|
||||
|
||||
|
||||
/* https://github.com/pho/WindViewer/wiki/Yaz0-and-Yay0 */
|
||||
PMSR.YAY0_decode=function(file){
|
||||
/* to-do */
|
||||
}
|
269
rom-patcher-js/modules/RomPatcher.format.ppf.js
Normal file
|
@ -0,0 +1,269 @@
|
|||
/* PPF module for Rom Patcher JS v20200221 - Marc Robledo 2019-2020 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://www.romhacking.net/utilities/353/ */
|
||||
|
||||
|
||||
const PPF_MAGIC='PPF';
|
||||
const PPF_IMAGETYPE_BIN=0x00;
|
||||
const PPF_IMAGETYPE_GI=0x01;
|
||||
const PPF_BEGIN_FILE_ID_DIZ_MAGIC='@BEG';//@BEGIN_FILE_ID.DIZ
|
||||
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = PPF;
|
||||
}
|
||||
function PPF(){
|
||||
this.version=3;
|
||||
this.imageType=PPF_IMAGETYPE_BIN;
|
||||
this.blockCheck=false;
|
||||
this.undoData=false;
|
||||
this.records=[];
|
||||
}
|
||||
PPF.prototype.addRecord=function(offset, data, undoData){
|
||||
if(this.undoData){
|
||||
this.records.push({offset:offset, data:data, undoData:undoData});
|
||||
}else{
|
||||
this.records.push({offset:offset, data:data});
|
||||
}
|
||||
}
|
||||
PPF.prototype.toString=function(){
|
||||
var s=this.description;
|
||||
s+='\nPPF version: '+this.version;
|
||||
s+='\n#Records: '+this.records.length;
|
||||
s+='\nImage type: '+this.imageType;
|
||||
s+='\nBlock check: '+!!this.blockCheck;
|
||||
s+='\nUndo data: '+this.undoData;
|
||||
if(this.fileIdDiz)
|
||||
s+='\nFILE_ID.DIZ: '+this.fileIdDiz;
|
||||
return s
|
||||
}
|
||||
PPF.prototype.export=function(fileName){
|
||||
var patchFileSize=5+1+50; //PPFx0
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
patchFileSize+=4+1+this.records[i].data.length;
|
||||
if(this.version===3)
|
||||
patchFileSize+=4; //offsets are u64
|
||||
}
|
||||
|
||||
if(this.version===3 || this.version===2){
|
||||
patchFileSize+=4;
|
||||
}
|
||||
if(this.blockCheck){
|
||||
patchFileSize+=1024;
|
||||
}
|
||||
if(this.fileIdDiz){
|
||||
patchFileSize+=18+this.fileIdDiz.length+16+4;
|
||||
}
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.fileName=fileName+'.ppf';
|
||||
tempFile.writeString(PPF_MAGIC);
|
||||
tempFile.writeString((this.version*10).toString());
|
||||
tempFile.writeU8(parseInt(this.version)-1);
|
||||
tempFile.writeString(this.description, 50);
|
||||
|
||||
|
||||
if(this.version===3){
|
||||
tempFile.writeU8(this.imageType);
|
||||
tempFile.writeU8(this.blockCheck?0x01:0x00);
|
||||
tempFile.writeU8(this.undoData?0x01:0x00);
|
||||
tempFile.writeU8(0x00); //dummy
|
||||
|
||||
}else if(this.version===2){
|
||||
tempFile.writeU32(this.inputFileSize);
|
||||
}
|
||||
|
||||
if(this.blockCheck){
|
||||
tempFile.writeBytes(this.blockCheck);
|
||||
}
|
||||
|
||||
|
||||
|
||||
tempFile.littleEndian=true;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeU32(this.records[i].offset & 0xffffffff);
|
||||
|
||||
if(this.version===3){
|
||||
var offset2=this.records[i].offset;
|
||||
for(var j=0; j<32; j++)
|
||||
offset2=parseInt((offset2/2)>>>0);
|
||||
tempFile.writeU32(offset2);
|
||||
}
|
||||
tempFile.writeU8(this.records[i].data.length);
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
if(this.undoData)
|
||||
tempFile.writeBytes(this.records[i].undoData);
|
||||
}
|
||||
|
||||
if(this.fileIdDiz){
|
||||
tempFile.writeString('@BEGIN_FILE_ID.DIZ');
|
||||
tempFile.writeString(this.fileIdDiz);
|
||||
tempFile.writeString('@END_FILE_ID.DIZ');
|
||||
tempFile.writeU16(this.fileIdDiz.length);
|
||||
tempFile.writeU16(0x00);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
PPF.prototype.apply=function(romFile){
|
||||
var newFileSize=romFile.fileSize;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].offset+this.records[i].data.length>newFileSize)
|
||||
newFileSize=this.records[i].offset+this.records[i].data.length;
|
||||
}
|
||||
if(newFileSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(newFileSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
|
||||
//check if undoing
|
||||
var undoingData=false;
|
||||
if(this.undoData){
|
||||
tempFile.seek(this.records[0].offset);
|
||||
var originalBytes=tempFile.readBytes(this.records[0].data.length);
|
||||
var foundDifferences=false;
|
||||
for(var i=0; i<originalBytes.length && !foundDifferences; i++){
|
||||
if(originalBytes[i]!==this.records[0].data[i]){
|
||||
foundDifferences=true;
|
||||
}
|
||||
}
|
||||
if(!foundDifferences){
|
||||
undoingData=true;
|
||||
}
|
||||
}
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
|
||||
if(undoingData){
|
||||
tempFile.writeBytes(this.records[i].undoData);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
PPF.MAGIC=PPF_MAGIC;
|
||||
|
||||
|
||||
|
||||
PPF.fromFile=function(patchFile){
|
||||
var patch=new PPF();
|
||||
|
||||
|
||||
patchFile.seek(3);
|
||||
var version1=parseInt(patchFile.readString(2))/10;
|
||||
var version2=patchFile.readU8()+1;
|
||||
if(version1!==version2 || version1>3){
|
||||
throw new Error('invalid PPF version');
|
||||
}
|
||||
|
||||
patch.version=version1;
|
||||
patch.description=patchFile.readString(50).replace(/ +$/,'');
|
||||
|
||||
|
||||
|
||||
|
||||
if(patch.version===3){
|
||||
patch.imageType=patchFile.readU8();
|
||||
if(patchFile.readU8())
|
||||
patch.blockCheck=true;
|
||||
if(patchFile.readU8())
|
||||
patch.undoData=true;
|
||||
|
||||
patchFile.skip(1);
|
||||
}else if(patch.version===2){
|
||||
patch.blockCheck=true;
|
||||
patch.inputFileSize=patchFile.readU32();
|
||||
}
|
||||
|
||||
if(patch.blockCheck){
|
||||
patch.blockCheck=patchFile.readBytes(1024);
|
||||
}
|
||||
|
||||
|
||||
|
||||
patchFile.littleEndian=true;
|
||||
while(!patchFile.isEOF()){
|
||||
|
||||
if(patchFile.readString(4)===PPF_BEGIN_FILE_ID_DIZ_MAGIC){
|
||||
patchFile.skip(14);
|
||||
//console.log('found file_id.diz begin');
|
||||
patch.fileIdDiz=patchFile.readString(3072);
|
||||
patch.fileIdDiz=patch.fileIdDiz.substr(0, patch.fileIdDiz.indexOf('@END_FILE_ID.DIZ'));
|
||||
break;
|
||||
}
|
||||
patchFile.skip(-4);
|
||||
|
||||
var offset;
|
||||
if(patch.version===3){
|
||||
var u64_1=patchFile.readU32();
|
||||
var u64_2=patchFile.readU32();
|
||||
offset=u64_1+(u64_2*0x100000000);
|
||||
}else
|
||||
offset=patchFile.readU32();
|
||||
|
||||
var len=patchFile.readU8();
|
||||
var data=patchFile.readBytes(len);
|
||||
|
||||
var undoData=false;
|
||||
if(patch.undoData){
|
||||
undoData=patchFile.readBytes(len);
|
||||
}
|
||||
|
||||
patch.addRecord(offset, data, undoData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
PPF.buildFromRoms=function(original, modified){
|
||||
var patch=new PPF();
|
||||
|
||||
patch.description='Patch description';
|
||||
|
||||
if(original.fileSize>modified.fileSize){
|
||||
var expandedModified=new BinFile(original.fileSize);
|
||||
modified.copyTo(expandedModified,0);
|
||||
modified=expandedModified;
|
||||
}
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var differentData=[];
|
||||
var offset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentData.length<0xff){
|
||||
differentData.push(b2);
|
||||
|
||||
if(modified.isEOF() || differentData.length===0xff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
patch.addRecord(offset, differentData);
|
||||
}
|
||||
}
|
||||
|
||||
if(original.fileSize<modified.fileSize){
|
||||
modified.seek(modified.fileSize-1);
|
||||
if(modified.readU8()===0x00)
|
||||
patch.addRecord(modified.fileSize-1, [0x00]);
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
350
rom-patcher-js/modules/RomPatcher.format.rup.js
Normal file
|
@ -0,0 +1,350 @@
|
|||
/* RUP module for Rom Patcher JS v20240721 - Marc Robledo 2018-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.romhacking.net/documents/288/ */
|
||||
|
||||
const RUP_MAGIC='NINJA2';
|
||||
const RUP_COMMAND_END=0x00;
|
||||
const RUP_COMMAND_OPEN_NEW_FILE=0x01;
|
||||
const RUP_COMMAND_XOR_RECORD=0x02;
|
||||
const RUP_ROM_TYPES=['raw','nes','fds','snes','n64','gb','sms','mega','pce','lynx'];
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = RUP;
|
||||
}
|
||||
|
||||
function RUP(){
|
||||
this.author='';
|
||||
this.version='';
|
||||
this.title='';
|
||||
this.genre='';
|
||||
this.language='';
|
||||
this.date='';
|
||||
this.web='';
|
||||
this.description='';
|
||||
this.files=[];
|
||||
}
|
||||
RUP.prototype.toString=function(){
|
||||
var s='Author: '+this.author;
|
||||
s+='\nVersion: '+this.version;
|
||||
s+='\nTitle: '+this.title;
|
||||
s+='\nGenre: '+this.genre;
|
||||
s+='\nLanguage: '+this.language;
|
||||
s+='\nDate: '+this.date;
|
||||
s+='\nWeb: '+this.web;
|
||||
s+='\nDescription: '+this.description;
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
s+='\n---------------';
|
||||
s+='\nFile '+i+':';
|
||||
s+='\nFile name: '+file.fileName;
|
||||
s+='\nRom type: '+RUP_ROM_TYPES[file.romType];
|
||||
s+='\nSource file size: '+file.sourceFileSize;
|
||||
s+='\nTarget file size: '+file.targetFileSize;
|
||||
s+='\nSource MD5: '+file.sourceMD5;
|
||||
s+='\nTarget MD5: '+file.targetMD5;
|
||||
s+='\nOverflow text: '+file.overflowText;
|
||||
s+='\n#records: '+file.records.length;
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
RUP.prototype.validateSource=function(romFile,headerSize){
|
||||
var md5string=romFile.hashMD5(headerSize);
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
if(this.files[i].sourceMD5===md5string){
|
||||
return this.files[i];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
RUP.prototype.getValidationInfo=function(){
|
||||
var values=[];
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
values.push(this.files[i].sourceMD5);
|
||||
}
|
||||
return {
|
||||
'type':'MD5',
|
||||
'value':values
|
||||
};
|
||||
}
|
||||
RUP.prototype.getDescription=function(){
|
||||
return this.description? this.description : null;
|
||||
}
|
||||
RUP.prototype.apply=function(romFile, validate){
|
||||
var validFile;
|
||||
if(validate){
|
||||
validFile=this.validateSource(romFile);
|
||||
|
||||
if(!validFile)
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}else{
|
||||
validFile=this.files[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
tempFile=new BinFile(validFile.targetFileSize);
|
||||
/* copy original file */
|
||||
romFile.copyTo(tempFile, 0);
|
||||
|
||||
|
||||
for(var i=0; i<validFile.records.length; i++){
|
||||
var offset=validFile.records[i].offset;
|
||||
romFile.seek(offset);
|
||||
tempFile.seek(offset);
|
||||
for(var j=0; j<validFile.records[i].xor.length; j++){
|
||||
tempFile.writeU8(
|
||||
(romFile.isEOF()?0x00:romFile.readU8()) ^ validFile.records[i].xor[j]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(validate && tempFile.hashMD5()!==validFile.targetMD5){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
RUP.MAGIC=RUP_MAGIC;
|
||||
|
||||
RUP.padZeroes=function(intVal, nBytes){
|
||||
var hexString=intVal.toString(16);
|
||||
while(hexString.length<nBytes*2)
|
||||
hexString='0'+hexString;
|
||||
return hexString
|
||||
};
|
||||
RUP.fromFile=function(file){
|
||||
var patch=new RUP();
|
||||
file.readVLV=RUP_readVLV;
|
||||
|
||||
file.seek(RUP_MAGIC.length);
|
||||
|
||||
patch.textEncoding=file.readU8();
|
||||
patch.author=file.readString(84);
|
||||
patch.version=file.readString(11);
|
||||
patch.title=file.readString(256);
|
||||
patch.genre=file.readString(48);
|
||||
patch.language=file.readString(48);
|
||||
patch.date=file.readString(8);
|
||||
patch.web=file.readString(512);
|
||||
patch.description=file.readString(1074).replace(/\\n/g,'\n');
|
||||
|
||||
|
||||
file.seek(0x800);
|
||||
var nextFile;
|
||||
while(!file.isEOF()){
|
||||
var command=file.readU8();
|
||||
|
||||
if(command===RUP_COMMAND_OPEN_NEW_FILE){
|
||||
if(nextFile)
|
||||
patch.files.push(nextFile)
|
||||
|
||||
nextFile={
|
||||
records:[]
|
||||
};
|
||||
|
||||
|
||||
nextFile.fileName=file.readString(file.readVLV());
|
||||
nextFile.romType=file.readU8();
|
||||
nextFile.sourceFileSize=file.readVLV();
|
||||
nextFile.targetFileSize=file.readVLV();
|
||||
|
||||
nextFile.sourceMD5='';
|
||||
for(var i=0; i<16; i++)
|
||||
nextFile.sourceMD5+=RUP.padZeroes(file.readU8(),1);
|
||||
|
||||
nextFile.targetMD5='';
|
||||
for(var i=0; i<16; i++)
|
||||
nextFile.targetMD5+=RUP.padZeroes(file.readU8(),1);
|
||||
|
||||
|
||||
|
||||
if(nextFile.sourceFileSize!==nextFile.targetFileSize){
|
||||
file.skip(1); //skip 'M' (source>target) or 'A' (source<target)
|
||||
nextFile.overflowText=file.readString(file.readVLV());
|
||||
}
|
||||
|
||||
}else if(command===RUP_COMMAND_XOR_RECORD){
|
||||
nextFile.records.push({
|
||||
offset:file.readVLV(),
|
||||
xor:file.readBytes(file.readVLV())
|
||||
});
|
||||
}else if(command===RUP_COMMAND_END){
|
||||
if(nextFile)
|
||||
patch.files.push(nextFile);
|
||||
break;
|
||||
}else{
|
||||
throw new Error('invalid RUP command');
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
function RUP_readVLV(){
|
||||
var nBytes=this.readU8();
|
||||
var data=0;
|
||||
for(var i=0; i<nBytes; i++){
|
||||
data+=this.readU8() << i*8;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
function RUP_writeVLV(data){
|
||||
var len=RUP_getVLVLen(data)-1;
|
||||
|
||||
this.writeU8(len);
|
||||
|
||||
while(data){
|
||||
this.writeU8(data & 0xff);
|
||||
data>>=8;
|
||||
}
|
||||
}
|
||||
function RUP_getVLVLen(data){
|
||||
var ret=1;
|
||||
while(data){
|
||||
ret++;
|
||||
data>>=8;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
RUP.prototype.export=function(fileName){
|
||||
var patchFileSize=2048;
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
patchFileSize++; //command 0x01
|
||||
|
||||
patchFileSize+=RUP_getVLVLen(file.fileName.length);
|
||||
patchFileSize+=file.fileName.length;
|
||||
patchFileSize++; //rom type
|
||||
patchFileSize+=RUP_getVLVLen(file.sourceFileSize);
|
||||
patchFileSize+=RUP_getVLVLen(file.targetFileSize);
|
||||
patchFileSize+=32; //MD5s
|
||||
|
||||
if(file.sourceFileSize!==file.targetFileSize){
|
||||
patchFileSize++; // M or A
|
||||
patchFileSize+=RUP_getVLVLen(file.overflowText);
|
||||
patchFileSize+=file.overflowText;
|
||||
}
|
||||
for(var j=0; j<file.records.length; j++){
|
||||
patchFileSize++; //command 0x01
|
||||
patchFileSize+=RUP_getVLVLen(file.records[j].offset);
|
||||
patchFileSize+=RUP_getVLVLen(file.records[j].xor.length);
|
||||
patchFileSize+=file.records[j].xor.length;
|
||||
}
|
||||
}
|
||||
patchFileSize++; //command 0x00
|
||||
|
||||
|
||||
var patchFile=new BinFile(patchFileSize);
|
||||
patchFile.fileName=fileName+'.rup';
|
||||
patchFile.writeVLV=RUP_writeVLV;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
patchFile.writeString(RUP_MAGIC);
|
||||
patchFile.writeU8(this.textEncoding);
|
||||
patchFile.writeString(this.author, 84);
|
||||
patchFile.writeString(this.version, 11);
|
||||
patchFile.writeString(this.title, 256);
|
||||
patchFile.writeString(this.genre, 48);
|
||||
patchFile.writeString(this.language, 48);
|
||||
patchFile.writeString(this.date, 8);
|
||||
patchFile.writeString(this.web, 512);
|
||||
patchFile.writeString(this.description.replace(/\n/g,'\\n'), 1074);
|
||||
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
patchFile.writeU8(RUP_COMMAND_OPEN_NEW_FILE);
|
||||
|
||||
patchFile.writeVLV(file.fileName);
|
||||
patchFile.writeU8(file.romType);
|
||||
patchFile.writeVLV(file.sourceFileSize);
|
||||
patchFile.writeVLV(file.targetFileSize);
|
||||
|
||||
for(var j=0; j<16; j++)
|
||||
patchFile.writeU8(parseInt(file.sourceMD5.substr(j*2,2), 16));
|
||||
for(var j=0; j<16; j++)
|
||||
patchFile.writeU8(parseInt(file.targetMD5.substr(j*2,2), 16));
|
||||
|
||||
|
||||
|
||||
if(file.sourceFileSize!==file.targetFileSize){
|
||||
patchFile.writeString(file.sourceFileSize>file.targetFileSize?'M':'A');
|
||||
patchFile.writeVLV(file.overflowText.length);
|
||||
patchFile.writeString(file.overflowText);
|
||||
}
|
||||
|
||||
for(var j=0; j<file.records.length; j++){
|
||||
patchFile.writeU8(RUP_COMMAND_XOR_RECORD);
|
||||
|
||||
patchFile.writeVLV(file.records[j].offset);
|
||||
patchFile.writeVLV(file.records[j].xor.length);
|
||||
patchFile.writeBytes(file.records[j].xor);
|
||||
}
|
||||
}
|
||||
|
||||
patchFile.writeU8(RUP_COMMAND_END);
|
||||
|
||||
|
||||
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
|
||||
|
||||
RUP.buildFromRoms=function(original, modified){
|
||||
var patch=new RUP();
|
||||
|
||||
var today=new Date();
|
||||
patch.date=(today.getYear()+1900)+RUP.padZeroes(today.getMonth()+1, 1)+RUP.padZeroes(today.getDate(), 1);
|
||||
|
||||
var file={
|
||||
fileName:'',
|
||||
romType:0,
|
||||
sourceFileSize:original.fileSize,
|
||||
targetFileSize:modified.fileSize,
|
||||
sourceMD5:original.hashMD5(),
|
||||
targetMD5:modified.hashMD5(),
|
||||
overflowText:'',
|
||||
records:[]
|
||||
};
|
||||
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var originalOffset=modified.offset-1;
|
||||
var xorDifferences=[];
|
||||
|
||||
while(b1!==b2){
|
||||
xorDifferences.push(b1^b2);
|
||||
|
||||
if(modified.isEOF())
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
file.records.push({offset:originalOffset, xor:xorDifferences});
|
||||
}
|
||||
}
|
||||
|
||||
patch.files.push(file);
|
||||
|
||||
return patch
|
||||
}
|
224
rom-patcher-js/modules/RomPatcher.format.ups.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/* UPS module for Rom Patcher JS v20240721 - Marc Robledo 2017-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.romhacking.net/documents/392/ */
|
||||
|
||||
const UPS_MAGIC='UPS1';
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = UPS;
|
||||
}
|
||||
function UPS(){
|
||||
this.records=[];
|
||||
this.sizeInput=0;
|
||||
this.sizeOutput=0;
|
||||
this.checksumInput=0;
|
||||
this.checksumOutput=0;
|
||||
}
|
||||
UPS.prototype.addRecord=function(relativeOffset, d){
|
||||
this.records.push({offset:relativeOffset, XORdata:d})
|
||||
}
|
||||
UPS.prototype.toString=function(){
|
||||
var s='Records: '+this.records.length;
|
||||
s+='\nInput file size: '+this.sizeInput;
|
||||
s+='\nOutput file size: '+this.sizeOutput;
|
||||
s+='\nInput file checksum: '+this.checksumInput.toString(16);
|
||||
s+='\nOutput file checksum: '+this.checksumOutput.toString(16);
|
||||
return s
|
||||
}
|
||||
UPS.prototype.export=function(fileName){
|
||||
var patchFileSize=UPS_MAGIC.length;//UPS1 string
|
||||
patchFileSize+=UPS_getVLVLength(this.sizeInput); //input file size
|
||||
patchFileSize+=UPS_getVLVLength(this.sizeOutput); //output file size
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
patchFileSize+=UPS_getVLVLength(this.records[i].offset);
|
||||
patchFileSize+=this.records[i].XORdata.length+1;
|
||||
}
|
||||
patchFileSize+=12; //input/output/patch checksums
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.writeVLV=UPS_writeVLV;
|
||||
tempFile.fileName=fileName+'.ups';
|
||||
tempFile.writeString(UPS_MAGIC);
|
||||
|
||||
tempFile.writeVLV(this.sizeInput);
|
||||
tempFile.writeVLV(this.sizeOutput);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeVLV(this.records[i].offset);
|
||||
tempFile.writeBytes(this.records[i].XORdata);
|
||||
tempFile.writeU8(0x00);
|
||||
}
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.writeU32(this.checksumInput);
|
||||
tempFile.writeU32(this.checksumOutput);
|
||||
tempFile.writeU32(tempFile.hashCRC32(0, tempFile.fileSize - 4));
|
||||
|
||||
return tempFile
|
||||
}
|
||||
UPS.prototype.validateSource=function(romFile,headerSize){return romFile.hashCRC32(headerSize)===this.checksumInput}
|
||||
UPS.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':this.checksumInput
|
||||
};
|
||||
}
|
||||
UPS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
/* fix the glitch that cut the end of the file if it's larger than the changed file patch was originally created with */
|
||||
/* more info: https://github.com/marcrobledo/RomPatcher.js/pull/40#issuecomment-1069087423 */
|
||||
sizeOutput = this.sizeOutput;
|
||||
sizeInput = this.sizeInput;
|
||||
if(!validate && sizeInput < romFile.fileSize){
|
||||
sizeInput = romFile.fileSize;
|
||||
if(sizeOutput < sizeInput){
|
||||
sizeOutput = sizeInput;
|
||||
}
|
||||
}
|
||||
|
||||
/* copy original file */
|
||||
tempFile=new BinFile(sizeOutput);
|
||||
romFile.copyTo(tempFile, 0, sizeInput);
|
||||
|
||||
romFile.seek(0);
|
||||
|
||||
|
||||
var nextOffset=0;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var record=this.records[i];
|
||||
tempFile.skip(record.offset);
|
||||
romFile.skip(record.offset);
|
||||
|
||||
for(var j=0; j<record.XORdata.length; j++){
|
||||
tempFile.writeU8((romFile.isEOF()?0x00:romFile.readU8()) ^ record.XORdata[j]);
|
||||
}
|
||||
tempFile.skip(1);
|
||||
romFile.skip(1);
|
||||
}
|
||||
|
||||
if(validate && tempFile.hashCRC32()!==this.checksumOutput){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
UPS.MAGIC=UPS_MAGIC;
|
||||
|
||||
|
||||
/* encode/decode variable length values, used by UPS file structure */
|
||||
function UPS_writeVLV(data){
|
||||
while(1){
|
||||
var x=data & 0x7f;
|
||||
data=data>>7;
|
||||
if(data===0){
|
||||
this.writeU8(0x80 | x);
|
||||
break;
|
||||
}
|
||||
this.writeU8(x);
|
||||
data=data-1;
|
||||
}
|
||||
}
|
||||
function UPS_readVLV(){
|
||||
var data=0;
|
||||
|
||||
var shift=1;
|
||||
while(1){
|
||||
var x=this.readU8();
|
||||
|
||||
if(x==-1)
|
||||
throw new Error('Can\'t read UPS VLV at 0x'+(this.offset-1).toString(16));
|
||||
|
||||
data+=(x&0x7f)*shift;
|
||||
if((x&0x80)!==0)
|
||||
break;
|
||||
shift=shift<<7;
|
||||
data+=shift;
|
||||
}
|
||||
return data
|
||||
}
|
||||
function UPS_getVLVLength(data){
|
||||
var len=0;
|
||||
while(1){
|
||||
var x=data & 0x7f;
|
||||
data=data>>7;
|
||||
len++;
|
||||
if(data===0){
|
||||
break;
|
||||
}
|
||||
data=data-1;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
UPS.fromFile=function(file){
|
||||
var patch=new UPS();
|
||||
file.readVLV=UPS_readVLV;
|
||||
|
||||
file.seek(UPS_MAGIC.length);
|
||||
|
||||
patch.sizeInput=file.readVLV();
|
||||
patch.sizeOutput=file.readVLV();
|
||||
|
||||
|
||||
var nextOffset=0;
|
||||
while(file.offset<(file.fileSize-12)){
|
||||
var relativeOffset=file.readVLV();
|
||||
|
||||
|
||||
var XORdifferences=[];
|
||||
while(file.readU8()){
|
||||
XORdifferences.push(file._lastRead);
|
||||
}
|
||||
patch.addRecord(relativeOffset, XORdifferences);
|
||||
}
|
||||
|
||||
file.littleEndian=true;
|
||||
patch.checksumInput=file.readU32();
|
||||
patch.checksumOutput=file.readU32();
|
||||
|
||||
if(file.readU32()!==file.hashCRC32(0, file.fileSize - 4)){
|
||||
throw new Error('Patch checksum mismatch');
|
||||
}
|
||||
|
||||
file.littleEndian=false;
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
UPS.buildFromRoms=function(original, modified){
|
||||
var patch=new UPS();
|
||||
patch.sizeInput=original.fileSize;
|
||||
patch.sizeOutput=modified.fileSize;
|
||||
|
||||
|
||||
var previousSeek=1;
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var currentSeek=modified.offset;
|
||||
var XORdata=[];
|
||||
|
||||
while(b1!==b2){
|
||||
XORdata.push(b1 ^ b2);
|
||||
|
||||
if(modified.isEOF())
|
||||
break;
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
patch.addRecord(currentSeek-previousSeek, XORdata);
|
||||
previousSeek=currentSeek+XORdata.length+1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
patch.checksumInput=original.hashCRC32();
|
||||
patch.checksumOutput=modified.hashCRC32();
|
||||
return patch
|
||||
}
|
377
rom-patcher-js/modules/RomPatcher.format.vcdiff.js
Normal file
|
@ -0,0 +1,377 @@
|
|||
/* VCDIFF module for RomPatcher.js v20181021 - Marc Robledo 2018 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://tools.ietf.org/html/rfc3284 */
|
||||
/*
|
||||
Mostly based in:
|
||||
https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
|
||||
some code and ideas borrowed from:
|
||||
https://hack64.net/jscripts/libpatch.js?6
|
||||
*/
|
||||
//const VCDIFF_MAGIC=0xd6c3c400;
|
||||
const VCDIFF_MAGIC='\xd6\xc3\xc4';
|
||||
/*
|
||||
const XDELTA_014_MAGIC='%XDELTA';
|
||||
const XDELTA_018_MAGIC='%XDZ000';
|
||||
const XDELTA_020_MAGIC='%XDZ001';
|
||||
const XDELTA_100_MAGIC='%XDZ002';
|
||||
const XDELTA_104_MAGIC='%XDZ003';
|
||||
const XDELTA_110_MAGIC='%XDZ004';
|
||||
*/
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = VCDIFF;
|
||||
BinFile = require("./BinFile");
|
||||
}
|
||||
|
||||
function VCDIFF(patchFile){
|
||||
this.file=patchFile;
|
||||
}
|
||||
VCDIFF.prototype.toString=function(){
|
||||
return 'VCDIFF patch'
|
||||
}
|
||||
|
||||
VCDIFF.prototype.apply=function(romFile, validate){
|
||||
//romFile._u8array=new Uint8Array(romFile._dataView.buffer);
|
||||
|
||||
//var t0=performance.now();
|
||||
var parser=new VCDIFF_Parser(this.file);
|
||||
|
||||
//read header
|
||||
parser.seek(4);
|
||||
var headerIndicator=parser.readU8();
|
||||
|
||||
if(headerIndicator & VCD_DECOMPRESS){
|
||||
//has secondary decompressor, read its id
|
||||
var secondaryDecompressorId=parser.readU8();
|
||||
|
||||
if(secondaryDecompressorId!==0)
|
||||
throw new Error('not implemented: secondary decompressor');
|
||||
}
|
||||
|
||||
|
||||
if(headerIndicator & VCD_CODETABLE){
|
||||
var codeTableDataLength=parser.read7BitEncodedInt();
|
||||
|
||||
if(codeTableDataLength!==0)
|
||||
throw new Error('not implemented: custom code table'); // custom code table
|
||||
}
|
||||
|
||||
if(headerIndicator & VCD_APPHEADER){
|
||||
// ignore app header data
|
||||
var appDataLength=parser.read7BitEncodedInt();
|
||||
parser.skip(appDataLength);
|
||||
}
|
||||
var headerEndOffset=parser.offset;
|
||||
|
||||
//calculate target file size
|
||||
var newFileSize=0;
|
||||
while(!parser.isEOF()){
|
||||
var winHeader=parser.decodeWindowHeader();
|
||||
newFileSize+=winHeader.targetWindowLength;
|
||||
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
|
||||
}
|
||||
tempFile=new BinFile(newFileSize);
|
||||
|
||||
|
||||
|
||||
|
||||
parser.seek(headerEndOffset);
|
||||
|
||||
|
||||
|
||||
var cache = new VCD_AdressCache(4,3);
|
||||
var codeTable = VCD_DEFAULT_CODE_TABLE;
|
||||
|
||||
var targetWindowPosition = 0; //renombrar
|
||||
|
||||
while(!parser.isEOF()){
|
||||
var winHeader = parser.decodeWindowHeader();
|
||||
|
||||
var addRunDataStream = new VCDIFF_Parser(this.file, parser.offset);
|
||||
var instructionsStream = new VCDIFF_Parser(this.file, addRunDataStream.offset + winHeader.addRunDataLength);
|
||||
var addressesStream = new VCDIFF_Parser(this.file, instructionsStream.offset + winHeader.instructionsLength);
|
||||
|
||||
var addRunDataIndex = 0;
|
||||
|
||||
cache.reset(addressesStream);
|
||||
|
||||
var addressesStreamEndOffset = addressesStream.offset;
|
||||
while(instructionsStream.offset<addressesStreamEndOffset){
|
||||
/*
|
||||
var instructionIndex=instructionsStream.readS8();
|
||||
if(instructionIndex===-1){
|
||||
break;
|
||||
}
|
||||
*/
|
||||
var instructionIndex = instructionsStream.readU8();
|
||||
|
||||
|
||||
for(var i=0; i<2; i++){
|
||||
var instruction=codeTable[instructionIndex][i];
|
||||
var size=instruction.size;
|
||||
|
||||
if(size===0 && instruction.type!==VCD_NOOP){
|
||||
size=instructionsStream.read7BitEncodedInt()
|
||||
}
|
||||
|
||||
if(instruction.type===VCD_NOOP){
|
||||
continue;
|
||||
|
||||
}else if(instruction.type===VCD_ADD){
|
||||
addRunDataStream.copyToFile2(tempFile, addRunDataIndex+targetWindowPosition, size);
|
||||
addRunDataIndex += size;
|
||||
|
||||
}else if(instruction.type===VCD_COPY){
|
||||
var addr = cache.decodeAddress(addRunDataIndex+winHeader.sourceLength, instruction.mode);
|
||||
var absAddr = 0;
|
||||
|
||||
// source segment and target segment are treated as if they're concatenated
|
||||
var sourceData = null;
|
||||
if(addr < winHeader.sourceLength){
|
||||
absAddr = winHeader.sourcePosition + addr;
|
||||
if(winHeader.indicator & VCD_SOURCE){
|
||||
sourceData = romFile;
|
||||
}else if(winHeader.indicator & VCD_TARGET){
|
||||
sourceData = tempFile;
|
||||
}
|
||||
}else{
|
||||
absAddr = targetWindowPosition + (addr - winHeader.sourceLength);
|
||||
sourceData = tempFile;
|
||||
}
|
||||
|
||||
while(size--){
|
||||
tempFile._u8array[targetWindowPosition + addRunDataIndex++]=sourceData._u8array[absAddr++];
|
||||
//targetU8[targetWindowPosition + targetWindowOffs++] = copySourceU8[absAddr++];
|
||||
}
|
||||
//to-do: test
|
||||
//sourceData.copyToFile2(tempFile, absAddr, size, targetWindowPosition + addRunDataIndex);
|
||||
//addRunDataIndex += size;
|
||||
}else if(instruction.type===VCD_RUN){
|
||||
var runByte = addRunDataStream.readU8();
|
||||
var offset = targetWindowPosition + addRunDataIndex;
|
||||
for(var j=0; j<size; j++){
|
||||
tempFile._u8array[offset+j]=runByte;
|
||||
}
|
||||
|
||||
addRunDataIndex += size;
|
||||
}else{
|
||||
throw new Error('invalid instruction type found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(validate && winHeader.adler32 && (winHeader.adler32 !== adler32(tempFile, targetWindowPosition, winHeader.targetWindowLength))){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
|
||||
targetWindowPosition += winHeader.targetWindowLength;
|
||||
}
|
||||
|
||||
//console.log((performance.now()-t0)/1000);
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
VCDIFF.MAGIC=VCDIFF_MAGIC;
|
||||
|
||||
VCDIFF.fromFile=function(file){
|
||||
return new VCDIFF(file);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function VCDIFF_Parser(binFile, offset)
|
||||
{
|
||||
BinFile.call(this, 0);
|
||||
this.fileSize=binFile.fileSize;
|
||||
this._u8array=binFile._u8array;
|
||||
this._dataView=binFile._dataView;
|
||||
this.offset=offset || 0;
|
||||
}
|
||||
|
||||
VCDIFF_Parser.prototype=Object.create(BinFile.prototype);
|
||||
VCDIFF_Parser.prototype.read7BitEncodedInt=function(){
|
||||
var num=0, bits = 0;
|
||||
|
||||
do {
|
||||
bits = this.readU8();
|
||||
num = (num << 7) + (bits & 0x7f);
|
||||
} while(bits & 0x80);
|
||||
|
||||
return num;
|
||||
}
|
||||
VCDIFF_Parser.prototype.decodeWindowHeader=function(){
|
||||
var windowHeader={
|
||||
indicator:this.readU8(),
|
||||
sourceLength:0,
|
||||
sourcePosition:0,
|
||||
adler32:false
|
||||
};
|
||||
|
||||
|
||||
if(windowHeader.indicator & (VCD_SOURCE | VCD_TARGET)){
|
||||
windowHeader.sourceLength = this.read7BitEncodedInt();
|
||||
windowHeader.sourcePosition = this.read7BitEncodedInt();
|
||||
}
|
||||
|
||||
windowHeader.deltaLength = this.read7BitEncodedInt();
|
||||
windowHeader.targetWindowLength = this.read7BitEncodedInt();
|
||||
windowHeader.deltaIndicator = this.readU8(); // secondary compression: 1=VCD_DATACOMP,2=VCD_INSTCOMP,4=VCD_ADDRCOMP
|
||||
if(windowHeader.deltaIndicator!==0){
|
||||
throw new Error('unimplemented windowHeader.deltaIndicator:'+windowHeader.deltaIndicator);
|
||||
}
|
||||
|
||||
windowHeader.addRunDataLength = this.read7BitEncodedInt();
|
||||
windowHeader.instructionsLength = this.read7BitEncodedInt();
|
||||
windowHeader.addressesLength = this.read7BitEncodedInt();
|
||||
|
||||
if(windowHeader.indicator & VCD_ADLER32){
|
||||
windowHeader.adler32 = this.readU32();
|
||||
}
|
||||
|
||||
|
||||
return windowHeader;
|
||||
}
|
||||
|
||||
|
||||
VCDIFF_Parser.prototype.copyToFile2=function(target, targetOffset, len){
|
||||
for(var i=0; i<len; i++){
|
||||
target._u8array[targetOffset+i]=this._u8array[this.offset+i];
|
||||
}
|
||||
//this.file.copyToFile(target, this.offset, len, targetOffset);
|
||||
this.skip(len);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// hdrIndicator
|
||||
const VCD_DECOMPRESS = 0x01;
|
||||
const VCD_CODETABLE = 0x02;
|
||||
const VCD_APPHEADER = 0x04; // nonstandard?
|
||||
|
||||
// winIndicator
|
||||
const VCD_SOURCE = 0x01;
|
||||
const VCD_TARGET = 0x02;
|
||||
const VCD_ADLER32 = 0x04;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function VCD_Instruction(instruction, size, mode){
|
||||
this.type=instruction;
|
||||
this.size=size;
|
||||
this.mode=mode;
|
||||
}
|
||||
|
||||
/*
|
||||
build the default code table (used to encode/decode instructions) specified in RFC 3284
|
||||
heavily based on
|
||||
https://github.com/vic-alexiev/TelerikAcademy/blob/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff/CodeTable.cs
|
||||
*/
|
||||
const VCD_NOOP=0;
|
||||
const VCD_ADD=1;
|
||||
const VCD_RUN=2;
|
||||
const VCD_COPY=3;
|
||||
const VCD_DEFAULT_CODE_TABLE=(function(){
|
||||
var entries=[];
|
||||
|
||||
var empty = {type: VCD_NOOP, size: 0, mode: 0};
|
||||
|
||||
// 0
|
||||
entries.push([{type: VCD_RUN, size: 0, mode: 0}, empty]);
|
||||
|
||||
// 1,18
|
||||
for(var size=0; size<18; size++){
|
||||
entries.push([{type: VCD_ADD, size: size, mode: 0}, empty]);
|
||||
}
|
||||
|
||||
// 19,162
|
||||
for(var mode=0; mode<9; mode++){
|
||||
entries.push([{type: VCD_COPY, size: 0, mode: mode}, empty]);
|
||||
|
||||
for(var size=4; size<19; size++){
|
||||
entries.push([{type: VCD_COPY, size: size, mode: mode}, empty]);
|
||||
}
|
||||
}
|
||||
|
||||
// 163,234
|
||||
for(var mode=0; mode<6; mode++){
|
||||
for(var addSize=1; addSize<5; addSize++){
|
||||
for(var copySize=4; copySize<7; copySize++){
|
||||
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
|
||||
{type: VCD_COPY, size: copySize, mode: mode}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 235,246
|
||||
for(var mode=6; mode<9; mode++){
|
||||
for(var addSize=1; addSize<5; addSize++){
|
||||
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
|
||||
{type: VCD_COPY, size: 4, mode: mode}]);
|
||||
}
|
||||
}
|
||||
|
||||
// 247,255
|
||||
for(var mode=0; mode<9; mode++){
|
||||
entries.push([{type: VCD_COPY, size: 4, mode: mode},
|
||||
{type: VCD_ADD, size: 1, mode: 0}]);
|
||||
}
|
||||
|
||||
return entries;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
/*
|
||||
ported from https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
|
||||
by Victor Alexiev (https://github.com/vic-alexiev)
|
||||
*/
|
||||
const VCD_MODE_SELF=0;
|
||||
const VCD_MODE_HERE=1;
|
||||
function VCD_AdressCache(nearSize, sameSize){
|
||||
this.nearSize=nearSize;
|
||||
this.sameSize=sameSize;
|
||||
|
||||
this.near=new Array(nearSize);
|
||||
this.same=new Array(sameSize*256);
|
||||
}
|
||||
VCD_AdressCache.prototype.reset=function(addressStream){
|
||||
this.nextNearSlot=0;
|
||||
this.near.fill(0);
|
||||
this.same.fill(0);
|
||||
|
||||
this.addressStream=addressStream;
|
||||
}
|
||||
VCD_AdressCache.prototype.decodeAddress=function(here, mode){
|
||||
var address=0;
|
||||
|
||||
if(mode===VCD_MODE_SELF){
|
||||
address=this.addressStream.read7BitEncodedInt();
|
||||
}else if(mode===VCD_MODE_HERE){
|
||||
address=here-this.addressStream.read7BitEncodedInt();
|
||||
}else if(mode-2<this.nearSize){ //near cache
|
||||
address=this.near[mode-2]+this.addressStream.read7BitEncodedInt();
|
||||
}else{ //same cache
|
||||
var m=mode-(2+this.nearSize);
|
||||
address=this.same[m*256+this.addressStream.readU8()];
|
||||
}
|
||||
|
||||
this.update(address);
|
||||
return address;
|
||||
}
|
||||
VCD_AdressCache.prototype.update=function(address){
|
||||
if(this.nearSize>0){
|
||||
this.near[this.nextNearSlot]=address;
|
||||
this.nextNearSlot=(this.nextNearSlot+1)%this.nearSize;
|
||||
}
|
||||
|
||||
if(this.sameSize>0){
|
||||
this.same[address%(this.sameSize*256)]=address;
|
||||
}
|
||||
}
|
28
rom-patcher-js/modules/zip.js/LICENSE
Normal file
|
@ -0,0 +1,28 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2013, Gildas Lormeau
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
36
rom-patcher-js/modules/zip.js/inflate.js
Normal file
2
rom-patcher-js/modules/zip.js/z-worker.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
/* jshint worker:true */
|
||||
!function(c){"use strict";if(c.zWorkerInitialized)throw new Error("z-worker.js should be run only once");c.zWorkerInitialized=!0,addEventListener("message",function(t){var e,r,c=t.data,n=c.type,s=c.sn,p=o[n];if(p)try{p(c)}catch(t){e={type:n,sn:s,error:(r=t,{message:r.message,stack:r.stack})},postMessage(e)}});var o={importScripts:function(t){t.scripts&&0<t.scripts.length&&importScripts.apply(void 0,t.scripts);postMessage({type:"importScripts"})},newTask:h,append:t,flush:t},f={};function h(t){var e=c[t.codecClass],r=t.sn;if(f[r])throw Error("duplicated sn");f[r]={codec:new e(t.options),crcInput:"input"===t.crcType,crcOutput:"output"===t.crcType,crc:new n},postMessage({type:"newTask",sn:r})}var l=c.performance?c.performance.now.bind(c.performance):Date.now;function t(t){var e=t.sn,r=t.type,c=t.data,n=f[e];!n&&t.codecClass&&(h(t),n=f[e]);var s,p="append"===r,o=l();if(p)try{s=n.codec.append(c,function(t){postMessage({type:"progress",sn:e,loaded:t})})}catch(t){throw delete f[e],t}else delete f[e],s=n.codec.flush();var a=l()-o;o=l(),c&&n.crcInput&&n.crc.append(c),s&&n.crcOutput&&n.crc.append(s);var i=l()-o,u={type:r,sn:e,codecTime:a,crcTime:i},d=[];s&&(u.data=s,d.push(s.buffer)),p||!n.crcInput&&!n.crcOutput||(u.crc=n.crc.get());try{postMessage(u,d)}catch(t){postMessage(u)}}function n(){this.crc=-1}function e(){}n.prototype.append=function(t){for(var e=0|this.crc,r=this.table,c=0,n=0|t.length;c<n;c++)e=e>>>8^r[255&(e^t[c])];this.crc=e},n.prototype.get=function(){return~this.crc},n.prototype.table=function(){var t,e,r,c=[];for(t=0;t<256;t++){for(r=t,e=0;e<8;e++)1&r?r=r>>>1^3988292384:r>>>=1;c[t]=r}return c}(),(c.NOOP=e).prototype.append=function(t,e){return t},e.prototype.flush=function(){}}(this);
|
28
rom-patcher-js/modules/zip.js/zip.min.js
vendored
Normal file
502
rom-patcher-js/style.css
Normal file
|
@ -0,0 +1,502 @@
|
|||
/* Rom Patcher JS template CSS stylesheet */
|
||||
/* customize it to your taste! */
|
||||
|
||||
|
||||
/* @FONT-FACES */
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700');
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300');
|
||||
|
||||
|
||||
/* Rom Patcher JS - container */
|
||||
#rom-patcher-container {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
line-height: 1.8;
|
||||
|
||||
background-color: #f9fafa;
|
||||
padding: 30px 15px;
|
||||
border-radius: 3px;
|
||||
color: black;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Rom Pacher JS - text classes */
|
||||
#rom-patcher-container .text-mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-muted {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-right {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-selectable {
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
-o-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* Rom Patcher JS - rows */
|
||||
#rom-patcher-container .rom-patcher-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-row>div:first-child {
|
||||
width: 25%
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-row>div:last-child {
|
||||
width: 73%
|
||||
}
|
||||
|
||||
#rom-patcher-container .margin-bottom {
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
|
||||
|
||||
#rom-patcher-container #rom-patcher-row-info-rom,
|
||||
#rom-patcher-container #rom-patcher-row-alter-header,
|
||||
#rom-patcher-container #rom-patcher-row-patch-description,
|
||||
#rom-patcher-container #rom-patcher-row-patch-requirements,
|
||||
#rom-patcher-container #rom-patcher-row-error-message {
|
||||
display: none
|
||||
}
|
||||
|
||||
#rom-patcher-container #rom-patcher-row-info-rom.show,
|
||||
#rom-patcher-container #rom-patcher-row-alter-header.show,
|
||||
#rom-patcher-container #rom-patcher-row-patch-description.show,
|
||||
#rom-patcher-container #rom-patcher-row-patch-requirements.show {
|
||||
display: flex
|
||||
}
|
||||
|
||||
#rom-patcher-container #rom-patcher-row-error-message.show {
|
||||
display: block
|
||||
}
|
||||
|
||||
#rom-patcher-patch-description {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
#rom-patcher-row-apply {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
|
||||
#rom-patcher-span-crc32 span.clickable {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
#rom-patcher-span-crc32 span.clickable:hover {
|
||||
cursor: pointer;
|
||||
color: black
|
||||
}
|
||||
|
||||
#rom-patcher-error-message {
|
||||
color: #ff0030;
|
||||
padding-left: 20px;
|
||||
background-image: url(assets/icon_x_circle_red.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
}
|
||||
#rom-patcher-error-message.warning {
|
||||
color: #ff7800;
|
||||
background-image: url(assets/icon_alert_orange.svg);
|
||||
}
|
||||
|
||||
/* Rom Patcher JS - form elements */
|
||||
#rom-patcher-container input[type=file],
|
||||
#rom-patcher-container input[type=checkbox],
|
||||
#rom-patcher-container select {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background-color: #edefef;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]:focus:not(:disabled),
|
||||
#rom-patcher-container select:focus:not(:disabled),
|
||||
#rom-patcher-container input[type=checkbox]:focus:not(:disabled) {
|
||||
box-shadow: #a8fff3 0 0 0 2px;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file] {
|
||||
width: 100%;
|
||||
padding: 6px 10px
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]::file-selector-button {
|
||||
display: none
|
||||
}
|
||||
|
||||
#rom-patcher-container select {
|
||||
width: 100%;
|
||||
padding: 6px 18px 6px 10px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
text-overflow: '';
|
||||
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMTJweCIgeT0iMHB4IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIzcHgiIHZpZXdCb3g9IjAgMCA2IDMiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYgMyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBvbHlnb24gcG9pbnRzPSI1Ljk5MiwwIDIuOTkyLDMgLTAuMDA4LDAgIi8+PC9zdmc+");
|
||||
background-position: 100% center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#rom-patcher-container select::-ms-expand {
|
||||
display: none
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]:hover:not(:disabled),
|
||||
#rom-patcher-container select:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
background-color: #dee1e1
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]:disabled,
|
||||
#rom-patcher-container select:disabled {
|
||||
color: #888
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#rom-patcher-container input[type=file].empty,
|
||||
#rom-patcher-container input[type=file]:not(:disabled):not(.empty):hover {
|
||||
padding-left: 32px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
background-size: 16px;
|
||||
background-image: url(assets/icon_upload.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].valid {
|
||||
background-color: #d6ffc8;
|
||||
padding-right: 28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
background-image: url(assets/icon_check_circle_green.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].valid:not(:disabled):not(.empty):hover {
|
||||
background-color: #adf795;
|
||||
background-position: 8px center, right 12px center;
|
||||
background-size: 16px, 16px;
|
||||
background-image: url(assets/icon_upload.svg), url(assets/icon_check_circle_green.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].invalid {
|
||||
background-color: #ffc8c8;
|
||||
padding-right: 28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
background-image: url(assets/icon_x_circle_red.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].invalid:not(:disabled):not(.empty):hover {
|
||||
background-color: #ffa3a3;
|
||||
background-position: 8px center, right 12px center;
|
||||
background-size: 16px, 16px;
|
||||
background-image: url(assets/icon_upload.svg), url(assets/icon_x_circle_red.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].icon-upload {
|
||||
padding-left: 32px;
|
||||
background-image: url(assets/app_icon_16.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
}
|
||||
|
||||
|
||||
#rom-patcher-container input[type=checkbox] {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: transparent;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
border: 2px solid #2a9ca5;
|
||||
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMuMiIgZmlsbD0ibm9uZSIgZD0iTSAxLjE5LDcuMTAgNi4wNywxMi4wNiAxNC44OCwzLjMyIi8+PC9zdmc+');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 0px;
|
||||
transition: background-color .2s, background-size .2s;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=checkbox]:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
border-color: #3aacb5;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=checkbox]:hover:checked:not(:disabled) {
|
||||
background-color: #3aacb5;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=checkbox]:checked {
|
||||
background-color: #2a9ca5;
|
||||
background-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* buttons */
|
||||
#rom-patcher-container button {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
min-width: 120px;
|
||||
border-radius: 3px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
|
||||
padding: 10px 20px;
|
||||
margin: 0 5px;
|
||||
|
||||
background-color: #2a9ca5;
|
||||
color: white;
|
||||
|
||||
transition: background-color .15s;
|
||||
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
#rom-patcher-container button:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#rom-patcher-container button:disabled {
|
||||
opacity: .35 !important;
|
||||
cursor: not-allowed
|
||||
}
|
||||
|
||||
#rom-patcher-container button:not(:disabled):hover {
|
||||
background-color: #3aacb5;
|
||||
}
|
||||
|
||||
#rom-patcher-container button:not(:disabled):active {
|
||||
background-color: #297b81;
|
||||
transform: translateY(1px)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* loading spinner */
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rom-patcher-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rom-patcher-spinner:before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #41bdc7;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
#patch-builder-button-create .rom-patcher-spinner:before,
|
||||
#rom-patcher-button-apply .rom-patcher-spinner:before {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#rom-patcher-container .rom-patcher-container-input {
|
||||
position: relative
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-container-input input.loading,
|
||||
#rom-patcher-container .rom-patcher-container-input select.loading {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-container-input input.loading+.rom-patcher-spinner,
|
||||
#rom-patcher-container .rom-patcher-container-input select.loading+.rom-patcher-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
/* ZIP dialog */
|
||||
#rom-patcher-dialog-zip::backdrop,
|
||||
#rom-patcher-dialog-zip-backdrop {
|
||||
background-color: rgba(0, 0, 0, .75);
|
||||
backdrop-filter: blur(3px);
|
||||
/*
|
||||
transition: overlay 0.35s allow-discrete, display 0.35s allow-discrete, opacity 0.35s;
|
||||
opacity: 0;
|
||||
}
|
||||
#rom-patcher-dialog-zip[open]::backdrop {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
#rom-patcher-dialog-zip-backdrop {
|
||||
/* fallback for browsers not compatible with <dialog> */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip {
|
||||
min-width: 420px;
|
||||
vertical-align: middle;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
color: #999;
|
||||
box-sizing: border-box;
|
||||
box-shadow: rgba(0, 0, 0, .7) 0 0 24px;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
/*
|
||||
transition: overlay 0.35s allow-discrete, display 0.35s allow-discrete, opacity 0.35s;
|
||||
opacity: 0;
|
||||
}
|
||||
#rom-patcher-dialog-zip[open] {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-message {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-file-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-file-list li {
|
||||
color: #3c3c3c;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-file-list li:hover {
|
||||
background-color: #eee;
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* responsive */
|
||||
@media only screen and (max-width:641px) {
|
||||
#rom-patcher-container {
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
#rom-patcher-rom-info {
|
||||
font-size: 11px
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#rom-patcher-powered {
|
||||
margin-top: 16px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#rom-patcher-powered a {
|
||||
color: #cce;
|
||||
padding: 4px 8px;
|
||||
text-decoration: none;
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
#rom-patcher-powered a>img {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#rom-patcher-powered a:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 1;
|
||||
}
|
171
test.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Test battery for Rom Patcher JS
|
||||
https://github.com/marcrobledo/RomPatcher.js
|
||||
by Marc Robledo, released under MIT license: https://github.com/marcrobledo/RomPatcher.js/blob/master/LICENSE
|
||||
|
||||
Usage:
|
||||
> npm install
|
||||
> npm run test
|
||||
|
||||
You need to provide the following ROMs and patches, unzip them in
|
||||
_test_files/roms and _test_files/patches folders respectively:
|
||||
- IPS test
|
||||
- Patch: https://www.romhacking.net/hacks/3784/
|
||||
- ROM: Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb [CRC32=d5ec24e4]
|
||||
- BPS test
|
||||
- Patch: https://www.romhacking.net/translations/6297/
|
||||
- ROM: Samurai Kid (Japan).gbc [CRC32=44a9ddfb]
|
||||
- UPS test
|
||||
- Patch: https://mother3.fobby.net/
|
||||
- ROM: Mother 3 (Japan).gba [CRC32=42ac9cb9]
|
||||
- APS test
|
||||
- Patch: http://dorando.emuverse.com/projects/eduardo_a2j/zelda-ocarina-of-time.html
|
||||
- ROM: Legend of Zelda, The - Ocarina of Time (USA).z64 [CRC32=7e107c35]
|
||||
- RUP test
|
||||
- Patch: https://www.romhacking.net/translations/843/
|
||||
- ROM: Uchuu no Kishi - Tekkaman Blade (Japan).sfc [CRC32=cd16c529]
|
||||
*/
|
||||
|
||||
const chalk=require('chalk');
|
||||
const { existsSync }=require('fs');
|
||||
|
||||
const BinFile = require('./app/modules/BinFile');
|
||||
const HashCalculator = require('./app/modules/HashCalculator');
|
||||
const RomPatcher = require('./app/RomPatcher');
|
||||
|
||||
|
||||
|
||||
const TEST_PATH='_test_files/';
|
||||
const TEST_PATCHES=[
|
||||
{
|
||||
title:'IPS - Super Mario Land 2 DX',
|
||||
romFile:'Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb',
|
||||
romCrc32:0xd5ec24e4,
|
||||
patchFile:'SML2DXv181.ips',
|
||||
patchCrc32:0x0b742316,
|
||||
patchDownload:'https://www.romhacking.net/hacks/3784/',
|
||||
outputCrc32:0xf0799017
|
||||
},{
|
||||
title:'BPS - Samurai Kid translation',
|
||||
romFile:'Samurai Kid (Japan).gbc',
|
||||
romCrc32:0x44a9ddfb,
|
||||
patchFile:'samurai_kid_en_v1.bps',
|
||||
patchCrc32:0x2144df1c,
|
||||
patchDownload:'https://www.romhacking.net/translations/6297/',
|
||||
outputCrc32:0xed238edb
|
||||
},{
|
||||
title:'UPS - Mother 3 translation',
|
||||
romFile:'Mother 3 (Japan).gba',
|
||||
romCrc32:0x42ac9cb9,
|
||||
patchFile:'mother3.ups',
|
||||
patchCrc32:0x2144df1c,
|
||||
patchDownload:'https://mother3.fobby.net/',
|
||||
outputCrc32:0x8a3bc5a8
|
||||
},{
|
||||
title:'APS - Zelda OoT spanish translation',
|
||||
romFile:'Legend of Zelda, The - Ocarina of Time (USA).z64',
|
||||
romCrc32:0xcd16c529,
|
||||
patchFile:'ZELDA64.APS',
|
||||
patchCrc32:0x7b70119d,
|
||||
patchDownload:'http://dorando.emuverse.com/projects/eduardo_a2j/zelda-ocarina-of-time.html',
|
||||
outputCrc32:0x7866f1ca
|
||||
},{
|
||||
title:'Tekkaman Blade translation',
|
||||
romFile:'Uchuu no Kishi - Tekkaman Blade (Japan).sfc',
|
||||
romCrc32:0x7e107c35,
|
||||
patchFile:'Tekkaman Blade v1.0.rup',
|
||||
patchCrc32:0x621ab323,
|
||||
patchDownload:'https://www.romhacking.net/hacks/4633/',
|
||||
outputCrc32:0xe83e9b0a
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const _test=function(title, testFunction){
|
||||
try{
|
||||
const startTime=(new Date()).getTime();
|
||||
const result=testFunction.call();
|
||||
|
||||
const executionTime=((new Date()).getTime() - startTime) / 1000;
|
||||
console.log(chalk.greenBright('√ '+title + ' ('+executionTime+'s)'));
|
||||
}catch(err){
|
||||
console.log(chalk.redBright('× '+title + ' - failed with error: '+err.message));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const TEST_DATA = (new Uint8Array([
|
||||
98, 91, 64, 8, 35, 53, 122, 167, 52, 253, 222, 156, 247, 82, 227, 213, 22, 221, 17, 247, 107, 102, 164, 254, 221, 102, 207, 63, 117, 164, 223, 10, 223, 200, 150, 4, 77, 250, 111, 64, 233, 118, 1, 36, 1, 60, 208, 245, 136, 126, 29, 231, 168, 18, 125, 172, 11, 184, 81, 20, 16, 30, 154, 16, 236, 21, 5, 74, 255, 112, 171, 198, 185, 89, 2, 98, 45, 164, 214, 55, 103, 15, 217, 95, 212, 133, 184, 21, 67, 144, 198, 163, 76, 35, 248, 229, 163, 37, 103, 33, 193, 160, 161, 245, 125, 144, 193, 178, 31, 253, 119, 168, 169, 187, 195, 165, 205, 140, 222, 134, 249, 68, 224, 248, 144, 207, 18, 126
|
||||
])).buffer;
|
||||
|
||||
|
||||
|
||||
|
||||
_test('HashCalculator integrity', function(){
|
||||
if(HashCalculator.md5(TEST_DATA) !== '55c76e7e683fd7cd63c673c5df3efa6e')
|
||||
throw new Error('invalid MD5');
|
||||
if(HashCalculator.crc32(TEST_DATA).toString(16) !== '903a031b')
|
||||
throw new Error('invalid CRC32');
|
||||
if(HashCalculator.adler32(TEST_DATA).toString(16) !== 'ef984205')
|
||||
throw new Error('invalid ADLER32');
|
||||
if(HashCalculator.crc16(TEST_DATA).toString(16) !== '96e4')
|
||||
throw new Error('invalid SHA1');
|
||||
});
|
||||
|
||||
|
||||
const MODIFIED_TEST_DATA = (new Uint8Array([
|
||||
98, 91, 64, 8, 35, 53, 122, 167, 52, 253, 222, 156, 247, 82, 227, 213, 22, 221, 17, 247, 107, 102, 164, 254, 221, 8, 207, 63, 117, 164, 223, 10, 1, 77, 87, 123, 48, 9, 111, 64, 233, 118, 1, 36, 1, 60, 208, 245, 136, 126, 29, 231, 168, 18, 125, 172, 11, 184, 81, 20, 16, 30, 154, 16, 236, 21, 5, 74, 255, 112, 171, 198, 185, 89, 2, 98, 45, 164, 214, 55, 103, 15, 217, 95, 212, 133, 184, 21, 67, 144, 198, 163, 76, 35, 248, 229, 163, 37, 103, 33, 193, 96, 77, 255, 117, 89, 193, 61, 64, 253, 119, 82, 49, 187, 195, 165, 205, 140, 222, 134, 249, 68, 224, 248, 144, 207, 18, 126
|
||||
])).buffer;
|
||||
['ips','bps','ppf','ups','aps','rup'].forEach(function(patchFormat){
|
||||
_test('create and apply '+patchFormat.toUpperCase(), function(){
|
||||
const originalFile=new BinFile(TEST_DATA);
|
||||
const modifiedFile=new BinFile(MODIFIED_TEST_DATA);
|
||||
const patch=RomPatcher.createPatch(originalFile, modifiedFile, patchFormat);
|
||||
const patchedFile=RomPatcher.applyPatch(originalFile, patch, {requireValidation:true});
|
||||
if(patchedFile.hashCRC32() !== modifiedFile.hashCRC32())
|
||||
throw new Error('modified and patched files\' crc32 do not match');
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
TEST_PATCHES.forEach(function(patchInfo){
|
||||
const patchPath=TEST_PATH+'patches/'+patchInfo.patchFile;
|
||||
if(!existsSync(patchPath)){
|
||||
console.log(chalk.yellow('! skipping patch '+patchInfo.title));
|
||||
console.log(chalk.yellow(' patch file not found: '+patchInfo.patchFile));
|
||||
console.log(chalk.yellow(' download patch at '+patchInfo.patchDownload));
|
||||
return false;
|
||||
}
|
||||
const patchFile=new BinFile(patchPath);
|
||||
if(patchFile.hashCRC32() !== patchInfo.patchCrc32){
|
||||
console.log(patchFile.hashCRC32().toString(16));
|
||||
console.log(chalk.yellow('! skipping '+patchInfo.title+' test: invalid patch crc32'));
|
||||
console.log(chalk.yellow(' download correct patch at '+patchInfo.patchDownload));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const romPath=TEST_PATH+'roms/'+patchInfo.romFile;
|
||||
if(!existsSync(romPath)){
|
||||
console.log(chalk.yellow('! skipping patch '+patchInfo.title));
|
||||
console.log(chalk.yellow(' ROM file not found: '+patchInfo.romFile));
|
||||
return false;
|
||||
}
|
||||
const romFile=new BinFile(romPath);
|
||||
if(romFile.hashCRC32() !== patchInfo.romCrc32){
|
||||
console.log(chalk.yellow('! skipping '+patchInfo.title+' test: invalid ROM crc32'));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
_test('patch '+patchInfo.title, function(){
|
||||
const patch=RomPatcher.parsePatchFile(patchFile);
|
||||
const patchedRom=RomPatcher.applyPatch(romFile, patch, {requireValidation:true});
|
||||
if(patchedRom.hashCRC32() !== patchInfo.outputCrc32)
|
||||
throw new Error('invalid patched file crc32');
|
||||
});
|
||||
});
|
||||
|
||||
|
BIN
webapp/app_icon_114.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
webapp/app_icon_144.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
webapp/app_icon_16.png
Normal file
After Width: | Height: | Size: 202 B |
BIN
webapp/app_icon_192.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
webapp/app_icon_maskable.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
1
webapp/icon_close.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><line stroke="#888" stroke-width="4" x1="1" x2="31" y1="1" y2="31"/><line stroke="#888" stroke-width="4" x1="31" x2="1" y1="1" y2="31"/></svg>
|
After Width: | Height: | Size: 206 B |
2
webapp/icon_github.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<!-- from octicon https://github.com/primer/octicons/ -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 760 B |
1
webapp/icon_heart.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M4.3,2.1C2.816504,2.218309 1.4015208,3.1679153 0.87398438,4.58375 0.34678348,5.8777663 0.60547985,7.4216259 1.4467505,8.5260791 2.428738,9.9126453 3.9007302,10.812811 5.1733475,11.900144 6.1025539,12.652873 7.0572605,13.374847 8.03,14.07 9.8414762,12.741487 11.62015,11.365101 13.338177,9.9179978 14.534069,8.8670814 15.617932,7.3839163 15.42875,5.70625 15.351533,4.1060809 14.135607,2.6806679 12.60355,2.2616895 11.873602,2.0258183 11.076327,2.0638822 10.328633,2.1798242 9.4188311,2.3910696 8.6202638,2.9340558 8.02,3.64 7.2081279,2.6482215 5.9494781,2.0210606 4.6572266,2.0850586 4.5320909,2.0883379 4.4030623,2.0709127 4.28,2.1" fill="#e74c3c" /></svg>
|
After Width: | Height: | Size: 729 B |
1
webapp/icon_settings.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><ellipse cx="8" cy="8" fill="transparent" rx="2.5" ry="2.5" stroke="#ffffff" stroke-width="1.13" style="fill:none" /><path d="M 6.34,15.93 C 5.4588832,15.789025 5.3688301,14.771577 5.1756353,14.070626 4.88526,12.736602 3.4341324,13.644518 2.5971627,13.722372 1.429913,13.634857 1.20405,12.274774 0.57110457,11.527239 0.01256473,10.862766 -0.28025442,9.5563977 0.70078865,9.1307029 1.6881819,8.6864782 1.6795687,7.1576053 0.59919067,6.8587653 -0.3188158,6.2116572 0.07040277,5.0345832 0.70962743,4.3579058 1.1458066,3.5997233 1.4745511,2.6165206 2.3772581,2.2995448 3.2853269,2.2266882 4.6163607,3.3077881 5.1380438,2.0525892 5.3177552,1.1958744 5.5325932,-0.04373646 6.6754583,0.01614659 7.7138044,0.09890156 8.7722596,-0.11077281 9.7963088,0.11827521 10.569368,0.31098193 10.571622,1.2355275 10.750403,1.8610601 c 0.307427,1.3026813 1.685123,0.6224444 2.503868,0.3533436 0.82764,-0.090277 1.050717,0.8516809 1.472505,1.360975 0.38881,0.6166529 0.834189,1.2049045 1.187448,1.8394061 0.330486,1.0831614 -0.991271,1.5371101 -1.414855,2.2998779 -0.2856,1.1308037 1.380075,1.3511924 1.444845,2.3814793 -0.0079,0.927004 -0.692382,1.66038 -1.102823,2.444419 -0.39067,0.912507 -1.296704,1.612005 -2.278176,1.000092 -0.946701,-0.63303 -1.993578,0.112458 -1.907388,1.191742 0.0079,1.047847 -1.1122687,1.390845 -1.9768201,1.270072 C 7.8997219,15.932974 7.1191405,16.008245 6.34,15.93 Z m 3,-1.22 c 0.072192,-1.038342 0.5424188,-2.201536 1.610519,-2.551894 0.862697,-0.55308 2.289047,0.880644 2.758176,-0.364357 0.567891,-0.682315 1.24254,-1.736649 0.133817,-2.2771293 C 12.89432,8.7323482 13.006994,7.194583 13.884539,6.4028471 14.39852,6.1084948 14.833041,5.6436658 14.236602,5.1479432 13.957925,4.5967831 13.628958,4.0709474 13.33,3.53 12.383852,3.9354666 11.178531,4.1915721 10.300674,3.4919254 9.2869972,3.1081771 9.8933516,1.1858595 8.7077559,1.2901448 8.0634396,1.4130361 6.9692111,1.0406335 6.610177,1.4803803 6.5078416,2.4182135 6.0678498,3.4743442 5.0999314,3.7923931 4.2722569,4.3906884 3.3123355,3.3221592 2.4772941,3.8142154 2.2352934,4.5382998 0.87395464,5.4749545 1.8007346,6.1937991 c 1.053982,0.664386 1.302272,2.235122 0.4884086,3.1815951 -0.5474043,0.4278527 -1.2164805,0.9805138 -0.5529679,1.6797018 0.467778,0.565173 0.7706283,1.822385 1.7323918,1.179994 0.7375009,-0.386335 1.6577438,-0.111792 2.2758,0.38501 0.5290179,0.419069 0.7676252,1.08802 0.7930337,1.743226 0.4594894,0.831464 1.6051495,0.156768 2.3667841,0.36409 C 9.0495442,14.724232 9.1948683,14.718824 9.34,14.71 Z" fill="#ffffff" /></svg>
|
After Width: | Height: | Size: 2.5 KiB |
BIN
webapp/logo.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
629
webapp/style.css
Normal file
|
@ -0,0 +1,629 @@
|
|||
/* Rom Patcher JS CSS template by Marc Robledo v20220323 */
|
||||
|
||||
/* @FONT-FACES */
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700');
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300');
|
||||
|
||||
|
||||
body{
|
||||
--rom-patcher-color-background:#31343a;
|
||||
--rom-patcher-color-primary:#2a9ca5;
|
||||
--rom-patcher-color-primary-hover:#3aacb5;
|
||||
--rom-patcher-color-primary-active:#297b81;
|
||||
--rom-patcher-color-primary-focus:#a8fff3;
|
||||
|
||||
--rom-patcher-color-danger:#ff0030;
|
||||
--rom-patcher-color-warning:#ff7800;
|
||||
|
||||
--rom-patcher-color-spinner:#41bdc7;
|
||||
|
||||
--rom-patcher-color-muted:#888;
|
||||
--rom-patcher-color-outer-btn:#fff;
|
||||
--rom-patcher-color-outer-btn-hover:#2b2e33;
|
||||
|
||||
--rom-patcher-color-footer:#767b86;
|
||||
--rom-patcher-color-footer-link:#969ba6;
|
||||
--rom-patcher-color-footer-link-border:#464b56;
|
||||
--rom-patcher-color-footer-hover:#fff;
|
||||
--rom-patcher-color-footer-hover-border:#41d5ff;
|
||||
|
||||
--rom-patcher-color-switch:#fff;
|
||||
--rom-patcher-color-switch-background:#474c56;
|
||||
--rom-patcher-color-switch-background-enabled:#41bdc7;
|
||||
|
||||
margin:0;
|
||||
font:15px 'Open Sans',sans-serif;
|
||||
cursor:default;
|
||||
line-height:1.8;
|
||||
background-color:var(--rom-patcher-color-background);
|
||||
color:#3c3c3c;
|
||||
-moz-user-select:none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select:none;
|
||||
-o-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
/* body.dragging-files{
|
||||
color:red !important;
|
||||
pointer-events: none;
|
||||
} */
|
||||
|
||||
/* flex main column */
|
||||
html, body{height:100%}
|
||||
#column{
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
header{margin: 1% 0 4%}
|
||||
header h1{display:none}
|
||||
footer{padding: 50px 0 20px}
|
||||
#wrapper{flex-basis:100%}
|
||||
|
||||
|
||||
.clickable{cursor:pointer}
|
||||
.hide{display:none !important}
|
||||
.text-center{text-align:center}
|
||||
.text-right{text-align:right}
|
||||
.text-truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.m-b{margin-bottom:8px}
|
||||
/* flex box */
|
||||
.row{
|
||||
display:flex;
|
||||
flex-flow:row wrap; /* this is the same as flex-direction:row;flex-wrap:wrap; */
|
||||
align-items:center; /* vertical align */
|
||||
justify-content:space-between
|
||||
}
|
||||
.row>div:first-child{width:25%}
|
||||
.row>div:last-child{width:73%}
|
||||
.row.row-lg>div:first-child{width:70%}
|
||||
.row.row-lg>div:last-child{width:30%}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* icons */
|
||||
.icon{
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
width:16px;height:16px
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* header+footer */
|
||||
header{text-align:center}
|
||||
header h1{margin:0}
|
||||
header img{max-width:90%; height:192px;}
|
||||
|
||||
footer{
|
||||
text-align:center;
|
||||
color:var(--rom-patcher-color-footer);
|
||||
font-size:85%;
|
||||
}
|
||||
footer a{
|
||||
color:var(--rom-patcher-color-footer-link);
|
||||
text-decoration:none;
|
||||
border-bottom:1px solid var(--rom-patcher-color-footer-link-border);
|
||||
}
|
||||
footer a:hover{
|
||||
color:var(--rom-patcher-color-footer-hover);
|
||||
border-color:var(--rom-patcher-color-footer-hover-border);
|
||||
}
|
||||
|
||||
|
||||
|
||||
hr{border:none;border-top:1px dotted #bbb;margin:15px 0}
|
||||
|
||||
|
||||
/* outer buttons */
|
||||
.btn-transparent{
|
||||
background-color:transparent;
|
||||
color:var(--rom-patcher-color-outer-btn);
|
||||
cursor:pointer;
|
||||
text-align:center;
|
||||
}
|
||||
.btn-transparent:hover,.btn-transparent:focus{
|
||||
background-color:var(--rom-patcher-color-outer-btn-hover);
|
||||
cursor:pointer;
|
||||
}
|
||||
.btn-transparent img{
|
||||
height:16px;
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
/* Switch mode */
|
||||
#switch-container{
|
||||
text-align:right;
|
||||
margin-bottom:10px;
|
||||
font-size:88%;
|
||||
}
|
||||
#switch-create-button{
|
||||
border-radius:2px;
|
||||
padding: 6px 8px;
|
||||
transition:background-color .1s;
|
||||
}
|
||||
.switch{
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
width:30px;height:16px;
|
||||
border-radius:8px;
|
||||
position:relative;
|
||||
transition:background-color .2s;
|
||||
background-color:#babfbf;
|
||||
}
|
||||
#switch-create-button .switch.disabled{background-color:var(--rom-patcher-color-switch-background);}
|
||||
#dialog-settings>.rom-patcher-dialog .switch{transition:opacity .1s;}
|
||||
#dialog-settings>.rom-patcher-dialog .switch:hover{cursor:pointer;opacity:.7}
|
||||
.switch:before{
|
||||
position:absolute;
|
||||
background-color:var(--rom-patcher-color-switch);
|
||||
height:10px;width:10px;
|
||||
content:" ";
|
||||
border-radius:6px;
|
||||
top:3px;
|
||||
left:4px;
|
||||
transition:left .2s;
|
||||
}
|
||||
.switch.enabled:before{
|
||||
left:16px;
|
||||
}
|
||||
.switch.enabled{background-color:var(--rom-patcher-color-switch-background-enabled);}
|
||||
|
||||
|
||||
|
||||
|
||||
.tab{background-color:#f9fafa;padding:30px 15px;border-radius: 3px}
|
||||
|
||||
|
||||
.buttons{
|
||||
margin-top:12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* forms */
|
||||
input[type=file], input[type=checkbox], select{
|
||||
box-sizing:border-box;
|
||||
max-width:100%;
|
||||
font-family:inherit;
|
||||
font-size:100%;
|
||||
outline:none;
|
||||
border:none;
|
||||
border-radius:3px;
|
||||
background-color:#edefef;
|
||||
}
|
||||
input[type=file]:focus:not(:disabled),
|
||||
select:focus:not(:disabled),
|
||||
input[type=checkbox].styled:focus:not(:disabled){
|
||||
box-shadow: var(--rom-patcher-color-primary-focus) 0 0 0 2px;
|
||||
}
|
||||
input[type=file].w100, select.w100{width:100%}
|
||||
input[type=file]{padding:6px 10px}
|
||||
input[type=file]::file-selector-button{display:none}
|
||||
select{
|
||||
padding:6px 18px 6px 10px;
|
||||
-webkit-appearance:none;
|
||||
-moz-appearance:none;
|
||||
text-overflow:'';
|
||||
|
||||
background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMTJweCIgeT0iMHB4IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIzcHgiIHZpZXdCb3g9IjAgMCA2IDMiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYgMyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBvbHlnb24gcG9pbnRzPSI1Ljk5MiwwIDIuOTkyLDMgLTAuMDA4LDAgIi8+PC9zdmc+");
|
||||
background-position:100% center;
|
||||
background-repeat:no-repeat;
|
||||
}
|
||||
select::-ms-expand{display:none}
|
||||
input[type=file]:hover:not(:disabled),select:hover:not(:disabled){cursor:pointer;background-color:#dee1e1}
|
||||
input[type=file]:disabled,select:disabled{color:var(--rom-patcher-color-muted)}
|
||||
|
||||
/* select:focus > option{background-color:#fff} */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
input[type=file].empty,
|
||||
input[type=file]:not(:disabled):not(.empty):hover{
|
||||
padding-left:32px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
background-size: 16px;
|
||||
background-image: url(../rom-patcher-js/assets/icon_upload.svg);
|
||||
}
|
||||
|
||||
input[type=file].valid{
|
||||
background-color:#d6ffc8;
|
||||
padding-right:28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
background-image: url(../rom-patcher-js/assets/icon_check_circle_green.svg);
|
||||
}
|
||||
input[type=file].valid:not(:disabled):not(.empty):hover{
|
||||
background-color:#adf795;
|
||||
background-position: 8px center, right 12px center;
|
||||
background-size: 16px, 16px;
|
||||
background-image: url(../rom-patcher-js/assets/icon_upload.svg), url(../rom-patcher-js/assets/icon_check_circle_green.svg);
|
||||
}
|
||||
input[type=file].invalid{
|
||||
background-color:#ffc8c8;
|
||||
padding-right:28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
background-image: url(../rom-patcher-js/assets/icon_x_circle_red.svg);
|
||||
}
|
||||
input[type=file].invalid:not(:disabled):not(.empty):hover{
|
||||
background-color:#ffa3a3;
|
||||
background-position: 8px center, right 12px center;
|
||||
background-size: 16px, 16px;
|
||||
background-image: url(../rom-patcher-js/assets/icon_upload.svg), url(../rom-patcher-js/assets/icon_x_circle_red.svg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* buttons */
|
||||
button{
|
||||
font-family:inherit;
|
||||
font-size:100%;
|
||||
min-width:120px;
|
||||
border-radius:3px;border:0;
|
||||
outline:none;
|
||||
|
||||
padding:10px 20px;
|
||||
margin:0 5px;
|
||||
|
||||
background-color:var(--rom-patcher-color-primary);
|
||||
color:white;
|
||||
|
||||
transition:background-color .15s;
|
||||
|
||||
box-sizing:border-box
|
||||
}
|
||||
button:not(:disabled){
|
||||
cursor:pointer;
|
||||
}
|
||||
button:disabled{opacity:.35 !important;cursor:not-allowed}
|
||||
.tab button:not(:disabled):hover{
|
||||
background-color:var(--rom-patcher-color-primary-hover);
|
||||
}
|
||||
.tab button:not(:disabled):active{
|
||||
background-color:var(--rom-patcher-color-primary-active);
|
||||
transform:translateY(1px)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.text-selectable{
|
||||
-moz-user-select:text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select:text;
|
||||
-o-user-select:text;
|
||||
user-select:text;
|
||||
cursor:text;
|
||||
}
|
||||
.text-mono{
|
||||
font-family:'Roboto Mono', monospace;
|
||||
color: var(--rom-patcher-color-muted);
|
||||
font-size:12px;
|
||||
}
|
||||
.text-muted{
|
||||
color: var(--rom-patcher-color-muted);
|
||||
}
|
||||
|
||||
#rom-patcher-span-crc32 span.clickable{text-decoration:underline}
|
||||
#rom-patcher-span-crc32 span.clickable:hover{cursor:pointer;color:black}
|
||||
|
||||
|
||||
#rom-patcher-row-info-rom,
|
||||
#rom-patcher-row-alter-header,
|
||||
#rom-patcher-row-patch-description,
|
||||
#rom-patcher-row-patch-requirements,
|
||||
#rom-patcher-row-error-message,
|
||||
#patch-builder-row-error-message{
|
||||
display:none
|
||||
}
|
||||
#rom-patcher-row-info-rom.show,
|
||||
#rom-patcher-row-alter-header.show,
|
||||
#rom-patcher-row-patch-description.show,
|
||||
#rom-patcher-row-patch-requirements.show{
|
||||
display:flex
|
||||
}
|
||||
#rom-patcher-row-error-message.show,
|
||||
#patch-builder-row-error-message.show{
|
||||
display:block
|
||||
}
|
||||
|
||||
#rom-patcher-patch-description{font-size: 85%;}
|
||||
|
||||
|
||||
#rom-patcher-select-file-patch{width:100%}
|
||||
|
||||
#rom-patcher-error-message,
|
||||
#patch-builder-error-message {
|
||||
color: var(--rom-patcher-color-danger);
|
||||
padding-left: 20px;
|
||||
background-image: url(../rom-patcher-js/assets/icon_x_circle_red.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
}
|
||||
#rom-patcher-error-message.warning,
|
||||
#patch-builder-error-message.warning {
|
||||
color: var(--rom-patcher-color-warning);
|
||||
background-image: url(../rom-patcher-js/assets/icon_alert_orange.svg);
|
||||
}
|
||||
|
||||
|
||||
/* loading spinner */
|
||||
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
|
||||
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
|
||||
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
|
||||
|
||||
.rom-patcher-spinner{
|
||||
width:20px;
|
||||
height:20px;
|
||||
display:inline-block;
|
||||
position:relative;
|
||||
-webkit-animation:spin 1s linear infinite;
|
||||
-moz-animation:spin 1s linear infinite;
|
||||
animation:spin 1s ease-in-out infinite;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.rom-patcher-spinner:before{
|
||||
width:6px;
|
||||
height:6px;
|
||||
background-color:var(--rom-patcher-color-spinner);
|
||||
border-radius:3px;
|
||||
display:inline-block;
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:50%;
|
||||
margin-left:-3px;
|
||||
}
|
||||
|
||||
|
||||
#rom-patcher-button-apply .rom-patcher-spinner:before{
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#wrapper{
|
||||
box-sizing:border-box;
|
||||
max-width:95%;
|
||||
width:600px;
|
||||
margin:0 auto
|
||||
}
|
||||
|
||||
|
||||
.rom-patcher-container-input{
|
||||
position:relative
|
||||
}
|
||||
.rom-patcher-container-input input.loading,
|
||||
.rom-patcher-container-input select.loading{
|
||||
padding-left:32px;
|
||||
}
|
||||
.rom-patcher-container-input input.loading + .rom-patcher-spinner,
|
||||
.rom-patcher-container-input select.loading + .rom-patcher-spinner{
|
||||
position:absolute;
|
||||
top:50%;
|
||||
margin-top: -10px;
|
||||
left:8px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* dialogs */
|
||||
.rom-patcher-dialog::backdrop,
|
||||
#rom-patcher-dialog-zip-backdrop{
|
||||
background-color:rgba(0,0,0,.75);
|
||||
backdrop-filter:blur(3px);
|
||||
/*
|
||||
transition: overlay 0.35s allow-discrete, display 0.35s allow-discrete, opacity 0.35s;
|
||||
opacity: 0;
|
||||
}
|
||||
.rom-patcher-dialog[open]::backdrop {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
#rom-patcher-dialog-zip-backdrop {
|
||||
/* fallback for browsers not compatible with <dialog> */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.rom-patcher-dialog{
|
||||
vertical-align:middle;
|
||||
margin:auto;
|
||||
background-color:white;
|
||||
color:#999;
|
||||
box-sizing:border-box;
|
||||
box-shadow:rgba(0,0,0,.7) 0 0 24px;
|
||||
padding:20px;
|
||||
border-radius:3px;
|
||||
border:none;
|
||||
/*
|
||||
transition: overlay 0.35s allow-discrete, display 0.35s allow-discrete, opacity 0.35s;
|
||||
opacity: 0;
|
||||
}
|
||||
.rom-patcher-dialog[open] {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/* settings dialog */
|
||||
#dialog-settings{
|
||||
min-width:400px;
|
||||
max-width:90%;
|
||||
}
|
||||
#dialog-settings-button-close{
|
||||
padding:8px;
|
||||
border-radius:30px;
|
||||
margin-right:-8px;
|
||||
margin-top:-8px;
|
||||
transition:background-color .2s;
|
||||
height:24px;
|
||||
}
|
||||
#dialog-settings-button-close:hover{
|
||||
cursor:pointer;
|
||||
background-color:#f4f4f4;
|
||||
}
|
||||
input[type=checkbox].styled{
|
||||
-moz-appearance:none;
|
||||
-webkit-appearance:none;
|
||||
appearance:none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color:transparent;
|
||||
border-radius:3px;
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
position:relative;
|
||||
border: 2px solid var(--rom-patcher-color-primary);
|
||||
|
||||
background-image:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMuMiIgZmlsbD0ibm9uZSIgZD0iTSAxLjE5LDcuMTAgNi4wNywxMi4wNiAxNC44OCwzLjMyIi8+PC9zdmc+');
|
||||
background-repeat:no-repeat;
|
||||
background-position:center center;
|
||||
background-size:0px;
|
||||
transition: background-color .2s, background-size .2s;
|
||||
}
|
||||
input[type=checkbox].styled:hover:not(:disabled){
|
||||
cursor:pointer;
|
||||
border-color:var(--rom-patcher-color-primary-hover);
|
||||
}
|
||||
input[type=checkbox].styled:hover:checked:not(:disabled){
|
||||
background-color:var(--rom-patcher-color-primary-hover);
|
||||
}
|
||||
|
||||
input[type=checkbox].styled:checked{
|
||||
background-color:var(--rom-patcher-color-primary);
|
||||
background-size:12px;
|
||||
}
|
||||
|
||||
|
||||
/* ZIP dialog */
|
||||
#rom-patcher-dialog-zip{
|
||||
min-width:360px;
|
||||
}
|
||||
#rom-patcher-dialog-zip-message{
|
||||
text-align:center
|
||||
}
|
||||
#rom-patcher-dialog-zip-file-list{
|
||||
list-style:none;
|
||||
padding:0;
|
||||
margin: 0;
|
||||
max-height:300px;
|
||||
overflow-y:auto;
|
||||
}
|
||||
#rom-patcher-dialog-zip-file-list li{
|
||||
color:#3c3c3c;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
#rom-patcher-dialog-zip-file-list li:hover{
|
||||
background-color:#eee;
|
||||
cursor:pointer;
|
||||
color: black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* light theme */
|
||||
body.theme-light{
|
||||
--rom-patcher-color-background:#eeeceb;
|
||||
--rom-patcher-color-primary:#ff9f36;
|
||||
--rom-patcher-color-primary-hover:#ffbe78;
|
||||
--rom-patcher-color-primary-active:#ff8400;
|
||||
--rom-patcher-color-primary-focus:#ffebb4;
|
||||
|
||||
--rom-patcher-color-spinner:#ff941e;
|
||||
|
||||
--rom-patcher-color-outer-btn:#575b66;
|
||||
--rom-patcher-color-outer-btn-hover:#e6e3e1;
|
||||
|
||||
--rom-patcher-color-footer:#a89d97;
|
||||
--rom-patcher-color-footer-link:#8c817c;
|
||||
--rom-patcher-color-footer-link-border:#e3d4cc;
|
||||
--rom-patcher-color-footer-hover:#484543;
|
||||
--rom-patcher-color-footer-hover-border:#ffac41;
|
||||
|
||||
--rom-patcher-color-switch:#fff;
|
||||
--rom-patcher-color-switch-background:#cec5bd;
|
||||
--rom-patcher-color-switch-background-enabled:#ff941e;
|
||||
}
|
||||
body.theme-light header img{filter: hue-rotate(98rad) brightness(97.9%) saturate(160%)}
|
||||
body.theme-light footer img.icon.settings, body.theme-light footer img.icon.github{filter: invert(0%) brightness(30%);}
|
||||
|
||||
|
||||
/* pastel theme */
|
||||
body.theme-pastel{
|
||||
--rom-patcher-color-background:#1d2433;
|
||||
--rom-patcher-color-primary:#e65a53;
|
||||
--rom-patcher-color-primary-hover:#f4736d;
|
||||
--rom-patcher-color-primary-active:#d9413a;
|
||||
|
||||
--rom-patcher-color-spinner:#ff941e;
|
||||
|
||||
--rom-patcher-color-outer-btn:#fff;
|
||||
--rom-patcher-color-outer-btn-hover:#2b2e33;
|
||||
|
||||
--rom-patcher-color-footer:#767b86;
|
||||
--rom-patcher-color-footer-link:#969ba6;
|
||||
--rom-patcher-color-footer-link-border:#464b56;
|
||||
--rom-patcher-color-footer-hover:#fff;
|
||||
--rom-patcher-color-footer-hover-border:#41d5ff;
|
||||
|
||||
/* to-do */
|
||||
}
|
||||
body.theme-pastel header img{filter: hue-rotate(22rad) brightness(81.9%) saturate(228%)}
|
||||
|
||||
/*
|
||||
body.theme-pastel .switch.enabled{background-color:#;}
|
||||
body.theme-pastel #switch-create.disabled{background-color:#cec5bd;}
|
||||
body.theme-pastel .spinner:before{background-color:#ff941e}
|
||||
*/
|
||||
|
||||
|
||||
/* responsive */
|
||||
@media only screen and (max-width:641px){
|
||||
#wrapper{font-size:14px}
|
||||
#rom-patcher-rom-info{font-size:11px}
|
||||
header img{max-height:96px}
|
||||
#dialog-settings, #rom-patcher-dialog-zip{min-width: auto;}
|
||||
}
|
BIN
webapp/thumbnail.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
137
webapp/webapp.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
/* Rom Patcher JS (complete webapp implementation) v20240809 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
|
||||
/* service worker */
|
||||
const FORCE_HTTPS = true;
|
||||
if (FORCE_HTTPS && location.protocol === 'http:')
|
||||
location.href = window.location.href.replace('http:', 'https:');
|
||||
else if (location.protocol === 'https:' && 'serviceWorker' in navigator && window.location.hostname === 'www.marcrobledo.com')
|
||||
navigator.serviceWorker.register('/RomPatcher.js/_cache_service_worker.js', { scope: '/RomPatcher.js/' }); /* using absolute paths to avoid unexpected behaviour in GitHub Pages */
|
||||
|
||||
|
||||
/* settings */
|
||||
const LOCAL_STORAGE_SETTINGS_ID = 'rom-patcher-js-settings';
|
||||
/* default settings */
|
||||
const settings = {
|
||||
language: typeof navigator.userLanguage === 'string' ? navigator.userLanguage.substr(0, 2) : 'en',
|
||||
outputSuffix: true,
|
||||
fixChecksum: false,
|
||||
theme: 'default'
|
||||
};
|
||||
/* load settings from localStorage */
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem(LOCAL_STORAGE_SETTINGS_ID)) {
|
||||
try {
|
||||
const loadedSettings = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SETTINGS_ID));
|
||||
|
||||
if (typeof loadedSettings.language === 'string')
|
||||
settings.language = loadedSettings.language;
|
||||
|
||||
if (typeof loadedSettings.outputSuffix === 'boolean')
|
||||
settings.outputSuffix = loadedSettings.outputSuffix;
|
||||
|
||||
if (typeof loadedSettings.fixChecksum === 'boolean')
|
||||
settings.fixChecksum = loadedSettings.fixChecksum;
|
||||
|
||||
if (typeof loadedSettings.theme === 'string' && ['light'].indexOf(loadedSettings.theme) !== -1)
|
||||
settings.theme = loadedSettings.theme;
|
||||
} catch (err) {
|
||||
console.error('Error while loading settings: ' + err.message);
|
||||
}
|
||||
}
|
||||
const buildSettingsForWebapp = function () {
|
||||
return {
|
||||
language: settings.language,
|
||||
outputSuffix: settings.outputSuffix,
|
||||
fixChecksum: settings.fixChecksum,
|
||||
allowDropFiles: true,
|
||||
ondropfiles:function(evt){
|
||||
if(currentMode === 'creator'){
|
||||
ocument.getElementById('switch-create-button').click();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const saveSettings = function () {
|
||||
if (typeof localStorage !== 'undefined')
|
||||
localStorage.setItem(LOCAL_STORAGE_SETTINGS_ID, JSON.stringify(settings));
|
||||
RomPatcherWeb.setSettings(buildSettingsForWebapp());
|
||||
}
|
||||
|
||||
|
||||
var currentMode = 'patcher';
|
||||
|
||||
|
||||
|
||||
window.addEventListener('load', function (evt) {
|
||||
/* set theme */
|
||||
document.body.className = 'theme-' + settings.theme;
|
||||
|
||||
/* event listeners */
|
||||
document.getElementById('button-settings').addEventListener('click', function (evt) {
|
||||
document.getElementById('dialog-settings').showModal();
|
||||
});
|
||||
document.getElementById('dialog-settings-button-close').addEventListener('click', function (evt) {
|
||||
document.getElementById('dialog-settings').close();
|
||||
});
|
||||
|
||||
document.getElementById('settings-language').value = settings.language;
|
||||
document.getElementById('settings-language').addEventListener('change', function () {
|
||||
settings.language = this.value;
|
||||
saveSettings();
|
||||
RomPatcherWeb.translateUI(settings.language);
|
||||
});
|
||||
|
||||
document.getElementById('settings-output-suffix').checked = !settings.outputSuffix;
|
||||
document.getElementById('settings-output-suffix').addEventListener('change', function () {
|
||||
settings.outputSuffix = !this.checked;
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
document.getElementById('settings-fix-checksum').checked = settings.fixChecksum;
|
||||
document.getElementById('settings-fix-checksum').addEventListener('change', function () {
|
||||
settings.fixChecksum = this.checked;
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
document.getElementById('settings-light-theme').checked = settings.theme === 'light';
|
||||
document.getElementById('settings-light-theme').addEventListener('change', function () {
|
||||
settings.theme = this.checked ? 'light' : 'default';
|
||||
saveSettings();
|
||||
document.body.className = 'theme-' + settings.theme;
|
||||
});
|
||||
|
||||
document.getElementById('switch-create-button').addEventListener('click', function () {
|
||||
if (/disabled/.test(document.getElementById('switch-create').className)) {
|
||||
try{
|
||||
if(!PatchBuilderWeb.isInitialized())
|
||||
PatchBuilderWeb.initialize();
|
||||
}catch(err){
|
||||
document.getElementById('patch-builder-container').innerHTML = err.message;
|
||||
document.getElementById('patch-builder-container').style.color = 'red';
|
||||
}
|
||||
|
||||
currentMode = 'creator';
|
||||
document.getElementById('rom-patcher-container').style.display = 'none';
|
||||
document.getElementById('patch-builder-container').style.display = 'block';
|
||||
document.getElementById('switch-create').className = 'switch enabled';
|
||||
} else {
|
||||
currentMode = 'patcher';
|
||||
document.getElementById('rom-patcher-container').style.display = 'block';
|
||||
document.getElementById('patch-builder-container').style.display = 'none';
|
||||
document.getElementById('switch-create').className = 'switch disabled';
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const initialSettings = buildSettingsForWebapp();
|
||||
RomPatcherWeb.initialize(initialSettings);
|
||||
} catch (err) {
|
||||
var message = err.message;
|
||||
if (/incompatible browser/i.test(message))
|
||||
message = 'Your browser is outdated and it is not compatible with the latest version of Rom Patcher JS.<br/><a href="legacy/">Try the legacy version</a>';
|
||||
|
||||
document.getElementById('rom-patcher-container').innerHTML = message;
|
||||
document.getElementById('rom-patcher-container').style.color = 'red';
|
||||
}
|
||||
});
|
||||
|