diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f28a60d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +_test_files +node_modules +package-lock.json \ No newline at end of file diff --git a/LICENSE b/LICENSE index 2ef16f2..be20a15 100644 --- a/LICENSE +++ b/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 diff --git a/README.md b/README.md index f1e6b7e..27afa0c 100644 --- a/README.md +++ b/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.
+- 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
+> 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. diff --git a/_cache_service_worker.js b/_cache_service_worker.js index a10b242..9a836dd 100644 --- a/_cache_service_worker.js +++ b/_cache_service_worker.js @@ -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); } }) diff --git a/index.html b/index.html index 1f41641..82fc5b8 100644 --- a/index.html +++ b/index.html @@ -7,14 +7,14 @@ - - + + - - - - - + + + + + @@ -28,147 +28,132 @@ - - + + - - - - - - - - - - - - - - - + - - - + + + + + + + + + + + + + + + +
-

Rom Patcher JS

+

Rom Patcher JS

-
Creator mode
+
Creator mode
-
-
-
-
- +
+
+
+
+
+ +
-
-
-
CRC32:
-
MD5:
-
SHA-1:
-
-
-
-
- +
+
+
+ +
-
-
-
-
- () +
+
+
CRC32:
+
+
+
+
MD5:
+
+
+
+
SHA-1:
+
+
+
+
ROM:
+
+
+
+ +
+
+
+ +
+
+
+
Description:
+
+
+
+
ROM requirements:
+
+
+ +
+
+
-
-
-
- + -
-
- - +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
-
-
-
-
- -
-
- -
-
-
- -
-
- -
-
Patch type:
-
- -
-
- -
- - -
-
@@ -178,64 +163,59 @@ -
+ -
-
-
-
    + +
    + +
    +
    +
    + +
    -
    -
    +
    +
    +
    +
    -
    -
    -
    - -
    -
    +
    +
    +
    +
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    +
    +
    +
    + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..de34321 --- /dev/null +++ b/index.js @@ -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('','the ROM file that will be patched') + .argument('', '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('', 'the original ROM') + .argument('','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() \ No newline at end of file diff --git a/index_template.html b/index_template.html new file mode 100644 index 0000000..2613e1a --- /dev/null +++ b/index_template.html @@ -0,0 +1,126 @@ + + + + + Rom Patcher JS - Custom patcher template + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Rom Patcher JS

    +

    + 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.
    + Take a look at the sourcecode to see how it's done. +

    +
    + + + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    CRC32:
    +
    +
    +
    +
    MD5:
    +
    +
    +
    +
    SHA-1:
    +
    +
    +
    +
    ROM:
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    Description:
    +
    +
    +
    +
    ROM requirements:
    +
    +
    + +
    +
    +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/legacy/.nojekyll b/legacy/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/legacy/LICENSE b/legacy/LICENSE new file mode 100644 index 0000000..2ef16f2 --- /dev/null +++ b/legacy/LICENSE @@ -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. diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 0000000..cf3d158 --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,2 @@ +# Rom Patcher JS - Legacy version +This is the legacy 2.9.1 Rom Patcher JS, kept for compatibility purposes. \ No newline at end of file diff --git a/legacy/index.html b/legacy/index.html new file mode 100644 index 0000000..8bb4ab1 --- /dev/null +++ b/legacy/index.html @@ -0,0 +1,207 @@ + + + + Rom Patcher JS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +

    Rom Patcher JS

    + + +
    +
    Creator mode
    + +
    +
    +
    +
    + +
    +
    +
    +
    CRC32:
    +
    MD5:
    +
    SHA-1:
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + () +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    + + + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    Patch type:
    +
    + +
    +
    + +
    + + +
    +
    +
    + + + + + + + + +
    + + + +
    +
    +
    +
      +
      + +
      +
      + +
      +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      + diff --git a/js/MarcFile.js b/legacy/js/MarcFile.js similarity index 100% rename from js/MarcFile.js rename to legacy/js/MarcFile.js diff --git a/js/RomPatcher.js b/legacy/js/RomPatcher.js similarity index 98% rename from js/RomPatcher.js rename to legacy/js/RomPatcher.js index 73ea8d1..03e7e13 100644 --- a/js/RomPatcher.js +++ b/legacy/js/RomPatcher.js @@ -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); } diff --git a/js/crc.js b/legacy/js/crc.js similarity index 100% rename from js/crc.js rename to legacy/js/crc.js diff --git a/js/formats/aps_gba.js b/legacy/js/formats/aps_gba.js similarity index 100% rename from js/formats/aps_gba.js rename to legacy/js/formats/aps_gba.js diff --git a/js/formats/aps_n64.js b/legacy/js/formats/aps_n64.js similarity index 100% rename from js/formats/aps_n64.js rename to legacy/js/formats/aps_n64.js diff --git a/js/formats/bps.js b/legacy/js/formats/bps.js similarity index 100% rename from js/formats/bps.js rename to legacy/js/formats/bps.js diff --git a/js/formats/ips.js b/legacy/js/formats/ips.js similarity index 100% rename from js/formats/ips.js rename to legacy/js/formats/ips.js diff --git a/js/formats/pmsr.js b/legacy/js/formats/pmsr.js similarity index 100% rename from js/formats/pmsr.js rename to legacy/js/formats/pmsr.js diff --git a/js/formats/ppf.js b/legacy/js/formats/ppf.js similarity index 100% rename from js/formats/ppf.js rename to legacy/js/formats/ppf.js diff --git a/js/formats/rup.js b/legacy/js/formats/rup.js similarity index 100% rename from js/formats/rup.js rename to legacy/js/formats/rup.js diff --git a/js/formats/ups.js b/legacy/js/formats/ups.js similarity index 100% rename from js/formats/ups.js rename to legacy/js/formats/ups.js diff --git a/js/formats/vcdiff.js b/legacy/js/formats/vcdiff.js similarity index 100% rename from js/formats/vcdiff.js rename to legacy/js/formats/vcdiff.js diff --git a/js/formats/zip.js b/legacy/js/formats/zip.js similarity index 100% rename from js/formats/zip.js rename to legacy/js/formats/zip.js diff --git a/js/locale.js b/legacy/js/locale.js similarity index 100% rename from js/locale.js rename to legacy/js/locale.js diff --git a/js/worker_apply.js b/legacy/js/worker_apply.js similarity index 100% rename from js/worker_apply.js rename to legacy/js/worker_apply.js diff --git a/js/worker_crc.js b/legacy/js/worker_crc.js similarity index 100% rename from js/worker_crc.js rename to legacy/js/worker_crc.js diff --git a/js/worker_create.js b/legacy/js/worker_create.js similarity index 100% rename from js/worker_create.js rename to legacy/js/worker_create.js diff --git a/js/zip.js/inflate.js b/legacy/js/zip.js/inflate.js similarity index 100% rename from js/zip.js/inflate.js rename to legacy/js/zip.js/inflate.js diff --git a/js/zip.js/z-worker.js b/legacy/js/zip.js/z-worker.js similarity index 100% rename from js/zip.js/z-worker.js rename to legacy/js/zip.js/z-worker.js diff --git a/js/zip.js/zip.js b/legacy/js/zip.js/zip.js similarity index 100% rename from js/zip.js/zip.js rename to legacy/js/zip.js/zip.js diff --git a/legacy/manifest.json b/legacy/manifest.json new file mode 100644 index 0000000..5fdbafe --- /dev/null +++ b/legacy/manifest.json @@ -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" +} \ No newline at end of file diff --git a/style/RomPatcher.css b/legacy/style/RomPatcher.css similarity index 100% rename from style/RomPatcher.css rename to legacy/style/RomPatcher.css diff --git a/style/app_icon_114.png b/legacy/style/app_icon_114.png similarity index 100% rename from style/app_icon_114.png rename to legacy/style/app_icon_114.png diff --git a/style/app_icon_144.png b/legacy/style/app_icon_144.png similarity index 100% rename from style/app_icon_144.png rename to legacy/style/app_icon_144.png diff --git a/style/app_icon_16.png b/legacy/style/app_icon_16.png similarity index 100% rename from style/app_icon_16.png rename to legacy/style/app_icon_16.png diff --git a/style/app_icon_192.png b/legacy/style/app_icon_192.png similarity index 100% rename from style/app_icon_192.png rename to legacy/style/app_icon_192.png diff --git a/style/app_icon_maskable.png b/legacy/style/app_icon_maskable.png similarity index 100% rename from style/app_icon_maskable.png rename to legacy/style/app_icon_maskable.png diff --git a/style/icon_close.svg b/legacy/style/icon_close.svg similarity index 100% rename from style/icon_close.svg rename to legacy/style/icon_close.svg diff --git a/style/icon_github.svg b/legacy/style/icon_github.svg similarity index 100% rename from style/icon_github.svg rename to legacy/style/icon_github.svg diff --git a/style/icon_heart.svg b/legacy/style/icon_heart.svg similarity index 100% rename from style/icon_heart.svg rename to legacy/style/icon_heart.svg diff --git a/style/icon_settings.svg b/legacy/style/icon_settings.svg similarity index 100% rename from style/icon_settings.svg rename to legacy/style/icon_settings.svg diff --git a/style/logo.png b/legacy/style/logo.png similarity index 100% rename from style/logo.png rename to legacy/style/logo.png diff --git a/style/thumbnail.jpg b/legacy/style/thumbnail.jpg similarity index 100% rename from style/thumbnail.jpg rename to legacy/style/thumbnail.jpg diff --git a/manifest.json b/manifest.json index 5fdbafe..7987cfe 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package.json b/package.json new file mode 100644 index 0000000..230f391 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/rom-patcher-js/RomPatcher.js b/rom-patcher-js/RomPatcher.js new file mode 100644 index 0000000..f28f95a --- /dev/null +++ b/rom-patcher-js/RomPatcher.js @@ -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'); +} \ No newline at end of file diff --git a/rom-patcher-js/RomPatcher.webapp.js b/rom-patcher-js/RomPatcher.webapp.js new file mode 100644 index 0000000..80e89be --- /dev/null +++ b/rom-patcher-js/RomPatcher.webapp.js @@ -0,0 +1,1762 @@ +/* +* Rom Patcher JS - Webapp implementation +* A web implementation for Rom Patcher JS +* 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. +*/ + + +/* + to-do list: + - load JS modules dynamically when RomPatchwerWeb is initialized + - allow multiple instances of RomPatcherWeb + - switch to ES6 classes and modules? +*/ + +const ROM_PATCHER_JS_PATH = './rom-patcher-js/'; + +var RomPatcherWeb = (function () { + const WEB_CRYPTO_AVAILABLE = window.crypto && window.crypto.subtle && window.crypto.subtle.digest; + const settings = { + language: typeof navigator.language === 'string' ? navigator.language.substring(0, 2) : 'en', + outputSuffix: true, + fixChecksum: false, + + allowDropFiles: false, + + onloadrom: null, + onvalidaterom: null, + onpatch: null + }; + var romFile, patch; + + + /* embeded patches */ + var embededPatchesInfo = null; + const _parseEmbededPatchInfo = function (embededPatchInfo) { + const parsedPatch = { + file: embededPatchInfo.file.trim(), + name: null, + description: null, + outputName: null, + outputExtension: null, + patches: null + }; + + if (typeof embededPatchInfo.name === 'string') { + parsedPatch.name = embededPatchInfo.name.trim(); + } else { + parsedPatch.name = embededPatchInfo.file.replace(/(.*?\/)+/g, ''); + } + + if (typeof embededPatchInfo.description === 'string') { + parsedPatch.description = embededPatchInfo.description; + } + + if (typeof embededPatchInfo.outputName === 'string') { + parsedPatch.outputName = embededPatchInfo.outputName; + } + if (typeof embededPatchInfo.outputExtension === 'string') { + parsedPatch.outputExtension = embededPatchInfo.outputExtension; + } + + if (typeof embededPatchInfo.inputCrc32 !== 'undefined') { + if (!Array.isArray(embededPatchInfo.inputCrc32)) + embededPatchInfo.inputCrc32 = [embededPatchInfo.inputCrc32]; + + const validCrcs = embededPatchInfo.inputCrc32.map(function (crc32) { + if (typeof crc32 === 'string' && /^(0x)?[0-9a-fA-F]{8}$/i.test(crc32.trim())) { + return parseInt(crc32.replace('0x', ''), 16); + } else if (typeof crc32 === 'number') { + return crc32 >>> 0; + } else { + return null; + } + }).filter(function (crc32) { + return typeof crc32 === 'number'; + }); + if (validCrcs.length) { + parsedPatch.inputCrc32 = validCrcs; + } else { + console.warn('Invalid inputCrc32 for embeded patch', embededPatchInfo); + } + } + + return parsedPatch; + } + const _fetchPatchFile = function (embededPatchInfo) { + htmlElements.disableAll(); + + const spinnerHtml = ''; + + + const loadingSpan = document.createElement('span'); + loadingSpan.id = 'rom-patcher-span-loading-embeded-patch'; + loadingSpan.innerHTML = _('Downloading...') + ' ' + spinnerHtml; + + const inputFilePatch = htmlElements.get('input-file-patch'); + if (inputFilePatch) { + inputFilePatch.parentElement.replaceChild(loadingSpan, inputFilePatch); + } else { + throw new Error('Rom Patcher JS: input#rom-patcher-input-file-patch[type=file] not found'); + } + + + + var uri = decodeURI(embededPatchInfo.file); + + fetch(uri) + .then(result => result.arrayBuffer()) // Gets the response and returns it as a blob + .then(arrayBuffer => { + const fetchedFile = new BinFile(arrayBuffer); + if (ZIPManager.isZipFile(fetchedFile)) { + if (typeof embededPatchInfo.patches === 'object') { + if (Array.isArray(embededPatchInfo.patches)) { + embededPatchesInfo = embededPatchInfo.patches.map((embededPatchInfo) => _parseEmbededPatchInfo(embededPatchInfo)); + } else { + console.warn('Rom Patcher JS: Invalid patches object for embeded patch', embededPatchInfo); + } + } else { + embededPatchesInfo = [_parseEmbededPatchInfo(embededPatchInfo)]; + } + loadingSpan.innerHTML = _('Unzipping...') + ' ' + spinnerHtml; + ZIPManager.unzipEmbededPatches(arrayBuffer, embededPatchesInfo); + } else { + embededPatchesInfo = [_parseEmbededPatchInfo(embededPatchInfo)]; + loadingSpan.innerHTML = embededPatchesInfo[0].name; + _setPatchInputSpinner(false); + fetchedFile.fileName = embededPatchInfo.file; + RomPatcherWeb.providePatchFile(fetchedFile); + } + }) + .catch(function (evt) { + _setToastError((_('Error downloading %s') + '
      ' + evt.message).replace('%s', embededPatchInfo.file.replace(/^.*[\/\\]/g, ''))); + }); + }; + const _getEmbededPatchInfo = function (fileName) { + if (embededPatchesInfo) + return embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === fileName); + return null; + } + + + + const _padZeroes = function (intVal, nBytes) { + var hexString = intVal.toString(16); + while (hexString.length < nBytes * 2) + hexString = '0' + hexString; + return hexString + } + + const _setElementsStatus = function (status) { + htmlElements.setEnabled('input-file-rom', status); + htmlElements.setEnabled('input-file-patch', status); + htmlElements.setEnabled('select-file-patch', status); + htmlElements.setEnabled('checkbox-alter-header', status); + + if (romFile && status && (patch /* || embededPatchesInfo */)) { + htmlElements.setEnabled('button-apply', status); + } else { + htmlElements.setEnabled('button-apply', false); + } + }; + + const _setInputFileSpinner = function (inputFileId, status) { + const elementId = embededPatchesInfo && inputFileId === 'patch' ? ('select-file-' + inputFileId) : ('input-file-' + inputFileId); + const spinnerId = 'spinner-' + inputFileId; + + htmlElements.removeClass(elementId, 'empty'); + + + if (status) { + const spinner = document.createElement('spinner'); + spinner.id = 'rom-patcher-' + spinnerId; + spinner.className = 'rom-patcher-spinner'; + + const htmlInputFile = htmlElements.get(elementId); + if (htmlInputFile) + htmlInputFile.parentElement.appendChild(spinner); + + htmlElements.addClass(elementId, 'loading'); + htmlElements.removeClass(elementId, 'valid'); + htmlElements.removeClass(elementId, 'invalid'); + + return spinner; + } else { + const spinner = htmlElements.get(spinnerId); + if (spinner) + spinner.parentElement.removeChild(spinner); + htmlElements.removeClass(elementId, 'loading'); + + return spinner; + } + } + const _setRomInputSpinner = function (status) { + return _setInputFileSpinner('rom', status); + } + const _setPatchInputSpinner = function (status) { + return _setInputFileSpinner('patch', status); + } + const _setApplyButtonSpinner = function (status) { + if (status) { + htmlElements.setText('button-apply', ' ' + _('Applying patch...')); + } else { + htmlElements.setText('button-apply', _('Apply patch')); + } + } + const _setToastError = function (errorMessage, className) { + const row = htmlElements.get('row-error-message'); + const span = htmlElements.get('error-message'); + if (row && span) { + if (errorMessage) { + htmlElements.addClass('row-error-message', 'show'); + htmlElements.setText('error-message', errorMessage); + if (className === 'warning') + htmlElements.addClass('error-message', 'warning'); + else + htmlElements.removeClass('error-message', 'warning'); + } else { + htmlElements.removeClass('row-error-message', 'show'); + htmlElements.setText('error-message', ''); + + } + } else { + console.error('Rom Patcher JS: ' + errorMessage); + } + } + + const htmlElements = { + get: function (id) { + return document.getElementById('rom-patcher-' + id); + }, + + enableAll: function () { + _setElementsStatus(true); + }, + disableAll: function () { + _setElementsStatus(false); + }, + + show: function (id) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).style.display = 'block'; + }, + hide: function (id) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).style.display = 'none'; + }, + + setValue: function (id, val) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).value = val; + }, + getValue: function (id, val, fallback) { + if (document.getElementById('rom-patcher-' + id)) + return document.getElementById('rom-patcher-' + id).value; + return fallback || 0; + }, + setFakeFile: function (id, fileName) { + if (document.getElementById('rom-patcher-' + id)) { + try { + /* add a fake file to the input file, so it shows the chosen file name */ + const fakeFile = new File(new Uint8Array(0), fileName); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(fakeFile); + document.getElementById('rom-patcher-' + id).files = dataTransfer.files; + } catch (ex) { + console.warning('File API constructor is not supported'); + } + } + }, + + setText: function (id, text) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).innerHTML = text; + }, + + addClass: function (id, className) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).classList.add(className); + }, + removeClass: function (id, className) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).classList.remove(className); + }, + setClass: function (id, className) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).className = className; + }, + + setEnabled: function (id, enabled) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).disabled = !enabled; + }, + setChecked: function (id, checked) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).checked = !!checked; + }, + isChecked: function (id) { + if (document.getElementById('rom-patcher-' + id)) + return document.getElementById('rom-patcher-' + id).checked; + return false; + }, + + setSpinner: function (inputFileId, status) { + if (inputFileId !== 'rom' && inputFileId !== 'patch') + throw new Error('RomPatcherWeb.htmlElements.setSpinner: only rom or patch input file ids are allowed'); + + return _setInputFileSpinner(inputFileId, status); + } + } + + + + /* web workers */ + const webWorkerApply = new Worker(ROM_PATCHER_JS_PATH + 'RomPatcher.webworker.apply.js'); + webWorkerApply.onmessage = event => { // listen for events from the worker + //retrieve arraybuffers back from webworker + romFile._u8array = event.data.romFileU8Array; + patch._originalPatchFile._u8array = event.data.patchFileU8Array; + + htmlElements.enableAll(); + _setApplyButtonSpinner(false); + if (event.data.patchedRomU8Array && !event.data.errorMessage) { + const patchedRom = new BinFile(event.data.patchedRomU8Array.buffer); + patchedRom.fileName = event.data.patchedRomFileName; + if (typeof settings.onpatch === 'function') + settings.onpatch(patchedRom); + patchedRom.save(); + _setToastError(); + } else { + _setToastError(event.data.errorMessage); + } + }; + webWorkerApply.onerror = event => { // listen for exceptions from the worker + htmlElements.enableAll(); + _setApplyButtonSpinner(false); + _setToastError('webWorkerApply error: ' + event.message); + }; + + const webWorkerCrc = new Worker(ROM_PATCHER_JS_PATH + 'RomPatcher.webworker.crc.js'); + webWorkerCrc.onmessage = event => { // listen for events from the worker + //console.log('received_crc'); + htmlElements.setText('span-crc32', _padZeroes(event.data.crc32, 4)); + htmlElements.setText('span-md5', _padZeroes(event.data.md5, 16)); + romFile._u8array = event.data.u8array; + + if (WEB_CRYPTO_AVAILABLE) { + romFile.hashSHA1().then(function (res) { + htmlElements.setText('span-sha1', res); + }); + } + + if (event.data.rom) { + htmlElements.setText('span-rom-info', event.data.rom); + htmlElements.addClass('row-info-rom', 'show'); + } + + RomPatcherWeb.validateCurrentRom(event.data.checksumStartOffset); + htmlElements.enableAll(); + }; + webWorkerCrc.onerror = event => { // listen for events from the worker + _setToastError('webWorkerCrc error: ' + event.message); + }; + /* zip-js web worker */ + zip.useWebWorkers = true; + zip.workerScriptsPath = ROM_PATCHER_JS_PATH + 'modules/zip.js/'; + + + const _getChecksumStartOffset = function () { + if (romFile) { + const headerInfo = RomPatcher.isRomHeadered(romFile); + if (headerInfo) { + const htmlCheckboxAlterHeader = htmlElements.get('checkbox-alter-header'); + if (htmlCheckboxAlterHeader && htmlCheckboxAlterHeader.checked) + return headerInfo.size; + } + } + return 0; + } + + + + /* localization */ + const _ = function (str) { return ROM_PATCHER_LOCALE[settings.language] && ROM_PATCHER_LOCALE[settings.language][str] ? ROM_PATCHER_LOCALE[settings.language][str] : str }; + + + var initialized = false; + return { + _: function (str) { /* public localization function for external usage purposes */ + return _(str); + }, + getHtmlElements: function () { + return htmlElements; + }, + + isInitialized: function () { + return initialized; + }, + getEmbededPatches: function () { + return embededPatchesInfo; + }, + + provideRomFile: function (binFile, transferFakeFile) { + htmlElements.disableAll(); + + + romFile = binFile; + + + + const canRomGetHeader = RomPatcher.canRomGetHeader(romFile); + const isRomHeadered = RomPatcher.isRomHeadered(romFile); + RomPatcherWeb.getHtmlElements().setChecked('checkbox-alter-header', false); + if (canRomGetHeader) { + RomPatcherWeb.getHtmlElements().setText('span-alter-header', _('Add %s header').replace('%s', _(canRomGetHeader.name))); + RomPatcherWeb.getHtmlElements().addClass('row-alter-header', 'show'); + } else if (isRomHeadered) { + RomPatcherWeb.getHtmlElements().setText('span-alter-header', _('Remove %s header').replace('%s', _(isRomHeadered.name))); + RomPatcherWeb.getHtmlElements().addClass('row-alter-header', 'show'); + } else { + RomPatcherWeb.getHtmlElements().setText('span-alter-header', ''); + RomPatcherWeb.getHtmlElements().removeClass('row-alter-header', 'show'); + } + + + romFile.seek(0); + + + _setRomInputSpinner(false); + if (ZIPManager.isZipFile(romFile)) { + ZIPManager.unzipRoms(romFile._u8array.buffer); + } else { + if (typeof settings.onloadrom === 'function') + settings.onloadrom(romFile); + RomPatcherWeb.calculateCurrentRomChecksums(); + } + + if (transferFakeFile) { + htmlElements.setFakeFile('input-file-rom', romFile.fileName); + } + }, + + providePatchFile: function (binFile, transferFakeFile) { + htmlElements.disableAll(); + + patch = null; + if (binFile) { + if (ZIPManager.isZipFile(binFile)) { + ZIPManager.unzipPatches(binFile._u8array.buffer); + } else { + const parsedPatch = RomPatcher.parsePatchFile(binFile); + if (parsedPatch) { + patch = parsedPatch; + _setPatchInputSpinner(false); + + const embededPatchInfo = _getEmbededPatchInfo(binFile.fileName); + if (embededPatchInfo) { + /* custom crc32s validation */ + if (embededPatchInfo.inputCrc32) { + patch.validateSource = function (romFile, headerSize) { + for (var i = 0; i < embededPatchInfo.inputCrc32.length; i++) { + if (embededPatchInfo.inputCrc32[i] === romFile.hashCRC32(headerSize)) + return true; + } + return false; + } + patch.getValidationInfo = function () { + return { + 'type': 'CRC32', + 'value': embededPatchInfo.inputCrc32 + } + }; + } + + /* custom description */ + if (embededPatchInfo.description) { + patch.getDescription = function () { + return embededPatchInfo.description; + } + } + } + + /* toggle ROM requirements */ + if (htmlElements.get('row-patch-requirements') && htmlElements.get('patch-requirements-value')) { + if (typeof patch.getValidationInfo === 'function' && patch.getValidationInfo()) { + var validationInfo = patch.getValidationInfo(); + if (Array.isArray(validationInfo) || !validationInfo.type) { + validationInfo = { + type: 'ROM', + value: validationInfo + } + } + htmlElements.setText('patch-requirements-value', ''); + + htmlElements.setText('patch-requirements-type', validationInfo.type === 'ROM' ? _('Required ROM:') : _('Required %s:').replace('%s', validationInfo.type)); + + if (!Array.isArray(validationInfo.value)) + validationInfo.value = [validationInfo.value]; + + validationInfo.value.forEach(function (value) { + var line = document.createElement('div'); + if (typeof value !== 'string') { + if (validationInfo.type === 'CRC32') { + value = value.toString(16); + while (value.length < 8) + value = '0' + value; + } else { + value = value.toString(); + } + } + /* + var a=document.createElement('a'); + a.href='https://www.google.com/search?q=%22'+value+'%22'; + a.target='_blank'; + a.className='clickable'; + a.innerHTML=value; + line.appendChild(a); + */ + line.innerHTML = value; + htmlElements.get('patch-requirements-value').appendChild(line); + }); + htmlElements.addClass('row-patch-requirements', 'show'); + } else { + htmlElements.setText('patch-requirements-value', ''); + htmlElements.removeClass('row-patch-requirements', 'show'); + } + } + + /* toggle patch description */ + if (typeof patch.getDescription === 'function' && patch.getDescription()) { + htmlElements.setText('patch-description', patch.getDescription()/* .replace(/\n/g, '
      ') */); + //htmlElements.setTitle('patch-description', patch.getDescription()); + htmlElements.addClass('row-patch-description', 'show'); + } else { + htmlElements.setText('patch-description', ''); + //htmlElements.setTitle('patch-description', ''); + htmlElements.removeClass('row-patch-description', 'show'); + } + + RomPatcherWeb.validateCurrentRom(_getChecksumStartOffset()); + + if (transferFakeFile) { + htmlElements.setFakeFile('input-file-patch', binFile.fileName); + } + }else{ + _setToastError(_('Invalid patch file')); + } + } + } + + if (patch) { + htmlElements.removeClass('input-file-patch', 'invalid'); + } else { + htmlElements.addClass('input-file-patch', 'invalid'); + } + htmlElements.enableAll(); + }, + + refreshRomFileName: function () { + if (romFile) + htmlElements.setFakeFile('input-file-rom', romFile.fileName); + }, + + pickEmbededFile: function (fileName) { + if (typeof fileName !== 'string') + throw new Error('Invalid embeded patch file name'); + + const selectFilePatch = htmlElements.get('select-file-patch'); + if (selectFilePatch) { + const embededPatchInfo = _getEmbededPatchInfo(fileName); + if (embededPatchInfo && typeof embededPatchInfo.selectIndex === 'number' && selectFilePatch.value != embededPatchInfo.selectIndex) { + selectFilePatch.value = embededPatchInfo.selectIndex; + + /* create and dispatch change event */ + const evt = new Event('change'); + selectFilePatch.dispatchEvent(evt); + } + } else { + console.warn('RomPatcherWeb.pickEmbededFile: select-file-patch not found'); + } + }, + + initialize: function (newSettings, embededPatchInfo) { + if (initialized) + throw new Error('Rom Patcher JS was already initialized'); + + /* embeded patches */ + var validEmbededPatch = false; + if (embededPatchInfo) { + if (typeof embededPatchInfo === 'string') + embededPatchInfo = { file: embededPatchInfo }; + + if (typeof embededPatchInfo === 'object') { + if (typeof embededPatchInfo.file === 'string') { + validEmbededPatch = true; + } else { + throw new Error('Rom Patcher JS: invalid embeded patch file'); + } + } + } + + /* check incompatible browsers */ + if ( + typeof window === 'undefined' || + typeof Worker !== 'function' || + typeof Array.isArray !== 'function' || + typeof window.addEventListener !== 'function' + // !document.createElement('div').classList instanceof DOMTokenList + ) + throw new Error('Rom Patcher JS: incompatible browser'); + + + + /* check if all required HTML elements are in DOM */ + const htmlInputFileRom = htmlElements.get('input-file-rom'); + if (htmlInputFileRom && htmlInputFileRom.tagName === 'INPUT' && htmlInputFileRom.type === 'file') { + htmlInputFileRom.addEventListener('change', function (evt) { + htmlElements.disableAll(); + new BinFile(this, RomPatcherWeb.provideRomFile); + }); + } else { + throw new Error('Rom Patcher JS: input#rom-patcher-input-file-rom[type=file] not found'); + } + const htmlInputFilePatch = htmlElements.get('input-file-patch'); + if (htmlInputFilePatch && htmlInputFilePatch.tagName === 'INPUT' && htmlInputFilePatch.type === 'file') { + htmlInputFilePatch.addEventListener('change', function (evt) { + htmlElements.disableAll(); + new BinFile(this, RomPatcherWeb.providePatchFile); + }); + } else { + throw new Error('Rom Patcher JS: input#rom-patcher-input-file-patch[type=file] not found'); + } + const htmlButtonApply = htmlElements.get('button-apply'); + if (htmlButtonApply && htmlButtonApply.tagName === 'BUTTON') { + htmlButtonApply.addEventListener('click', RomPatcherWeb.applyPatch); + } else { + throw new Error('Rom Patcher JS: button#rom-patcher-button-apply not found'); + } + const htmlCheckboxAlterHeader = htmlElements.get('checkbox-alter-header'); + if (htmlCheckboxAlterHeader && htmlCheckboxAlterHeader.tagName === 'INPUT' && htmlCheckboxAlterHeader.type === 'checkbox') { + htmlCheckboxAlterHeader.addEventListener('change', function (evt) { + if (!romFile) + return false; + + const headerInfo = RomPatcher.isRomHeadered(romFile); + if (headerInfo) { + htmlElements.disableAll(); + webWorkerCrc.postMessage({ u8array: romFile._u8array, fileName: romFile.fileName, checksumStartOffset: _getChecksumStartOffset() }, [romFile._u8array.buffer]); + } + }); + } + /* set all default input status just in case HTML is wrong */ + htmlElements.setEnabled('button-apply', false); + /* reset input files */ + htmlElements.setValue('input-file-rom', ''); + htmlElements.setValue('input-file-patch', ''); + + + /* translatable elements */ + const translatableElements = document.querySelectorAll('*[data-localize="yes"]'); + for (var i = 0; i < translatableElements.length; i++) { + translatableElements[i].setAttribute('data-localize', translatableElements[i].innerHTML); + } + + /* add drag and drop events */ + if (newSettings && newSettings.allowDropFiles) { + window.addEventListener('dragover', function (evt) { + if (_dragEventContainsFiles(evt)) + evt.preventDefault(); /* needed ! */ + }); + window.addEventListener('drop', function (evt) { + evt.preventDefault(); + if (_dragEventContainsFiles(evt)) { + const droppedFiles = evt.dataTransfer.files; + if (droppedFiles && droppedFiles.length === 1) { + new BinFile(droppedFiles[0], function (binFile) { + if (RomPatcherWeb.getEmbededPatches()) { + RomPatcherWeb.provideRomFile(binFile, true); + } else if (ZIPManager.isZipFile(binFile)) { + ZIPManager.unzipAny(binFile._u8array.buffer); + } else if (RomPatcher.parsePatchFile(binFile)) { + RomPatcherWeb.providePatchFile(binFile, null, true); + } else { + RomPatcherWeb.provideRomFile(binFile, true); + } + }); + } + } + }); + htmlInputFileRom.addEventListener('drop', function (evt) { + evt.stopPropagation(); + }); + htmlInputFilePatch.addEventListener('drop', function (evt) { + evt.stopPropagation(); + }); + } + + console.log('Rom Patcher JS initialized'); + initialized = true; + + + /* initialize Rom Patcher */ + RomPatcherWeb.setSettings(newSettings); + + /* download embeded patch */ + if (validEmbededPatch) + _fetchPatchFile(embededPatchInfo); + }, + + applyPatch: function () { + if (romFile && patch) { + const romPatcherOptions = { + requireValidation: false, + removeHeader: RomPatcher.isRomHeadered(romFile) && htmlElements.isChecked('checkbox-alter-header'), + addHeader: RomPatcher.canRomGetHeader(romFile) && htmlElements.isChecked('checkbox-alter-header'), + fixChecksum: settings.fixChecksum, + outputSuffix: settings.outputSuffix + }; + + htmlElements.disableAll(); + _setApplyButtonSpinner(true); + + const embededPatchInfo = _getEmbededPatchInfo(patch._originalPatchFile.fileName); + webWorkerApply.postMessage( + { + romFileU8Array: romFile._u8array, + patchFileU8Array: patch._originalPatchFile._u8array, + romFileName: romFile.fileName, + patchFileName: patch._originalPatchFile.fileName, + patchExtraInfo: embededPatchInfo, + //romFileType:romFile.fileType, + options: romPatcherOptions + }, + [ + romFile._u8array.buffer, + patch._originalPatchFile._u8array.buffer + ] + ); + } else if (!romFile) { + _setToastError(_('No ROM provided')); + } else if (!patch) { + _setToastError(_('No patch file provided')); + } + }, + + calculateCurrentRomChecksums: function (force) { + if (romFile.fileSize > 67108864 && !force) { + htmlElements.setText('span-crc32', _('File is too big.') + ' ' + _('Force calculate checksum') + ''); + htmlElements.setText('span-md5', ''); + htmlElements.setText('span-sha1', ''); + htmlElements.enableAll(); + return false; + } + + htmlElements.setText('span-crc32', _('Calculating...')); + htmlElements.setText('span-md5', _('Calculating...')); + if (WEB_CRYPTO_AVAILABLE) + htmlElements.setText('span-sha1', _('Calculating...')); + + htmlElements.setText('span-rom-info', ''); + htmlElements.removeClass('row-info-rom', 'show'); + + htmlElements.disableAll(); + webWorkerCrc.postMessage({ u8array: romFile._u8array, fileName: romFile.fileName, checksumStartOffset: _getChecksumStartOffset() }, [romFile._u8array.buffer]); + }, + + validateCurrentRom: function (checksumStartOffset) { + if (romFile && patch && typeof patch.validateSource === 'function') { + const validRom = RomPatcher.validateRom(romFile, patch, checksumStartOffset ?? 0); + if (validRom) { + htmlElements.addClass('input-file-rom', 'valid'); + htmlElements.removeClass('input-file-rom', 'invalid'); + _setToastError(); + } else { + htmlElements.addClass('input-file-rom', 'invalid'); + htmlElements.removeClass('input-file-rom', 'valid'); + _setToastError(_('Source ROM checksum mismatch')); + } + + if (typeof settings.onvalidaterom === 'function') + settings.onvalidaterom(romFile, validRom); + } else { + htmlElements.removeClass('input-file-rom', 'valid'); + htmlElements.removeClass('input-file-rom', 'invalid'); + _setToastError(); + if (romFile && patch && typeof settings.onvalidaterom === 'function') + settings.onvalidaterom(romFile, true); + } + }, + + enable: function () { + htmlElements.enableAll(); + }, + disable: function () { + htmlElements.disableAll(); + }, + setErrorMessage: function (message, className) { + _setToastError(message, className); + }, + translateUI: function (newLanguage) { + if (typeof newLanguage === 'object' && typeof newLanguage.language === 'string') + newLanguage = newLanguage.language; + if (typeof newLanguage === 'string') + settings.language = newLanguage; + + const translatableElements = document.querySelectorAll('*[data-localize]'); + for (var i = 0; i < translatableElements.length; i++) { + translatableElements[i].innerHTML = _(translatableElements[i].getAttribute('data-localize')); + } + }, + + getCurrentLanguage: function () { + return settings.language; + }, + setSettings: function (newSettings) { + if (typeof newSettings === 'object') { + if (typeof newSettings.language === 'string') { + settings.language = newSettings.language; + RomPatcherWeb.translateUI(); + } + + if (typeof newSettings.outputSuffix === 'boolean') { + settings.outputSuffix = newSettings.outputSuffix; + } + if (typeof newSettings.fixChecksum === 'boolean') { + settings.fixChecksum = newSettings.fixChecksum; + } + + if (typeof newSettings.onloadrom === 'function') + settings.onloadrom = newSettings.onloadrom; + else if (typeof newSettings.onloadrom !== 'undefined') + settings.onloadrom = null; + + if (typeof newSettings.onvalidaterom === 'function') + settings.onvalidaterom = newSettings.onvalidaterom; + else if (typeof newSettings.onvalidaterom !== 'undefined') + settings.onvalidaterom = null; + + if (typeof newSettings.onpatch === 'function') + settings.onpatch = newSettings.onpatch; + else if (typeof newSettings.onpatch !== 'undefined') + settings.onpatch = null; + } + } + } +}()); + + + + +/* ZIP manager */ +const ZIPManager = (function (romPatcherWeb) { + const _ = romPatcherWeb._; + const htmlElements = romPatcherWeb.getHtmlElements(); + + const _setRomInputSpinner = function (status) { + htmlElements.setSpinner('rom', status); + }; + const _setPatchInputSpinner = function (status) { + htmlElements.setSpinner('patch', status); + + }; + + const ZIP_MAGIC = '\x50\x4b\x03\x04'; + + const FILTER_PATCHES = /\.(ips|ups|bps|aps|rup|ppf|mod|xdelta|vcdiff)$/i; + //const FILTER_ROMS=/(? 1) { + _showFilePicker(filteredEntries, RomPatcherWeb.provideRomFile); + RomPatcherWeb.enable(); + } else { + /* no possible patchable files found in zip, treat zip file as ROM file */ + RomPatcherWeb.calculateCurrentRomChecksums(); + } + }); + }, + /* failed */ + _unzipError + ); + }, + + unzipPatches: function (arrayBuffer) { + zip.createReader( + new zip.BlobReader(new Blob([arrayBuffer])), + /* success */ + function (zipReader) { + zipReader.getEntries(function (zipEntries) { + const filteredEntries = _filterEntriesPatches(zipEntries); + + if (filteredEntries.length === 1) { + _unzipEntry(filteredEntries[0], RomPatcherWeb.providePatchFile); + } else if (filteredEntries.length > 1) { + _showFilePicker(filteredEntries, RomPatcherWeb.providePatchFile); + } else { + RomPatcherWeb.providePatchFile(null); + } + + }); + }, + /* failed */ + _unzipError + ); + }, + + unzipAny: function (arrayBuffer) { + zip.createReader( + new zip.BlobReader(new Blob([arrayBuffer])), + /* success */ + function (zipReader) { + zipReader.getEntries(function (zipEntries) { + const filteredEntriesRoms = _filterEntriesRoms(zipEntries); + const filteredEntriesPatches = _filterEntriesPatches(zipEntries); + + if (filteredEntriesRoms.length && filteredEntriesPatches.length === 0) { + if (filteredEntriesRoms.length === 1) { + _unzipEntry(filteredEntriesRoms[0], RomPatcherWeb.provideRomFile); + } else { + _showFilePicker(filteredEntriesRoms, RomPatcherWeb.provideRomFile); + RomPatcherWeb.enable(); + } + } else if (filteredEntriesPatches.length && filteredEntriesRoms.length === 0) { + if (filteredEntriesPatches.length === 1) { + _unzipEntry(filteredEntriesPatches[0], RomPatcherWeb.providePatchFile); + } else { + _showFilePicker(filteredEntriesPatches, RomPatcherWeb.providePatchFile); + } + } else { + console.warn('ZIPManager.unzipAny: zip file contains both ROMs and patches, cannot guess'); + } + }); + }, + /* failed */ + _unzipError + ); + }, + + unzipEmbededPatches: function (arrayBuffer, embededPatchesInfo) { + zip.createReader( + new zip.BlobReader(new Blob([arrayBuffer])), + /* success */ + function (zipReader) { + zipReader.getEntries(function (zipEntries) { + const filteredEntries = _filterEntriesPatches(zipEntries); + + if (filteredEntries.length) { + if (filteredEntries.length === 1) { + if (embededPatchesInfo) { + embededPatchesInfo[0].file = filteredEntries[0].filename; + htmlElements.setText('span-loading-embeded-patch', embededPatchesInfo[0].file); + } else { + htmlElements.setText('span-loading-embeded-patch', filteredEntries[0].filename); + } + } else { + var select = document.createElement('select'); + select.id = 'rom-patcher-select-file-patch'; + + const spanLoadingEmbededPatch = htmlElements.get('span-loading-embeded-patch'); + if (spanLoadingEmbededPatch) { + spanLoadingEmbededPatch.parentElement.replaceChild(select, spanLoadingEmbededPatch); + + for (var i = 0; i < filteredEntries.length; i++) { + const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === filteredEntries[i].filename); + var option = document.createElement('option'); + option.innerHTML = embededPatchInfo ? embededPatchInfo.name : filteredEntries[i].filename; + option.value = i; + select.appendChild(option); + if (embededPatchInfo) { + embededPatchInfo.selectIndex = i; + } + } + + select.addEventListener('change', function (evt) { + const fileIndex = parseInt(this.value); + _unzipEntry(filteredEntries[fileIndex], RomPatcherWeb.providePatchFile); + }); + } else { + throw new Error('rom-patcher-select-file-patch not found'); + } + } + + //_setPatchInputSpinner(false); + _unzipEntry(filteredEntries[0], RomPatcherWeb.providePatchFile); + } else { + RomPatcherWeb.setErrorMessage(_('No valid patches found in ZIP'), 'error'); + RomPatcherWeb.disable(); + } + }); + }, + /* failed */ + _unzipError + ); + } + } +})(RomPatcherWeb); + + + + + +/* Patch Builder */ +const PatchBuilderWeb = (function (romPatcherWeb) { + var originalRom, modifiedRom; + + /* localization */ + const _ = function (str) { + const language = romPatcherWeb.getCurrentLanguage(); + return ROM_PATCHER_LOCALE[language] && ROM_PATCHER_LOCALE[language][str] ? ROM_PATCHER_LOCALE[language][str] : str + }; + + const _setCreateButtonSpinner = function (status) { + if (status) { + document.getElementById('patch-builder-button-create').innerHTML = ' ' + _('Creating patch...'); + } else { + document.getElementById('patch-builder-button-create').innerHTML = _('Create patch'); + } + } + + const _setToastError = function (errorMessage, className) { + const row = document.getElementById('patch-builder-row-error-message'); + const span = document.getElementById('patch-builder-error-message'); + + if (row && span) { + if (errorMessage) { + row.classList.add('show'); + span.innerHTML = errorMessage; + } else { + row.classList.remove('show'); + span.innerHTML = ''; + } + if (className === 'warning') + span.classList.add('warning'); + else + span.classList.remove('warning'); + } else { + if (className === 'warning') + console.warn('Patch Builder JS: ' + errorMessage); + else + console.error('Patch Builder JS: ' + errorMessage); + } + } + + const _setElementsStatus = function (status) { + document.getElementById('patch-builder-input-file-original').disabled = !status; + document.getElementById('patch-builder-input-file-modified').disabled = !status; + document.getElementById('patch-builder-select-patch-type').disabled = !status; + if (originalRom && modifiedRom && status) { + document.getElementById('patch-builder-button-create').disabled = !status; + } else { + document.getElementById('patch-builder-button-create').disabled = true + } + }; + + const webWorkerCreate = new Worker(ROM_PATCHER_JS_PATH + 'RomPatcher.webworker.create.js'); + webWorkerCreate.onmessage = event => { // listen for events from the worker + //retrieve arraybuffers back from webworker + originalRom._u8array = event.data.originalRomU8Array; + modifiedRom._u8array = event.data.modifiedRomU8Array; + + _setElementsStatus(true); + _setCreateButtonSpinner(false); + + const patchFile = new BinFile(event.data.patchFileU8Array.buffer); + patchFile.fileName = modifiedRom.getName() + '.' + document.getElementById('patch-builder-select-patch-type').value; + patchFile.save(); + + _setToastError(); + }; + webWorkerCreate.onerror = event => { // listen for events from the worker + _setElementsStatus(true); + _setCreateButtonSpinner(false); + _setToastError('webWorkerCreate error: ' + event.message); + }; + + var initialized = false; + return{ + isInitialized: function () { + return initialized; + }, + + initialize: function () { + if (initialized) + throw new Error('Patch Builder JS was already initialized'); + + document.getElementById('patch-builder-button-create').disabled = true; + + document.getElementById('patch-builder-input-file-original').addEventListener('change', function () { + _setElementsStatus(false); + this.classList.remove('empty'); + originalRom = new BinFile(this.files[0], function (evt) { + _setElementsStatus(true); + + if (RomPatcher.isRomTooBig(originalRom)) + _setToastError(_('Using big files is not recommended'), 'warning'); + }); + }); + document.getElementById('patch-builder-input-file-modified').addEventListener('change', function () { + _setElementsStatus(false); + this.classList.remove('empty'); + modifiedRom = new BinFile(this.files[0], function (evt) { + _setElementsStatus(true); + + if (RomPatcher.isRomTooBig(modifiedRom)) + _setToastError(_('Using big files is not recommended'), 'warning'); + }); + }); + document.getElementById('patch-builder-button-create').addEventListener('click', function () { + _setElementsStatus(false); + _setCreateButtonSpinner(true); + webWorkerCreate.postMessage( + { + originalRomU8Array: originalRom._u8array, + modifiedRomU8Array: modifiedRom._u8array, + format: document.getElementById('patch-builder-select-patch-type').value + }, [ + originalRom._u8array.buffer, + modifiedRom._u8array.buffer + ] + ); + }); + + console.log('Patch Builder JS initialized'); + initialized = true; + } + } +}(RomPatcherWeb)); + + + + + + + + + + + + + + + + + + + + +const ROM_PATCHER_LOCALE = { + 'fr': { + 'Creator mode': 'Mode créateur', + 'Settings': 'Configurations', + 'Use patch name for output': 'Utiliser le nom du patch pour renommer la ROM une fois patchée', + 'Light theme': 'Thème Clair', + + 'Apply patch': 'Appliquer le patch', + 'ROM file:': 'Fichier ROM:', + 'Patch file:': 'Fichier patch:', + 'Remove %s header': 'Supprimer l\'en-tête %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Formats compatibles:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Application du patch...', + 'Downloading...': 'Téléchargement...', + 'Unzipping...': 'Décompresser...', + + 'Create patch': 'Créer le patch', + 'Original ROM:': 'ROM originale:', + 'Modified ROM:': 'ROM modifiée:', + 'Patch type:': 'Type de patch:', + 'Creating patch...': 'Création du patch...', + + 'Source ROM checksum mismatch': 'Non-concordance de la somme de contrôle de la ROM source', + 'Target ROM checksum mismatch': 'Non-concordance de la somme de contrôle de la ROM cible', + 'Patch checksum mismatch': 'Non-concordance de la somme de contrôle du patch', + 'Error downloading %s': 'Erreur lors du téléchargement du patch', + 'Error unzipping file': 'Erreur lors de la décompression du fichier', + 'Invalid patch file': 'Fichier patch invalide', + 'Using big files is not recommended': 'L\'utilisation de gros fichiers n\'est pas recommandée' + }, + 'de': { + 'Creator mode': 'Erstellmodus', + 'Settings': 'Einstellungen', + 'Use patch name for output': 'Output ist Name vom Patch', + 'Fix ROM header checksum': 'Prüfsumme im ROM Header korrigieren', + 'Light theme': 'Helles Design', + + 'Apply patch': 'Patch anwenden', + 'ROM file:': 'ROM-Datei:', + 'Patch file:': 'Patch-Datei:', + 'Remove %s header': 'Header entfernen %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Unterstützte Formate:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Patch wird angewandt...', + 'Downloading...': 'Herunterladen...', + 'Unzipping...': 'Entpacken...', + + 'Create patch': 'Patch erstellen', + 'Original ROM:': 'Originale ROM:', + 'Modified ROM:': 'Veränderte ROM:', + 'Patch type:': 'Patch-Format:', + 'Creating patch...': 'Patch wird erstellt...', + + 'Source ROM checksum mismatch': 'Prüfsumme der Input-ROM stimmt nicht überein', + 'Target ROM checksum mismatch': 'Prüfsumme der Output-ROM stimmt nicht überein', + 'Patch checksum mismatch': 'Prüfsumme vom Patch stimmt nicht überein', + 'Error downloading %s': 'Fehler beim Herunterladen vom %s', + 'Error unzipping file': 'Fehler beim Entpacken', + 'Invalid patch file': 'Ungültiger Patch', + 'Using big files is not recommended': 'Große Dateien zu verwenden ist nicht empfohlen' + }, + 'es': { + 'Creator mode': 'Modo creador', + 'Settings': 'Configuración', + 'Use patch name for output': 'Guardar con nombre del parche', + 'Fix ROM header checksum': 'Corregir checksum cabecera ROM', + 'Light theme': 'Tema claro', + + 'Apply patch': 'Aplicar parche', + 'ROM file:': 'Archivo ROM:', + 'Patch file:': 'Archivo parche:', + 'Remove %s header': 'Quitar cabecera %s', + 'Add %s header': 'Añadir cabecera %s', + 'Compatible formats:': 'Formatos compatibles:', + 'Description:': 'Descripción:', + 'Required ROM:': 'ROM requerida:', + 'Required %s:': '%s requerido:', + 'Applying patch...': 'Aplicando parche...', + 'Downloading...': 'Descargando...', + 'Unzipping...': 'Descomprimiendo...', + 'Calculating...': 'Calculando...', + 'Force calculate checksum': 'Forzar cálculo de checksum', + + 'Create patch': 'Crear parche', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipo de parche:', + 'Creating patch...': 'Creando parche...', + + 'Source ROM checksum mismatch': 'Checksum de ROM original no válida', + 'Target ROM checksum mismatch': 'Checksum de ROM creada no válida', + 'Patch checksum mismatch': 'Checksum de parche no válida', + 'Error downloading %s': 'Error descargando %s', + 'Error unzipping file': 'Error descomprimiendo archivo', + 'Invalid patch file': 'Archivo de parche no válido', + 'Using big files is not recommended': 'No es recomendable usar archivos muy grandes', + + 'SNES copier': 'copión SNES' + }, + 'it': { + 'Creator mode': 'Modalità creatore', + 'Settings': 'Impostazioni', + 'Use patch name for output': 'Usa il nome della patch per uscita', + 'Light theme': 'Tema chiaro', + + 'Apply patch': 'Applica patch', + 'ROM file:': 'File ROM:', + 'Patch file:': 'File patch:', + 'Remove %s header': 'Rimuovi header %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Formati:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Applica patch...', + 'Downloading...': 'Scaricamento...', + 'Unzipping...': 'Estrazione...', + + 'Create patch': 'Crea patch', + 'Original ROM:': 'ROM originale:', + 'Modified ROM:': 'ROM modificata:', + 'Patch type:': 'Tipologia patch:', + 'Creating patch...': 'Creazione patch...', + + 'Source ROM checksum mismatch': 'Checksum della ROM sorgente non valido', + 'Target ROM checksum mismatch': 'Checksum della ROM destinataria non valido', + 'Patch checksum mismatch': 'Checksum della patch non valido', + 'Error downloading %s': 'Errore di scaricamento %s', + 'Error unzipping file': 'Errore estrazione file', + 'Invalid patch file': 'File della patch non valido', + 'Using big files is not recommended': 'Non è raccomandato usare file di grandi dimensioni' + }, + 'nl': { + 'Creator mode': 'Creator-modus', + 'Settings': 'Settings', + 'Use patch name for output': 'Use patch name for output', + 'Light theme': 'Light theme', + + 'Apply patch': 'Pas patch toe', + 'ROM file:': 'ROM bestand:', + 'Patch file:': 'Patch bestand:', + 'Remove %s header': 'Verwijder rubriek %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Compatibele formaten:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Patch toepassen...', + 'Downloading...': 'Downloaden...', + 'Unzipping...': 'Uitpakken...', + + 'Create patch': 'Maak patch', + 'Original ROM:': 'Originale ROM:', + 'Modified ROM:': 'Aangepaste ROM:', + 'Patch type:': 'Type patch:', + 'Creating patch...': 'Patch maken...', + + 'Source ROM checksum mismatch': 'Controlesom van bron-ROM komt niet overeen', + 'Target ROM checksum mismatch': 'Controlesom van doel-ROM komt niet overeen', + 'Patch checksum mismatch': 'Controlesom van patch komt niet overeen', + 'Error downloading %s': 'Fout bij downloaden van patch', + 'Error unzipping file': 'Fout bij uitpakken van bestand', + 'Invalid patch file': 'Ongeldig patchbestand', + 'Using big files is not recommended': 'Het gebruik van grote bestanden wordt niet aanbevolen' + }, + 'sv': { + 'Creator mode': 'Skaparläge', + 'Settings': 'Settings', + 'Use patch name for output': 'Use patch name for output', + 'Light theme': 'Light theme', + + 'Apply patch': 'Tillämpa korrigeringsfil', + 'ROM file:': 'ROM-fil:', + 'Patch file:': 'Korrigeringsfil:', + 'Remove %s header': 'Ta bort rubrik %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Kompatibla format:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Tillämpar korrigeringsfil...', + 'Downloading...': 'Ladda ner...', + 'Unzipping...': 'Packa upp...', + + 'Create patch': 'Skapa korrigeringsfil', + 'Original ROM:': 'Original-ROM:', + 'Modified ROM:': 'Modifierad ROM:', + 'Patch type:': 'Korrigeringsfil-typ:', + 'Creating patch...': 'Skapa korrigeringsfil...', + + 'Source ROM checksum mismatch': 'ROM-källans kontrollsumman matchar inte', + 'Target ROM checksum mismatch': 'ROM-målets kontrollsumman matchar inte', + 'Patch checksum mismatch': 'korrigeringsfilens kontrollsumman matchar inte', + 'Error downloading %s': 'Fel vid nedladdning av korrigeringsfilen', + 'Error unzipping file': 'Det gick inte att packa upp filen', + 'Invalid patch file': 'Ogiltig korrigeringsfil', + 'Using big files is not recommended': 'Användning av stora filer rekommenderas inte' + }, + 'ca': { + 'Creator mode': 'Mode creador', + 'Settings': 'Configuració', + 'Use patch name for output': 'Desar amb nom del pedaç', + 'Light theme': 'Tema clar', + + 'Apply patch': 'Aplicar pedaç', + 'ROM file:': 'Arxiu ROM:', + 'Patch file:': 'Arxiu pedaç:', + 'Remove %s header': 'Treure capçalera %s', + 'Add %s header': 'Afegir capçalera %s', + 'Compatible formats:': 'Formats compatibles:', + 'Description:': 'Descripció:', + 'Required ROM:': 'ROM requerida:', + 'Required %s:': '%s requerit:', + 'Applying patch...': 'Aplicant pedaç...', + 'Downloading...': 'Descarregant...', + 'Unzipping...': 'Descomprimint...', + + 'Create patch': 'Crear pedaç', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipus de pedaç:', + 'Creating patch...': 'Creant pedaç...', + + 'Source ROM checksum mismatch': 'Checksum de ROM original no vàlida', + 'Target ROM checksum mismatch': 'Checksum de ROM creada no vàlida', + 'Patch checksum mismatch': 'Checksum de pedaç no vàlida', + 'Error downloading %s': 'Error descarregant %s', + 'Error unzipping file': 'Error descomprimint arxiu', + 'Invalid patch file': 'Arxiu de pedaç no vàlid', + 'Using big files is not recommended': 'No és recomanable usar arxius molt grans' + }, + 'ca-va': { + 'Creator mode': 'Mode creador', + 'Settings': 'Configuració', + 'Use patch name for output': 'Guardar amb nom del pedaç', + 'Light theme': 'Tema clar', + + 'Apply patch': 'Aplicar pedaç', + 'ROM file:': 'Arxiu ROM:', + 'Patch file:': 'Arxiu pedaç:', + 'Remove %s header': 'Llevar capçalera %s', + 'Add %s header': 'Afegir capçalera %s', + 'Compatible formats:': 'Formats compatibles:', + 'Description:': 'Descripció:', + 'Required ROM:': 'ROM requerida:', + 'Required %s:': '%s requerit:', + 'Applying patch...': 'Aplicant pedaç...', + 'Downloading...': 'Descarregant...', + 'Unzipping...': 'Descomprimint...', + + 'Create patch': 'Crear pedaç', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipus de pedaç:', + 'Creating patch...': 'Creant pedaç...', + + 'Source ROM checksum mismatch': 'Checksum de ROM original incorrecta', + 'Target ROM checksum mismatch': 'Checksum de ROM creada incorrecta', + 'Patch checksum mismatch': 'Checksum de pedaç incorrecte', + 'Error downloading %s': 'Error descarregant %s', + 'Error unzipping file': 'Error descomprimint arxiu', + 'Invalid patch file': 'Arxiu de pedaç incorrecte', + 'Using big files is not recommended': 'No és recomanable utilitzar arxius molt grans' + }, + 'ru': { + 'Creator mode': 'Режим создания', + 'Settings': 'Settings', + 'Use patch name for output': 'Use patch name for output', + 'Light theme': 'Light theme', + + 'Apply patch': 'Применить патч', + 'ROM file:': 'Файл ROM:', + 'Patch file:': 'Файл патча:', + 'Remove %s header': 'Удалить заголовок перед применением', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Совместимые форматы:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Применяется патч...', + 'Downloading...': 'Загрузка...', + 'Unzipping...': 'Unzipping...', + + 'Create patch': 'Создать патч', + 'Original ROM:': 'Оригинальный ROM:', + 'Modified ROM:': 'Изменённый ROM:', + 'Patch type:': 'Тип патча:', + 'Creating patch...': 'Патч создаётся...', + + 'Source ROM checksum mismatch': 'Неправильная контрольная сумма входного ROM', + 'Target ROM checksum mismatch': 'Неправильная контрольная сумма выходного ROM', + 'Patch checksum mismatch': 'Неправильная контрольная сумма патча', + 'Error downloading %s': 'Ошибка при скачивании патча', + 'Error unzipping file': 'Error unzipping file', + 'Invalid patch file': 'Неправильный файл патча', + 'Using big files is not recommended': 'Не рекомендуется использовать большие файлы' + }, + 'pt-br': { + 'Creator mode': 'Modo criador', + 'Settings': 'Configurações', + 'Use patch name for output': 'Usar o nome do patch na saída', + 'Fix ROM header checksum': 'Consertar o checksum do cabeçalho da ROM', + 'Light theme': 'Tema leve', + + 'Apply patch': 'Aplicar patch', + 'ROM file:': 'Arquivo da ROM:', + 'Patch file:': 'Arquivo do patch:', + 'Remove %s header': 'Remover cabeçalho %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Formatos compatíveis:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Aplicando patch...', + 'Downloading...': 'Baixando...', + 'Unzipping...': 'Descompactando...', + + 'Create patch': 'Criar patch', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipo de patch:', + 'Creating patch...': 'Criando o patch...', + + 'Source ROM checksum mismatch': 'O checksum da ROM original é inválido', + 'Target ROM checksum mismatch': 'O checksum da ROM alvo é inválido', + 'Patch checksum mismatch': 'O checksum do patch é inválido', + 'Error downloading %s': 'Erro ao baixar o %s', + 'Error unzipping file': 'Erro ao descompactar o arquivo', + 'Invalid patch file': 'Arquivo do patch inválido', + 'Using big files is not recommended': 'O uso de arquivos grandes não é recomendado' + }, + 'ja': { + 'Creator mode': '作成モード', + 'Settings': '設定', + 'Use patch name for output': 'パッチと同じ名前で出力', + 'Light theme': 'ライトテーマ', + + 'Apply patch': 'パッチを当て', + 'ROM file:': 'ROMファィル:', + 'Patch file:': 'パッチファイル:', + 'Remove %s header': 'ヘッダーを削除', + //'Add %s header': 'Add %s header', + 'Compatible formats:': '互換性のあるフォーマット:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'パッチを当ている…', + 'Downloading...': 'ダウンロードしている…', + 'Unzipping...': '解凍している…', + + 'Create patch': 'パッチを作成', + 'Original ROM:': '元のROM:', + 'Modified ROM:': '変更されたROM:', + 'Patch type:': 'パッチのタイプ:', + 'Creating patch...': 'パッチを作成している…', + + 'Source ROM checksum mismatch': 'ソースROMチェックサムの不一致', + 'Target ROM checksum mismatch': 'ターゲットROMチェクサムの不一致', + 'Patch checksum mismatch': 'バッチチェックサムの不一致', + 'Error downloading %s': 'パッチのダウンロードエラー', + 'Error unzipping file': 'パッチの解凍エラー', + 'Invalid patch file': '無効なパッチエラー', + 'Using big files is not recommended': '大きなファイルの使いはおすすめしない。' + }, + 'zh-cn': { + 'Creator mode': '创建模式', + 'Settings': '设置', + 'Use patch name for output': '修改后ROM文件名和补丁保持一致', + 'Light theme': '浅色主题', + + 'Apply patch': '打补丁', + 'ROM file:': 'ROM文件:', + 'Patch file:': '补丁文件:', + 'Remove %s header': '删除文件头', + //'Add %s header': 'Add %s header', + 'Compatible formats:': '兼容补丁格式:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': '正在打补丁……', + 'Downloading...': '正在下载……', + 'Unzipping...': '正在解压……', + + 'Create patch': '创建补丁', + 'Original ROM:': '原始ROM:', + 'Modified ROM:': '修改后ROM:', + 'Patch type:': '补丁类型:', + 'Creating patch...': '正在创建补丁……', + + 'Source ROM checksum mismatch': '原始ROM校验和不匹配', + 'Target ROM checksum mismatch': '目标ROM校验和不匹配', + 'Patch checksum mismatch': '补丁文件校验和不匹配', + 'Error downloading %s': '下载出错:%s', + 'Error unzipping file': '解压出错', + 'Invalid patch file': '无效补丁', + 'Using big files is not recommended': '不推荐使用大文件。' + }, + 'zh-tw': { + 'Creator mode': '創作者模式', + 'Settings': '設定', + 'Use patch name for output': '修改後ROM檔名和patch保持一致', + 'Fix ROM header checksum': '修正ROM檔頭校驗碼', + 'Light theme': '淺色主題', + + 'Apply patch': '套用patch', + 'ROM file:': 'ROM檔:', + 'Patch file:': 'patch檔:', + 'Remove %s header': '刪除檔頭', + //'Add %s header': 'Add %s header', + 'Compatible formats:': '相容格式:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': '套用patch中……', + 'Downloading...': '下載中……', + 'Unzipping...': '解壓中……', + + 'Create patch': '創建patch', + 'Original ROM:': '原始ROM:', + 'Modified ROM:': '修改後ROM:', + 'Patch type:': 'patch類型:', + 'Creating patch...': '正在創建patch……', + + 'Source ROM checksum mismatch': '原始ROM校驗碼不匹配', + 'Target ROM checksum mismatch': '目標ROM校驗碼不匹配', + 'Patch checksum mismatch': 'patch檔校驗碼不匹配', + 'Error downloading %s': '下載出錯:%s', + 'Error unzipping file': '解壓出錯', + 'Invalid patch file': '無效的patch檔', + 'Using big files is not recommended': '不建議使用大檔。' + } +}; \ No newline at end of file diff --git a/rom-patcher-js/RomPatcher.webworker.apply.js b/rom-patcher-js/RomPatcher.webworker.apply.js new file mode 100644 index 0000000..f26f18a --- /dev/null +++ b/rom-patcher-js/RomPatcher.webworker.apply.js @@ -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 + ] + ); + } +}; \ No newline at end of file diff --git a/rom-patcher-js/RomPatcher.webworker.crc.js b/rom-patcher-js/RomPatcher.webworker.crc.js new file mode 100644 index 0000000..8d39204 --- /dev/null +++ b/rom-patcher-js/RomPatcher.webworker.crc.js @@ -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] + ); +}; \ No newline at end of file diff --git a/rom-patcher-js/RomPatcher.webworker.create.js b/rom-patcher-js/RomPatcher.webworker.create.js new file mode 100644 index 0000000..9fde1d7 --- /dev/null +++ b/rom-patcher-js/RomPatcher.webworker.create.js @@ -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 + ] + ); +}; \ No newline at end of file diff --git a/rom-patcher-js/assets/icon_alert_orange.svg b/rom-patcher-js/assets/icon_alert_orange.svg new file mode 100644 index 0000000..6552d6a --- /dev/null +++ b/rom-patcher-js/assets/icon_alert_orange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rom-patcher-js/assets/icon_check_circle_green.svg b/rom-patcher-js/assets/icon_check_circle_green.svg new file mode 100644 index 0000000..a4f5e02 --- /dev/null +++ b/rom-patcher-js/assets/icon_check_circle_green.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rom-patcher-js/assets/icon_upload.svg b/rom-patcher-js/assets/icon_upload.svg new file mode 100644 index 0000000..2219eed --- /dev/null +++ b/rom-patcher-js/assets/icon_upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rom-patcher-js/assets/icon_x_circle_red.svg b/rom-patcher-js/assets/icon_x_circle_red.svg new file mode 100644 index 0000000..a782ae5 --- /dev/null +++ b/rom-patcher-js/assets/icon_x_circle_red.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rom-patcher-js/assets/powered_by_rom_patcher_js.png b/rom-patcher-js/assets/powered_by_rom_patcher_js.png new file mode 100644 index 0000000..3b7033a Binary files /dev/null and b/rom-patcher-js/assets/powered_by_rom_patcher_js.png differ diff --git a/rom-patcher-js/modules/BinFile.js b/rom-patcher-js/modules/BinFile.js new file mode 100644 index 0000000..53e3dfc --- /dev/null +++ b/rom-patcher-js/modules/BinFile.js @@ -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'); +} diff --git a/rom-patcher-js/modules/HashCalculator.js b/rom-patcher-js/modules/HashCalculator.js new file mode 100644 index 0000000..1d9d3cb --- /dev/null +++ b/rom-patcher-js/modules/HashCalculator.js @@ -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; +} diff --git a/rom-patcher-js/modules/RomPatcher.format.aps_gba.js b/rom-patcher-js/modules/RomPatcher.format.aps_gba.js new file mode 100644 index 0000000..66e258f --- /dev/null +++ b/rom-patcher-js/modules/RomPatcher.format.aps_gba.js @@ -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; i2){ + patch.addRLERecord(offset, differentBytes[0], differentBytes.length); + }else{ + patch.addRecord(offset, differentBytes); + } + //NO se puede comentar??? why???? + } + } + + return patch +} \ No newline at end of file diff --git a/rom-patcher-js/modules/RomPatcher.format.bps.js b/rom-patcher-js/modules/RomPatcher.format.bps.js new file mode 100644 index 0000000..9a2803c --- /dev/null +++ b/rom-patcher-js/modules/RomPatcher.format.bps.js @@ -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> 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= 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 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; +} \ No newline at end of file diff --git a/rom-patcher-js/modules/RomPatcher.format.ips.js b/rom-patcher-js/modules/RomPatcher.format.ips.js new file mode 100644 index 0000000..7bbb5a9 --- /dev/null +++ b/rom-patcher-js/modules/RomPatcher.format.ips.js @@ -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; iromFile.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; inewFileSize){ + 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; i6){ + // 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(lastOffsetpatch.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 */ +} \ No newline at end of file diff --git a/rom-patcher-js/modules/RomPatcher.format.ppf.js b/rom-patcher-js/modules/RomPatcher.format.ppf.js new file mode 100644 index 0000000..42a2c9b --- /dev/null +++ b/rom-patcher-js/modules/RomPatcher.format.ppf.js @@ -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>>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; inewFileSize) + 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; i3){ + 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.fileSizetarget) or 'A' (source>=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; ifile.targetFileSize?'M':'A'); + patchFile.writeVLV(file.overflowText.length); + patchFile.writeString(file.overflowText); + } + + for(var j=0; j>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 +} \ No newline at end of file diff --git a/rom-patcher-js/modules/RomPatcher.format.vcdiff.js b/rom-patcher-js/modules/RomPatcher.format.vcdiff.js new file mode 100644 index 0000000..3040d0d --- /dev/null +++ b/rom-patcher-js/modules/RomPatcher.format.vcdiff.js @@ -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.offset0){ + this.near[this.nextNearSlot]=address; + this.nextNearSlot=(this.nextNearSlot+1)%this.nearSize; + } + + if(this.sameSize>0){ + this.same[address%(this.sameSize*256)]=address; + } +} \ No newline at end of file diff --git a/rom-patcher-js/modules/zip.js/LICENSE b/rom-patcher-js/modules/zip.js/LICENSE new file mode 100644 index 0000000..d4b12d8 --- /dev/null +++ b/rom-patcher-js/modules/zip.js/LICENSE @@ -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. diff --git a/rom-patcher-js/modules/zip.js/inflate.js b/rom-patcher-js/modules/zip.js/inflate.js new file mode 100644 index 0000000..3355821 --- /dev/null +++ b/rom-patcher-js/modules/zip.js/inflate.js @@ -0,0 +1,36 @@ +/* + Copyright (c) 2013 Gildas Lormeau. All rights reserved. + + 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. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE 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. + */ + +/* + * This program is based on JZlib 1.0.2 ymnk, JCraft,Inc. + * JZlib is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +!function(i){"use strict";var P=0,q=1,B=-2,C=-3,x=-4,F=-5,G=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],H=1440,a=[96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,192,80,7,10,0,8,96,0,8,32,0,9,160,0,8,0,0,8,128,0,8,64,0,9,224,80,7,6,0,8,88,0,8,24,0,9,144,83,7,59,0,8,120,0,8,56,0,9,208,81,7,17,0,8,104,0,8,40,0,9,176,0,8,8,0,8,136,0,8,72,0,9,240,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,200,81,7,13,0,8,100,0,8,36,0,9,168,0,8,4,0,8,132,0,8,68,0,9,232,80,7,8,0,8,92,0,8,28,0,9,152,84,7,83,0,8,124,0,8,60,0,9,216,82,7,23,0,8,108,0,8,44,0,9,184,0,8,12,0,8,140,0,8,76,0,9,248,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,196,81,7,11,0,8,98,0,8,34,0,9,164,0,8,2,0,8,130,0,8,66,0,9,228,80,7,7,0,8,90,0,8,26,0,9,148,84,7,67,0,8,122,0,8,58,0,9,212,82,7,19,0,8,106,0,8,42,0,9,180,0,8,10,0,8,138,0,8,74,0,9,244,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,204,81,7,15,0,8,102,0,8,38,0,9,172,0,8,6,0,8,134,0,8,70,0,9,236,80,7,9,0,8,94,0,8,30,0,9,156,84,7,99,0,8,126,0,8,62,0,9,220,82,7,27,0,8,110,0,8,46,0,9,188,0,8,14,0,8,142,0,8,78,0,9,252,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,194,80,7,10,0,8,97,0,8,33,0,9,162,0,8,1,0,8,129,0,8,65,0,9,226,80,7,6,0,8,89,0,8,25,0,9,146,83,7,59,0,8,121,0,8,57,0,9,210,81,7,17,0,8,105,0,8,41,0,9,178,0,8,9,0,8,137,0,8,73,0,9,242,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,202,81,7,13,0,8,101,0,8,37,0,9,170,0,8,5,0,8,133,0,8,69,0,9,234,80,7,8,0,8,93,0,8,29,0,9,154,84,7,83,0,8,125,0,8,61,0,9,218,82,7,23,0,8,109,0,8,45,0,9,186,0,8,13,0,8,141,0,8,77,0,9,250,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,198,81,7,11,0,8,99,0,8,35,0,9,166,0,8,3,0,8,131,0,8,67,0,9,230,80,7,7,0,8,91,0,8,27,0,9,150,84,7,67,0,8,123,0,8,59,0,9,214,82,7,19,0,8,107,0,8,43,0,9,182,0,8,11,0,8,139,0,8,75,0,9,246,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,206,81,7,15,0,8,103,0,8,39,0,9,174,0,8,7,0,8,135,0,8,71,0,9,238,80,7,9,0,8,95,0,8,31,0,9,158,84,7,99,0,8,127,0,8,63,0,9,222,82,7,27,0,8,111,0,8,47,0,9,190,0,8,15,0,8,143,0,8,79,0,9,254,96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,193,80,7,10,0,8,96,0,8,32,0,9,161,0,8,0,0,8,128,0,8,64,0,9,225,80,7,6,0,8,88,0,8,24,0,9,145,83,7,59,0,8,120,0,8,56,0,9,209,81,7,17,0,8,104,0,8,40,0,9,177,0,8,8,0,8,136,0,8,72,0,9,241,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,201,81,7,13,0,8,100,0,8,36,0,9,169,0,8,4,0,8,132,0,8,68,0,9,233,80,7,8,0,8,92,0,8,28,0,9,153,84,7,83,0,8,124,0,8,60,0,9,217,82,7,23,0,8,108,0,8,44,0,9,185,0,8,12,0,8,140,0,8,76,0,9,249,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,197,81,7,11,0,8,98,0,8,34,0,9,165,0,8,2,0,8,130,0,8,66,0,9,229,80,7,7,0,8,90,0,8,26,0,9,149,84,7,67,0,8,122,0,8,58,0,9,213,82,7,19,0,8,106,0,8,42,0,9,181,0,8,10,0,8,138,0,8,74,0,9,245,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,205,81,7,15,0,8,102,0,8,38,0,9,173,0,8,6,0,8,134,0,8,70,0,9,237,80,7,9,0,8,94,0,8,30,0,9,157,84,7,99,0,8,126,0,8,62,0,9,221,82,7,27,0,8,110,0,8,46,0,9,189,0,8,14,0,8,142,0,8,78,0,9,253,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,195,80,7,10,0,8,97,0,8,33,0,9,163,0,8,1,0,8,129,0,8,65,0,9,227,80,7,6,0,8,89,0,8,25,0,9,147,83,7,59,0,8,121,0,8,57,0,9,211,81,7,17,0,8,105,0,8,41,0,9,179,0,8,9,0,8,137,0,8,73,0,9,243,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,203,81,7,13,0,8,101,0,8,37,0,9,171,0,8,5,0,8,133,0,8,69,0,9,235,80,7,8,0,8,93,0,8,29,0,9,155,84,7,83,0,8,125,0,8,61,0,9,219,82,7,23,0,8,109,0,8,45,0,9,187,0,8,13,0,8,141,0,8,77,0,9,251,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,199,81,7,11,0,8,99,0,8,35,0,9,167,0,8,3,0,8,131,0,8,67,0,9,231,80,7,7,0,8,91,0,8,27,0,9,151,84,7,67,0,8,123,0,8,59,0,9,215,82,7,19,0,8,107,0,8,43,0,9,183,0,8,11,0,8,139,0,8,75,0,9,247,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,207,81,7,15,0,8,103,0,8,39,0,9,175,0,8,7,0,8,135,0,8,71,0,9,239,80,7,9,0,8,95,0,8,31,0,9,159,84,7,99,0,8,127,0,8,63,0,9,223,82,7,27,0,8,111,0,8,47,0,9,191,0,8,15,0,8,143,0,8,79,0,9,255],r=[80,5,1,87,5,257,83,5,17,91,5,4097,81,5,5,89,5,1025,85,5,65,93,5,16385,80,5,3,88,5,513,84,5,33,92,5,8193,82,5,9,90,5,2049,86,5,129,192,5,24577,80,5,2,87,5,385,83,5,25,91,5,6145,81,5,7,89,5,1537,85,5,97,93,5,24577,80,5,4,88,5,769,84,5,49,92,5,12289,82,5,13,90,5,3073,86,5,193,192,5,24577],w=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],c=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,112,112],v=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],h=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],D=15;function J(){var f,o,E,S,U,z;function b(i,t,e,n,a,r,_,l,d,s,f){var o,b,u,x,w,c,v,h,k,m,y,g,p,I,A;for(m=0,w=e;E[i[t+m]]++,m++,0!==--w;);if(E[0]==e)return _[0]=-1,l[0]=0,P;for(h=l[0],c=1;c<=D&&0===E[c];c++);for(h<(v=c)&&(h=c),w=D;0!==w&&0===E[w];w--);for((u=w)o+1&&(b-=o+1,p=v,cH)return C;U[x]=y=s[0],s[0]+=A,0!==x?(z[x]=w,S[0]=c,c=w>>>g-(S[1]=h),S[2]=y-U[x-1]-c,d.set(S,3*(U[x-1]+c))):_[0]=y}for(S[1]=v-g,e<=m?S[0]=192:f[m]>>g;c>>=1)w^=c;for(w^=c,k=(1<>=s[p+1],u-=s[p+1],0!=(16&o)){for(o&=15,m=s[p+2]+(b&G[o]),b>>=o,u-=o;u<15;)w--,b|=(255&l.read_byte(x++))<>=s[p+1],u-=s[p+1],0!=(16&o)){for(o&=15;u>=o,u-=o,v-=m,y<=c)0>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,C;d+=s[p+2],o=s[p=3*(f+(d+=b&G[o]))]}break}if(0!=(64&o))return 0!=(32&o)?(w+=m=u>>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,q):(l.msg="invalid literal/length code",w+=m=u>>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,C);if(d+=s[p+2],0===(o=s[p=3*(f+(d+=b&G[o]))])){b>>=s[p+1],u-=s[p+1],_.window[c++]=s[p+2],v--;break}}else b>>=s[p+1],u-=s[p+1],_.window[c++]=s[p+2],v--}while(258<=v&&10<=w);return w+=m=u>>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,P}this.init=function(i,t,e,n,a,r){u=U,p=i,I=t,w=e,A=n,c=a,E=r,x=null},this.proc=function(i,t,e){var n,a,r,_,l,d,s,f=0,o=0,b=0;for(b=t.next_in_index,_=t.avail_in,f=i.bitb,o=i.bitk,d=(l=i.write)>>=x[a+1],o-=x[a+1],0===(r=x[a])){m=x[a+2],u=N;break}if(0!=(16&r)){y=15&r,v=x[a+2],u=j;break}if(0==(64&r)){k=r,h=a/3+x[a+2];break}if(0==(32&r))return u=R,t.msg="invalid literal/length code",e=C,i.bitb=f,i.bitk=o,t.avail_in=_,t.total_in+=b-t.next_in_index,t.next_in_index=b,i.write=l,i.inflate_flush(t,e);u=O;break;case j:for(n=y;o>=n,o-=n,k=I,x=c,h=E,u=K;case K:for(n=k;o>=x[a+1],o-=x[a+1],0!=(16&(r=x[a]))){y=15&r,g=x[a+2],u=L;break}if(0!=(64&r))return u=R,t.msg="invalid distance code",e=C,i.bitb=f,i.bitk=o,t.avail_in=_,t.total_in+=b-t.next_in_index,t.next_in_index=b,i.write=l,i.inflate_flush(t,e);k=r,h=a/3+x[a+2];break;case L:for(n=y;o>=n,o-=n,u=M;case M:for(s=l-g;s<0;)s+=i.end;for(;0!==v;){if(0===d&&(l==i.end&&0!==i.read&&(d=(l=0)i.avail_out&&(e=i.avail_out),0!==e&&t==F&&(t=P),i.avail_out-=e,i.total_out+=e,i.next_out.set(y.window.subarray(a,a+e),n),n+=e,(a+=e)==y.end&&(a=0,y.write==y.end&&(y.write=0),(e=y.write-a)>i.avail_out&&(e=i.avail_out),0!==e&&t==F&&(t=P),i.avail_out-=e,i.total_out+=e,i.next_out.set(y.window.subarray(a,a+e),n),n+=e,a+=e),i.next_out_index=n,y.read=a,t},y.proc=function(i,t){var e,n,a,r,_,l,d,s;for(r=i.next_in_index,_=i.avail_in,n=y.bitb,a=y.bitk,d=(l=y.write)>>1){case 0:n>>>=3,n>>>=e=7&(a-=3),a-=e,g=W;break;case 1:var f=[],o=[],b=[[]],u=[[]];J.inflate_trees_fixed(f,o,b,u),U.init(f[0],o[0],b[0],0,u[0],0),n>>>=3,a-=3,g=ii;break;case 2:n>>>=3,a-=3,g=Y;break;case 3:return n>>>=3,a-=3,g=ni,i.msg="invalid block type",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t)}break;case W:for(;a<32;){if(0===_)return y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);t=P,_--,n|=(255&i.read_byte(r++))<>>16&65535)!=(65535&n))return g=ni,i.msg="invalid stored block lengths",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);p=65535&n,n=a=0,g=0!==p?X:0!==z?ti:V;break;case X:if(0===_)return y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);if(0===d&&(l==y.end&&0!==y.read&&(d=(l=0)>5&31))return g=ni,i.msg="too many length or distance symbols",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);if(e=258+(31&e)+(e>>5&31),!m||m.length>>=14,a-=14,A=0,g=Z;case Z:for(;A<4+(I>>>10);){for(;a<3;){if(0===_)return y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);t=P,_--,n|=(255&i.read_byte(r++))<>>=3,a-=3}for(;A<19;)m[T[A++]]=0;if(E[0]=7,(e=j.inflate_trees_bits(m,E,S,D,i))!=P)return(t=e)==C&&(m=null,g=ni),y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);A=0,g=$;case $:for(;!(258+(31&(e=I))+(e>>5&31)<=A);){var x,w;for(e=E[0];a>>=e,a-=e,m[A++]=w;else{for(s=18==w?7:w-14,x=18==w?11:3;a>>=e)&G[s],n>>>=s,a-=s,258+(31&(e=I))+(e>>5&31)<(s=A)+x||16==w&&s<1)return m=null,g=ni,i.msg="invalid bit length repeat",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);for(w=16==w?m[s-1]:0;m[s++]=w,0!=--x;);A=s}}S[0]=-1;var c=[],v=[],h=[],k=[];if(c[0]=9,v[0]=6,e=I,(e=j.inflate_trees_dynamic(257+(31&e),1+(e>>5&31),m,c,v,h,k,D,i))!=P)return e==C&&(m=null,g=ni),t=e,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);U.init(c[0],v[0],D,h[0],D,k[0]),g=ii;case ii:if(y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,(t=U.proc(y,i,t))!=q)return y.inflate_flush(i,t);if(t=P,U.free(i),r=i.next_in_index,_=i.avail_in,n=y.bitb,a=y.bitk,d=(l=y.write)>4)>i.istate.wbits){i.istate.mode=13,i.msg="invalid window size",i.istate.marker=5;break}i.istate.mode=1;case 1:if(0===i.avail_in)return e;if(e=t,i.avail_in--,i.total_in++,n=255&i.read_byte(i.next_in_index++),((i.istate.method<<8)+n)%31!=0){i.istate.mode=13,i.msg="incorrect header check",i.istate.marker=5;break}if(0==(32&n)){i.istate.mode=7;break}i.istate.mode=2;case 2:if(0===i.avail_in)return e;e=t,i.avail_in--,i.total_in++,i.istate.need=(255&i.read_byte(i.next_in_index++))<<24&4278190080,i.istate.mode=3;case 3:if(0===i.avail_in)return e;e=t,i.avail_in--,i.total_in++,i.istate.need+=(255&i.read_byte(i.next_in_index++))<<16&16711680,i.istate.mode=4;case 4:if(0===i.avail_in)return e;e=t,i.avail_in--,i.total_in++,i.istate.need+=(255&i.read_byte(i.next_in_index++))<<8&65280,i.istate.mode=5;case 5:return 0===i.avail_in?e:(e=t,i.avail_in--,i.total_in++,i.istate.need+=255&i.read_byte(i.next_in_index++),i.istate.mode=6,2);case 6:return i.istate.mode=13,i.msg="need dictionary",i.istate.marker=0,B;case 7:if((e=i.istate.blocks.proc(i,e))==C){i.istate.mode=13,i.istate.marker=0;break}if(e==P&&(e=t),e!=q)return e;e=t,i.istate.blocks.reset(i,i.istate.was),i.istate.mode=12;case 12:return q;case 13:return C;default:return B}},e.inflateSetDictionary=function(i,t,e){var n=0,a=e;return i&&i.istate&&6==i.istate.mode?(a>=1<>>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); \ No newline at end of file diff --git a/rom-patcher-js/modules/zip.js/zip.min.js b/rom-patcher-js/modules/zip.js/zip.min.js new file mode 100644 index 0000000..620e021 --- /dev/null +++ b/rom-patcher-js/modules/zip.js/zip.min.js @@ -0,0 +1,28 @@ +/* + Copyright (c) 2013 Gildas Lormeau. All rights reserved. + + 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. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE 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. + */ +!function(b){"use strict";var o,k="File format is not recognized.",a="File contains encrypted entry.",s="File is using Zip64 (4gb+ file size).",w="Error while reading zip file.",n="Error while reading file data.",y=524288,c="text/plain";try{o=0===new Blob([new DataView(new ArrayBuffer(0))]).size}catch(e){}function r(){this.crc=-1}function l(){}function A(e,t){var r,n;return r=new ArrayBuffer(e),n=new Uint8Array(r),t&&n.set(t,0),{buffer:r,array:n,view:new DataView(r)}}function e(){}function t(n){var i,o=this;o.size=0,o.init=function(e,t){var r=new Blob([n],{type:c});(i=new f(r)).init(function(){o.size=i.size,e()},t)},o.readUint8Array=function(e,t,r,n){i.readUint8Array(e,t,r,n)}}function i(f){var u,r=this;r.size=0,r.init=function(e){for(var t=f.length;"="==f.charAt(t-1);)t--;u=f.indexOf(",")+1,r.size=Math.floor(.75*(t-u)),e()},r.readUint8Array=function(e,t,r){var n,i=A(t),o=4*Math.floor(e/3),a=4*Math.ceil((e+t)/3),s=b.atob(f.substring(o+u,a+u)),c=e-3*Math.floor(o/4);for(n=c;ne.size)throw new RangeError("offset:"+t+", length:"+r+", size:"+e.size);return e.slice?e.slice(t,t+r):e.webkitSlice?e.webkitSlice(t,t+r):e.mozSlice?e.mozSlice(t,t+r):e.msSlice?e.msSlice(t,t+r):void 0}(o,e,t))}catch(e){n(e)}}}function u(){}function h(n){var i;this.init=function(e){i=new Blob([],{type:c}),e()},this.writeUint8Array=function(e,t){i=new Blob([i,o?e:e.buffer],{type:c}),t()},this.getData=function(t,e){var r=new FileReader;r.onload=function(e){t(e.target.result)},r.onerror=e,r.readAsText(i,n)}}function p(t){var o="",a="";this.init=function(e){o+="data:"+(t||"")+";base64,",e()},this.writeUint8Array=function(e,t){var r,n=a.length,i=a;for(a="",r=0;r<3*Math.floor((n+e.length)/3)-n;r++)i+=String.fromCharCode(e[r]);for(;r>16,r=65535&e;try{return new Date(1980+((65024&t)>>9),((480&t)>>5)-1,31&t,(63488&r)>>11,(2016&r)>>5,2*(31&r),0)}catch(e){}}(e.lastModDateRaw),1!=(1&e.bitFlag)?((n||8!=(8&e.bitFlag))&&(e.crc32=t.view.getUint32(r+10,!0),e.compressedSize=t.view.getUint32(r+14,!0),e.uncompressedSize=t.view.getUint32(r+18,!0)),4294967295!==e.compressedSize&&4294967295!==e.uncompressedSize?(e.filenameLength=t.view.getUint16(r+22,!0),e.extraFieldLength=t.view.getUint16(r+24,!0)):i(s)):i(a)}function m(m,t,U){var z=0;function l(){}l.prototype.getData=function(w,i,h,p){var v=this;function d(e,t){var r,n;p&&(r=t,(n=A(4)).view.setUint32(0,r),v.crc32!=n.view.getUint32(0))?U("CRC failed."):w.getData(function(e){i(e)})}function g(e){U(e||n)}function y(e){U(e||"Error while writing file data.")}m.readUint8Array(v.offset,30,function(e){var l,t=A(e.length,e);1347093252==t.view.getUint32(0)?(M(v,t,4,!1,U),l=v.offset+30+v.filenameLength+v.extraFieldLength,w.init(function(){var e,t,r,n,i,o,a,s,c,f,u;0===v.compressionMethod?D(v._worker,z++,m,w,l,v.compressedSize,p,d,h,g,y):(e=v._worker,t=z++,r=m,n=w,i=l,o=v.compressedSize,a=d,s=h,c=g,f=y,u=p?"output":"none",b.zip.useWebWorkers?S(e,{sn:t,codecClass:"Inflater",crcType:u},r,n,i,o,s,a,c,f):_(new b.zip.Inflater,r,n,i,o,u,s,a,c,f))},y)):U(k)},g)};var r={getEntries:function(f){var u=this._worker;!function(n){var i=22;if(m.size=m.size?U(k):m.readUint8Array(t,m.size-t,function(e){var t,r,n,i,o=0,a=[],s=A(e.length,e);for(t=0;t>>8^r[255&(t^e[n])];this.crc=t},r.prototype.get=function(){return~this.crc},r.prototype.table=function(){var e,t,r,n=[];for(e=0;e<256;e++){for(r=e,t=0;t<8;t++)1&r?r=r>>>1^3988292384:r>>>=1;n[e]=r}return n}(),l.prototype.append=function(e,t){return e},l.prototype.flush=function(){},(t.prototype=new e).constructor=t,(i.prototype=new e).constructor=i,(f.prototype=new e).constructor=f,u.prototype.getData=function(e){e(this.data)},(h.prototype=new u).constructor=h,(p.prototype=new u).constructor=p,(v.prototype=new u).constructor=v;var C={deflater:["z-worker.js","deflate.js"],inflater:["z-worker.js","inflate.js"]};function E(e,n,i){if(null===b.zip.workerScripts||null===b.zip.workerScriptsPath){var t,r,o;if(b.zip.workerScripts){if(t=b.zip.workerScripts[e],!Array.isArray(t))return void i(new Error("zip.workerScripts."+e+" is not an array!"));r=t,o=document.createElement("a"),t=r.map(function(e){return o.href=e,o.href})}else(t=C[e].slice(0))[0]=(b.zip.workerScriptsPath||"")+t[0];var a=new Worker(t[0]);a.codecTime=a.crcTime=0,a.postMessage({type:"importScripts",scripts:t.slice(1)}),a.addEventListener("message",function e(t){var r=t.data;if(r.error)return a.terminate(),void i(r.error);"importScripts"===r.type&&(a.removeEventListener("message",e),a.removeEventListener("error",s),n(a))}),a.addEventListener("error",s)}else i(new Error("Either zip.workerScripts or zip.workerScriptsPath may be set, not both."));function s(e){a.terminate(),i(e)}}function F(e){console.error(e)}b.zip={Reader:e,Writer:u,BlobReader:f,Data64URIReader:i,TextReader:t,BlobWriter:v,Data64URIWriter:p,TextWriter:h,createReader:function(e,t,r){r=r||F,e.init(function(){m(e,t,r)},r)},createWriter:function(e,t,r,n){r=r||F,n=!!n,e.init(function(){U(e,t,r,n)},r)},useWebWorkers:!0,workerScriptsPath:null,workerScripts:null}}(this); diff --git a/rom-patcher-js/style.css b/rom-patcher-js/style.css new file mode 100644 index 0000000..1265ccd --- /dev/null +++ b/rom-patcher-js/style.css @@ -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 */ + 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; +} \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..c7e9821 --- /dev/null +++ b/test.js @@ -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'); + }); +}); + + diff --git a/webapp/app_icon_114.png b/webapp/app_icon_114.png new file mode 100644 index 0000000..ea24c1a Binary files /dev/null and b/webapp/app_icon_114.png differ diff --git a/webapp/app_icon_144.png b/webapp/app_icon_144.png new file mode 100644 index 0000000..d1a3629 Binary files /dev/null and b/webapp/app_icon_144.png differ diff --git a/webapp/app_icon_16.png b/webapp/app_icon_16.png new file mode 100644 index 0000000..7261e98 Binary files /dev/null and b/webapp/app_icon_16.png differ diff --git a/webapp/app_icon_192.png b/webapp/app_icon_192.png new file mode 100644 index 0000000..1172656 Binary files /dev/null and b/webapp/app_icon_192.png differ diff --git a/webapp/app_icon_maskable.png b/webapp/app_icon_maskable.png new file mode 100644 index 0000000..60c74f1 Binary files /dev/null and b/webapp/app_icon_maskable.png differ diff --git a/webapp/icon_close.svg b/webapp/icon_close.svg new file mode 100644 index 0000000..3087eb9 --- /dev/null +++ b/webapp/icon_close.svg @@ -0,0 +1 @@ + diff --git a/webapp/icon_github.svg b/webapp/icon_github.svg new file mode 100644 index 0000000..0cb5bab --- /dev/null +++ b/webapp/icon_github.svg @@ -0,0 +1,2 @@ + + diff --git a/webapp/icon_heart.svg b/webapp/icon_heart.svg new file mode 100644 index 0000000..e2f3079 --- /dev/null +++ b/webapp/icon_heart.svg @@ -0,0 +1 @@ + diff --git a/webapp/icon_settings.svg b/webapp/icon_settings.svg new file mode 100644 index 0000000..ee9104e --- /dev/null +++ b/webapp/icon_settings.svg @@ -0,0 +1 @@ + diff --git a/webapp/logo.png b/webapp/logo.png new file mode 100644 index 0000000..51f3206 Binary files /dev/null and b/webapp/logo.png differ diff --git a/webapp/style.css b/webapp/style.css new file mode 100644 index 0000000..5fe2d62 --- /dev/null +++ b/webapp/style.css @@ -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 */ + 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;} +} diff --git a/webapp/thumbnail.jpg b/webapp/thumbnail.jpg new file mode 100644 index 0000000..ce99cfe Binary files /dev/null and b/webapp/thumbnail.jpg differ diff --git a/webapp/webapp.js b/webapp/webapp.js new file mode 100644 index 0000000..5624e2b --- /dev/null +++ b/webapp/webapp.js @@ -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.
      Try the legacy version'; + + document.getElementById('rom-patcher-container').innerHTML = message; + document.getElementById('rom-patcher-container').style.color = 'red'; + } +}); +