From f7014075ec5cef7284173ba92c67459385196788 Mon Sep 17 00:00:00 2001 From: Marc Robledo Date: Fri, 9 Aug 2024 19:30:49 +0200 Subject: [PATCH] version 3.0 --- .gitignore | 3 + LICENSE | 4 +- README.md | 35 +- _cache_service_worker.js | 101 +- index.html | 318 ++- index.js | 73 + index_template.html | 126 ++ legacy/.nojekyll | 0 legacy/LICENSE | 25 + legacy/README.md | 2 + legacy/index.html | 207 ++ {js => legacy/js}/MarcFile.js | 0 {js => legacy/js}/RomPatcher.js | 22 +- {js => legacy/js}/crc.js | 0 {js => legacy/js}/formats/aps_gba.js | 0 {js => legacy/js}/formats/aps_n64.js | 0 {js => legacy/js}/formats/bps.js | 0 {js => legacy/js}/formats/ips.js | 0 {js => legacy/js}/formats/pmsr.js | 0 {js => legacy/js}/formats/ppf.js | 0 {js => legacy/js}/formats/rup.js | 0 {js => legacy/js}/formats/ups.js | 0 {js => legacy/js}/formats/vcdiff.js | 0 {js => legacy/js}/formats/zip.js | 0 {js => legacy/js}/locale.js | 0 {js => legacy/js}/worker_apply.js | 0 {js => legacy/js}/worker_crc.js | 0 {js => legacy/js}/worker_create.js | 0 {js => legacy/js}/zip.js/inflate.js | 0 {js => legacy/js}/zip.js/z-worker.js | 0 {js => legacy/js}/zip.js/zip.js | 0 legacy/manifest.json | 32 + {style => legacy/style}/RomPatcher.css | 0 {style => legacy/style}/app_icon_114.png | Bin {style => legacy/style}/app_icon_144.png | Bin {style => legacy/style}/app_icon_16.png | Bin {style => legacy/style}/app_icon_192.png | Bin {style => legacy/style}/app_icon_maskable.png | Bin {style => legacy/style}/icon_close.svg | 0 {style => legacy/style}/icon_github.svg | 0 {style => legacy/style}/icon_heart.svg | 0 {style => legacy/style}/icon_settings.svg | 0 {style => legacy/style}/logo.png | Bin {style => legacy/style}/thumbnail.jpg | Bin manifest.json | 10 +- package.json | 19 + rom-patcher-js/RomPatcher.js | 405 ++++ rom-patcher-js/RomPatcher.webapp.js | 1762 +++++++++++++++++ rom-patcher-js/RomPatcher.webworker.apply.js | 82 + rom-patcher-js/RomPatcher.webworker.crc.js | 26 + rom-patcher-js/RomPatcher.webworker.create.js | 36 + rom-patcher-js/assets/icon_alert_orange.svg | 1 + .../assets/icon_check_circle_green.svg | 1 + rom-patcher-js/assets/icon_upload.svg | 1 + rom-patcher-js/assets/icon_x_circle_red.svg | 1 + .../assets/powered_by_rom_patcher_js.png | Bin 0 -> 799 bytes rom-patcher-js/modules/BinFile.js | 475 +++++ rom-patcher-js/modules/HashCalculator.js | 179 ++ .../modules/RomPatcher.format.aps_gba.js | 114 ++ .../modules/RomPatcher.format.aps_n64.js | 212 ++ .../modules/RomPatcher.format.bps.js | 463 +++++ .../modules/RomPatcher.format.ips.js | 235 +++ .../modules/RomPatcher.format.pmsr.js | 97 + .../modules/RomPatcher.format.ppf.js | 269 +++ .../modules/RomPatcher.format.rup.js | 350 ++++ .../modules/RomPatcher.format.ups.js | 224 +++ .../modules/RomPatcher.format.vcdiff.js | 377 ++++ rom-patcher-js/modules/zip.js/LICENSE | 28 + rom-patcher-js/modules/zip.js/inflate.js | 36 + rom-patcher-js/modules/zip.js/z-worker.js | 2 + rom-patcher-js/modules/zip.js/zip.min.js | 28 + rom-patcher-js/style.css | 502 +++++ test.js | 171 ++ webapp/app_icon_114.png | Bin 0 -> 1943 bytes webapp/app_icon_144.png | Bin 0 -> 2592 bytes webapp/app_icon_16.png | Bin 0 -> 202 bytes webapp/app_icon_192.png | Bin 0 -> 8674 bytes webapp/app_icon_maskable.png | Bin 0 -> 7382 bytes webapp/icon_close.svg | 1 + webapp/icon_github.svg | 2 + webapp/icon_heart.svg | 1 + webapp/icon_settings.svg | 1 + webapp/logo.png | Bin 0 -> 7065 bytes webapp/style.css | 629 ++++++ webapp/thumbnail.jpg | Bin 0 -> 35037 bytes webapp/webapp.js | 137 ++ 86 files changed, 7587 insertions(+), 238 deletions(-) create mode 100644 .gitignore create mode 100644 index.js create mode 100644 index_template.html create mode 100644 legacy/.nojekyll create mode 100644 legacy/LICENSE create mode 100644 legacy/README.md create mode 100644 legacy/index.html rename {js => legacy/js}/MarcFile.js (100%) rename {js => legacy/js}/RomPatcher.js (98%) rename {js => legacy/js}/crc.js (100%) rename {js => legacy/js}/formats/aps_gba.js (100%) rename {js => legacy/js}/formats/aps_n64.js (100%) rename {js => legacy/js}/formats/bps.js (100%) rename {js => legacy/js}/formats/ips.js (100%) rename {js => legacy/js}/formats/pmsr.js (100%) rename {js => legacy/js}/formats/ppf.js (100%) rename {js => legacy/js}/formats/rup.js (100%) rename {js => legacy/js}/formats/ups.js (100%) rename {js => legacy/js}/formats/vcdiff.js (100%) rename {js => legacy/js}/formats/zip.js (100%) rename {js => legacy/js}/locale.js (100%) rename {js => legacy/js}/worker_apply.js (100%) rename {js => legacy/js}/worker_crc.js (100%) rename {js => legacy/js}/worker_create.js (100%) rename {js => legacy/js}/zip.js/inflate.js (100%) rename {js => legacy/js}/zip.js/z-worker.js (100%) rename {js => legacy/js}/zip.js/zip.js (100%) create mode 100644 legacy/manifest.json rename {style => legacy/style}/RomPatcher.css (100%) rename {style => legacy/style}/app_icon_114.png (100%) rename {style => legacy/style}/app_icon_144.png (100%) rename {style => legacy/style}/app_icon_16.png (100%) rename {style => legacy/style}/app_icon_192.png (100%) rename {style => legacy/style}/app_icon_maskable.png (100%) rename {style => legacy/style}/icon_close.svg (100%) rename {style => legacy/style}/icon_github.svg (100%) rename {style => legacy/style}/icon_heart.svg (100%) rename {style => legacy/style}/icon_settings.svg (100%) rename {style => legacy/style}/logo.png (100%) rename {style => legacy/style}/thumbnail.jpg (100%) create mode 100644 package.json create mode 100644 rom-patcher-js/RomPatcher.js create mode 100644 rom-patcher-js/RomPatcher.webapp.js create mode 100644 rom-patcher-js/RomPatcher.webworker.apply.js create mode 100644 rom-patcher-js/RomPatcher.webworker.crc.js create mode 100644 rom-patcher-js/RomPatcher.webworker.create.js create mode 100644 rom-patcher-js/assets/icon_alert_orange.svg create mode 100644 rom-patcher-js/assets/icon_check_circle_green.svg create mode 100644 rom-patcher-js/assets/icon_upload.svg create mode 100644 rom-patcher-js/assets/icon_x_circle_red.svg create mode 100644 rom-patcher-js/assets/powered_by_rom_patcher_js.png create mode 100644 rom-patcher-js/modules/BinFile.js create mode 100644 rom-patcher-js/modules/HashCalculator.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.aps_gba.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.aps_n64.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.bps.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.ips.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.pmsr.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.ppf.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.rup.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.ups.js create mode 100644 rom-patcher-js/modules/RomPatcher.format.vcdiff.js create mode 100644 rom-patcher-js/modules/zip.js/LICENSE create mode 100644 rom-patcher-js/modules/zip.js/inflate.js create mode 100644 rom-patcher-js/modules/zip.js/z-worker.js create mode 100644 rom-patcher-js/modules/zip.js/zip.min.js create mode 100644 rom-patcher-js/style.css create mode 100644 test.js create mode 100644 webapp/app_icon_114.png create mode 100644 webapp/app_icon_144.png create mode 100644 webapp/app_icon_16.png create mode 100644 webapp/app_icon_192.png create mode 100644 webapp/app_icon_maskable.png create mode 100644 webapp/icon_close.svg create mode 100644 webapp/icon_github.svg create mode 100644 webapp/icon_heart.svg create mode 100644 webapp/icon_settings.svg create mode 100644 webapp/logo.png create mode 100644 webapp/style.css create mode 100644 webapp/thumbnail.jpg create mode 100644 webapp/webapp.js 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 0000000000000000000000000000000000000000..3b7033a147467ddebbaa697060bb3ece06160e0b GIT binary patch literal 799 zcmV+)1K|9LP)glISr82rMrKi(re&7+p2O>V;CJEhxaxBV7a!Csod56q&v36N#pm}7LI~EOF6n+% zb7FbrA&iG-paxSs24(OY1n@JEQG=!bGe9`f(Wc_Ru!8`pDsDn_Ar}$D86Z^c{-DCG z$U*`x6QGoVmSH>U6yzrk1hXvt6_3M174AWao}GlxT7n+j#vGi1S3zpA8leLXFc#TP z5q!ofBwL*b+ylt>@Pe8B73EcpGX}pc$E1f>OJ1F996ykbk#=Pv(mu>+X(zZ6iQ z2(toCL@)u&u`EIij#}$)l%W{iPJl-!$7t+yDbkKpSfGHN{RDV`&*S`4Z z(d-Hpz`15SkfROh6l=mBEV53E9pF1QVujY+dG6EktGAxb)6H3E0vyI;wc4wJ&Du{u zwM$1#$6}FY0u!K_WGXbJ-7Y)cjw+duhMPJ~G^|x2c zkw}1r40HrPtfEhF%li8c!q9|cx}sm3P6XbBVOn!@2NdWSR_jf;#-$rx0<^oV^J$FI z&mI+fsS00j)R0aZF?#ewVGX8^z7 dKcYrFz(4L5(3SMW{JH=D002ovPDHLkV1hl;XzKs~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea24c1ac65792b77649a0d5e08db1c4b813e201c GIT binary patch literal 1943 zcma*o`#;kQ0|)SJk;G!9IWEa)%gn7ck;`6Tg&}hJ_C@wI<~reV3=c1rnp+sdBE?Fk zcEPp|X+0Ga%O!G~OQmzVyvilzHr3XMww=-O#PKrZ4?$_SzIq!P2t4#WyU{0`YrhN`bM34;-d|@;w!>1JAxr% zjphBb%jdf@n;)-#QgA7oD;v5OHaGuV9<=uTsP$wk`}`-+QSs86h`GewTKrhK^`mfc z#`+268L@+>Wv^^q?707inu8yKD-X6U9a6-F-1>516CV8NO#N}uxXXSFHz^Uf$`U{R z))UPh3l)dIAXP7bhZ6`b8IHh;dYUx8L_r38{Zej{ZWPtL74QBUe1a>++h}Nr zYLPmKq16o8ud$7jSay3|ki~6_b>f2Jk6cxtd0Q7{@V>zNRGo$1a!87Np5{{}8=p&9 zl8$o+{GAx?!O8Hq4nb`JmFMiA&Z253W{X*M*k0Khb1LCPK-7E`HR~BE<^86c(@cOj zSZ-Fh$8K)hJ)0yp(6vWW=-Mz_0f_i@vI6mxNaBF9@TxfGw8%}kHe6s^SsZ3ql>#P#SNWc?Q9%WxnciyQ zK8LL_2xXZ_eVbcDOz*Lq?<5Nv{emPnG);g-P362#i|M&LjZnHuyG`UvKSy_6YY%L2 z{|K$QVk_JupfR}GWN*t2&9Le%R$SftCJB*U7Nq`;JXi(~e%aixxJKly4PqR1g?^Ly zjgC>p9U+$GPTQ5At!TYQ{oNtvv(GO&x~UU%4J)Z1GOt=0n!i2Ipr4Lffrw%|RaOJ0 z_bDwk;)mRJJL3m`rwh~_XbIWHsWQ~p0!=jg} zMlH@y-?-ZW)rKFQ=38%y3-X79ZJV#!wink9oYH25w(bKF);%Gy|BAUD_TbcH6aQ|c zLZ9dQ`{e4u=*XeRdN?Lx;fQ?a$F}H~*(EHUw#90UXtFeRkir$0gI@0ET>09iUF2rr zXpASIYfR45)YZE!=xj`FAqC%k^fngPCk4G`I@lmoV~t!Gd;f}!D&4%cXy<+ju&iOP zDV%R=n~y!hFa2y{w#IZ)3UiJyV6>48*{lPE9*88!3Imy*+|s?&$pGf-x7aaXa*>TD z&cIYdG_3!g)_P<5miG*Ljnjc< zEpUGM0bp}|_%I=ma?ik_YS=PTGMp$_iPkpmOJ?no4Hedwdiqw3ip@=2zUb{3!$1oz zQaWkrzV{+N9D|G$vtT9`miO#9KLii;e8FI@H6Gol_!Z z@&S@(kH#D*k;gdX@+9b#G%B9RSNYVsoO+362&Qev$pt#FdKVATYqfCKv`(8upC#Cv zIINbz=c*TG^r_qcy5+g0_%Flyvd}XdFLQZ&?-;PXPxl2~e)7;7s;ulDFnqN;5qmvU zHlfRisI2o&_oqBy!|vISNXD|u=0*vLR{mg0ss20&wJ8qtZ zm+SLNIls6l*3U*$^l*RtZt$!(Xwqc$f-?qN$<<^d)!T!eC% zLH;1DpyES;-Crwy#ILK=I2&xT!JaqRSl!_Na6k@*S+?8E)^An;Z6IEZS@@LB0xlZ3zM%4eo5&PS9+RkB;a{ELfGGwc5c&5mLv6jbmpd!GQtf zkiz)xo3Dz(e~WR5{`U3%)=qW%XA42;tp-$j*3K(O_!IT=l`~>;N6BM29~xun&R>87 za$W4!=L;oSzs<2h4)SOU-Q|&~UheEICv6@BcwVut^_A2gpB6d_RlCI8tvFb(=r23x zys{BRjHzJ2PShidY*E!YlKAFTwfByIVzyJ_@P9b4Zwnf9Bd9C4*qQzJ;d&lFNosOE Gd+C3=%C49I literal 0 HcmV?d00001 diff --git a/webapp/app_icon_144.png b/webapp/app_icon_144.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a36296e4759218e66a8bc92e7623fc1c26ef7e GIT binary patch literal 2592 zcmajh`9Bkm9|!QEAyKI|=RP@_V+$*ro{0{2R#Fo35&!@|%EsEl>DN#B z&xs5FO8-+~O8~&Z85;{T%fTr;<;3IdzqV!4z%lcxRZr_2A4^u*=0eNH! zJJ-_qUYX>tUi@yl=d63b261C_DH3BzIHuft_9azB)*csEHjDRJlh5UGUfoTUVkHfb z9WhgQ;~k&HRLelW{uEb{rh>*M`#DqJDdIEXDZ^fhK(~KfF>jPx?q49_4nP6pUw(*O zHqi)dPrK?2Qr>Kz-uO&DF$^y;5Z%E__VoI`Mx=pIbs@EN02RFA*m_0Gf4d z%l*`G@9W%9RMzG_Rhe?(jmd>6wOqQ4T6+iFc&9+tuDK9HACvR@obs(hjst$<-LJIM zMuBLp*&gjd^xi=6;tts=n{#VjA_TEx6OT z;#~kQ#jh}LY3shPf9`g@z3!BGV<||D>Ej2P?PS4%ytMb(G4x=sysL^gkP zz_h1x6}v4ay{z?|S0g9F)xx%Z|FLFT3)xwRp&l5}cZ2@(rt~MssbTHl)U?HLo|TZT zWK`bT$%oyQpUBOhN;qJw(RY7>-fV2{E__esy0yMUrlT0er?}qV<{DScnG78X0|!{^ zTh!p%S)Frc`&(tuOf0L=Ry&A485>x|4DD)JluDy|a%ue)BE`ZAn03FoiFY5@4YFEB z%|VySIf)8FwkNEv1y~O$4gVp3Cc+g{JGdwgPyMqyF{P?8bkwn~WTmT{OsA+?;r84! zee?^{K2=`HG&v)B8j$tAe*QVp_hvifnaEz`x&h?%A%?o{v4nJ0mFLV^(;`Yywq|GA z+`KsCwKT&b7q05SuZX;6NN8{7XloqXx$NiGNd0&n%HS2O8mwVxl`MHqwFGa<=7uX_ zlCE5Ibdot?@goKuHu$b#{RN)?*xh*>QH~t3MH%nvY^SLD8pvNTdI6h$Mt@I%R zmlWTP3hVOzA(9@IS*_e~HOwu}*gulal&C!>+i@n!Y#;!PiyN2^!VY~M0bYU*qiwM6 zGXY5Qc66%%{ZCB!?Dw{iax+$y954_W|}iryU#f|iVEdpMKB^o8%sNa|~L#Pc->p_Q|5N<@!5ta`siHB?lN zK0SK4qIH7(x2LCmv<@PkoNZTF?pym8Uexu~QGF)%+(dv{s*=&8yv95iIm62yPLb37&Vy@?V}^4DF#=k2EwH8mSK-pNEi_r5yrRriy;c zRj(@(e`?l!hGo5UqYrVWJ0anM3nww*+xy*P&_A+v7#=*td zhFvc*F^3XL05!3M#nQE}BZZcs#h0pmh`VY-YyBH+6-F~rA$jvJtit2hgtqBqM|FhZ z!K`XFGF%X+&Y#@i#(Ee1S*>wJ!mbyWPM_gCUg&v~nS4lSt=8b>rmI2}DV{G`mrzr) z7+R)%KJFW+Y8zC2t@@}Q3JlrJNB*dLquY7mjL76l%vRW(e)j^v_)s|P!L0bS`(H^-H+= zB~zs7-u-&Xrq7`B=%3|8zS7xCO0S@JU$b37?cgue(I>~Q&5 zHK^#0sqp=d+l(w3blRB-4hn8)oS39J2VRDQb+RCbv*wmkz)nO^lU&YJ*TBF|ac9m5 zw?>5#GhJ}_`pEK``Vi4Ht86moG+DkMUof$PgP*~%C zJG@p0!9TjMG>xpw&~u3(K~JIla66fa<9&I;;A??S7kc1-u7q!viGiP6ScP8wU^wiI zNzMrF^u@WF$eDMXAFexbLBQ6D&?IzZ)qgB9Hs^ysRCz5qIUx5M>Nhl!y{IW2@e}O% zvz zdbQE^LW_2CT1U^Fy%~gNDCW?v^k+{U`>>)84_>xLuRHMWkoc|2PxbNwz4@8Q)*`jm z0Po1yoFxs|kPUdFy@`!7ck9qn6l1EQ1|ij$CYh^XZQ!&Hof88d0*TFBs3g z4B9}fieB3Hb9cA>#zD{6xc8+!9xXB&;NLcj!-NR|RGSs$TCE0`9_xE_UcQ*hzRv%% j(|^~N=r4wy`@Am#vx$EqXj-NGk{`eZZD&zsPPqFoN9p5A literal 0 HcmV?d00001 diff --git a/webapp/app_icon_16.png b/webapp/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..7261e98d3042089c393728c6df68afa3de27a8a5 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`6FprVLo7}&oovY0puofMTfz5F zlcC<4eb;ZsKX{7PLADc{iGTVQVYoy24c0cf#yJduGG@kNJhnc56B#8{98V zQ0ZzkxzzMSzJ_;4Ux&8cvbKiSTgBJ%)Mnmc_qz5@WPOL%bD-lGJYD@<);T3K0RZ45 BPVfK# literal 0 HcmV?d00001 diff --git a/webapp/app_icon_192.png b/webapp/app_icon_192.png new file mode 100644 index 0000000000000000000000000000000000000000..11726561c99201a2b15bd526976efd342dc310ff GIT binary patch literal 8674 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 KOI1pkg!~U@q=jVw literal 0 HcmV?d00001 diff --git a/webapp/app_icon_maskable.png b/webapp/app_icon_maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..60c74f138496b445feeee1b6fe3b994dc8671bd5 GIT binary patch literal 7382 zcmdsc)mv2G8}H08$P674Lr6CW5;B0aq?GU#kQ}5N1f*eT2?Y@)rMpqOOFAS31e8G$ zkQ`|kV9xe;cm9NPF81Cx>)Gph*89GnT5SV->#2^reOjF~b0r2hl_rPueZ-W(A zA`poAgXTk}|NOJ}K3v$)d|et^4J&_}F(URhLo>6;?3B)N%#=<^Q2%KZUATd*AfeJz zTPrB>@O!pD4!PlTC9b2k|O1CK~M_n$u6ttIZx9)3_SZrLZ9T`5-m^sQg z%HDjiDUE+Su%t+-_B$-*l8kadUl~Ff69Z)&e0DHQ zMdknT1tN&~wQ&79%1O6&%v}X=&>wq~1g##uvDlFene~Gy$7GNQe6XbFzAv4D3nS&_ zHCQYclA#AaTkS#@*3-WBPD8DBv0fD+P|%^|ZmU8?NC)&BgKQyZQNeDw*~(v3x?o+P zj;_(1Gmzp6s%PJfJo8;evqjerLK;*c9c&D`AWkHkuvr}TJ5i?6Y9;6%a_4TpQ454; zge-NbMRCu!EJA+Yy|+J0L<_Bi_}rC2C~ z_V|nRp~To7R4dPrH0WWKblS7ftah`sjl&_Ik&;85!0C z@u{L%|Mj&_*ot62Et-os;T9n^3Z3pMHb5AS4Y{Ee%axy%!>XLI_TzJRJdNghy-p#_ zZ|FieB$|}vO~GTkQoS!nRBkTRWw;azupC0aN_)JoB$T^iQ2gvrm5UJ}6^R%dk2VigJJbGxV1ySR%=}P_NCpM2LF;NHG(J~iu6J37gJ)_y zFbYoUm`yW{A6-n@QY-J49je5Dnt0v_i5eB};P3I@F(IKV=a`fF^IlQrIph3=s%YyB zzZ)YF7n+aS`GC z=g*?8ozvlCaM6GcXOsUB({fCFCu!_mVW8ks+gJZRJV*wGSU2@&XOL~4T41fNmCg(m zeDHcjR_@UaMGV=?m0fhaXy1~)n+-FK?z#9coeG6d^Df7dPv7?+EF7)PQ-u| zLK%0nUMQBkM-@IEQkI^9{I(U2O7TTS+Vq<6xh>IiDT;{N%dS5u#)e*y^(EzOhi(&f z;i(RncY2D6B=cW#`w`DK#-2s( zQ0yRkz@|L(CWv6|j(xH_4)lhOkM@Mp$PG$AsYJa%CG(eK@wS4i$KB}Q4gFsG3&n%$H7M<;BJozn zZ!TKSUe#Ml^>GL%>*YrJTCy{+^WI&auZ+XcHnT=`BTg0stG0f9nNo%pq6)hTKHGl0$eF4b)nBC% z^}OnYp9Dmw1kfS)dwh*)#~uYA9+2#GaHOwTF6r2l-31A@vfUPX7&towQ*fQV9-z4K zA*mryM@^3Inc9kVz~m*!YY1YgI{vyOt?4R5d}36eY_eA^lip4JPW$X$2kfml*?TGC zU&{)PjxoR%CiWAAh5j*D2LN)|Y8!}E%{ugk>~i(_`3_#VO;L87Fd0)P`V?4CLZWG}y9$M#Qoy_4&Pftg=b>oaDu8_)GOIdf{)_;A%LFKpf~zO)uJ2Nz!k zGk+T6)(9E}PDAzWBujOoC3yUWrxO_2w5zx^DF;mYxVqavw2S(7RNy7g+r!1J;_r61 z!zxrfkM{;ef(?!tdT=6{zB`h6G+?gAv%7rO>$RV9gIHVPh6yf2>X4ujpE(lc!ANkG zIN?mnv!dqU2rR(|+AOy{Y}hqR_CY6wJo3J2yUiO^#sil9!cxk%0N$GG@HAmGBb^1h z#X74=yNy%mA3^aXL7gjr|q-9rq(_p_W4h_3t z$q{$3&Pb=Xz#MWQtr)bv*`L>1upWow3!qz>WCDz+5EojcM+r}=wc$0TQL3{1W10na zW?W6bs9p$dNqXnY>7Fu5^u1;44-zcPOLSLvd~FkU4_t|I z-%2^h@2Yixs`?4k|L{O-Ru%2DCYgiHWFZS4il%MUJkq9ogC<-ir4!0(JNUtnMLQ(% z7u67&qDa<=sjOoDxAp*R?~c?R$Q6VgumSY>@lMlk7d2Jh_BVp)Z{%xVTQy|Eo`z-P zZ^NzwDQZGudmJ_#bDo(8Qr2{`vU&b$+DF(9z`jU(R!*vzd_uSe^0f{AS=4Eov?_2f z`15{Stmu2JbxuxF5moRhY8~E_fcpN&lU0FbL6J-$Fg|gr3mu^CzTk2%8}xQ7s-=!^ z?Nt>OXOFj-$wmiU&BytpDv!FrfQ~HYWnt4Wl~Z`gCVO4^4Hbg!6$6;T5}HO8ddg% zdfv>B;hjLY*omZOCR{qC&H4iDOmd%7q5wG2DA2!Y4Z0(CbBYjS#pibQemy14H|y^A zJ!e3AXmF-T3lfdAQ?U6TO(v6FvD4=f6rXYod=$p?OJ|0U4ZEAri^NjvHSx_rF0lOR z4&%3Mm=M;}Sh_sU_P5VVWcE`CR;3o^c6B#7(&&3}VrwSd@Gg;VwR61>9Iu}LZ_GGY zN{8aLdhDZYv})|IHav<;7fR;*hQ)1Er^4;Mn;Yy->CLW)c?hg_LKTC{{b`lj)Y#u@ zS6cGS&_Co(F#en|!;9HNA|5o&>o2};^Tb(B-{HX&95bkmF4T4K`oJSp5i1>blblpY z#Shvfsr0>!v$b7XS_}3FI@YvrQZa3!?((Q1yFjzfL;i^n3zD%8Wzhoh%jV(nI4ks^ z5t7l2EzKA`mLazO%V_|Png}AiIk?Hf_8bCb@VBy;80&>&W*{2XPA@AP4idl81izY=1EEtxH#j3m7B;JdJYt!rK(l6ls|Pf1T# zUacA08QUx5$`l9TlcZ$VXr= znNY6V5s7Q601+j>uZ}t!Rrn?US<)1moMhA^69zX)GB&&zjsC=5vs`s5@XF@zBM7Mq zSO$I-OtMe$x=-w$hya8aP4Zglu*m`ze5R87ok{H#sQ%R_h)rR%TL;IaC87c<&PNzrAWVqq!ry$q7YC*?wGvMZFa^WW*4^h4PpKBtK9ZA7lf_MIa@E4=L|I64k)?0m-YNmEtM zH6fwbZo{XFzT%QD9Lf+3?hAXczdbb@a1Uzpj$PQSeDYQt>0qpzhQ}qtjG16h+f3V! zSj|(#Y^yT5iSKytMUuQZNu{L_*v3;rQE7vMTounURrjnPv4y9F1D4%4Vy+(5@inaD z)QcFCMXh-y`H!ajH^g9>N%j+EM`e4lbi`CO!W z&b~Ioc7%t6G9V94*;s2M+`Q@oF9q%Cvrim)n?6d^wBHI`-z7}v)jc@v6sLn%r`fZE!H`#JCJepcn$d!YSW^CF+ zsM+qFe?j`=Z@8b)H|1OH_lU9A;tu?L9FOpuGPisknyW1=SX15R$C29Vc21+^0#QW% z_Wp{HIVs?{9(F1qPetRO!~~eir&3Ij2Xj_tx%vWfY^#3d+xnz_yY9fv&4?-1(4OqV zpV14#ReyhY%-A{b_>2pbMr9yUa`XH1*V9wd5mWV(c*GW`O^^*ZRq$FOFD44=yg)rz1S@aZ=(EguLZ3fz<79%)K^AtGJL znzbM$Y7`1`EKZ=II(9Mv(p!4-cZ3%9Y18*>+tH1-zrr;~eTgxkU#@oqic-F2E(Ht< z^xx-Xb=nm9lTxVwPSqeOl2b?Jh#4~Z9dV4mb?ECl(vN7otk&24T84vW=h-LpNpI)- z%W+JTiYh}LgJOD4m^6OW(`PGGni-4Rom>uNtu)E0oKJRceE{FPzPvbQOZ{pGsIVuw z){k?IxmaqW8q1*Of%<0E2xg+Sy%T-i1@5k4H^-Q|7v9aX(K@ehn-CMTv&t98sQo3b zOM)^y06#zg`c|3^93gd}m@VGDpgVL}F%#|=;2snMSZs~*D#D#acX>dE&%cXVX9)@t z{0*kIqH-cBI1VN48ltpZ9uyte6V?FgL7vVvw+Dl}m4(A^OCvUd*I?w#fmD z$H@+Rq(z+AI@&WWCj$H*I_H+F%&GX@Zqbj`U^b?nz{;~?p=e)PtjtNLxB$E{QhU5O z6n<9B>uUH_Fq7tohB@7@?ZE(|GHZ6Vlv z&qBE=tn94Q+2nkr8Pv>Mz3J0fa&1jr*<^T|{&pXJuaSQC`1G&UHrZdA66c>U0Cm$5 zC8M|7Qy8y^>xL!+qwWcVu;wf{@B0$0$3Y7mIZC zek56`m+5E!q|X97*3c?iI!u1uX6cf{>CAfK(h(_EMX2%2jq-5wkBj+95z8N)+#c_b z1~lG5y2idc_>2x4kv@^Un$3%EfKnk}QJ0V6_R~s(C4M7+v~Sx&&K!j_!Uxt-+tF8KQedQzP3 zOur>)kVEC$xn+0Sw~MaMTgweklUFTE;c;zn)Y~@(Pni}_Y9+7I;OD|I6@aB3DsBz{sp#0grGaK8Me;yDx!@ zWx_31G)M-jgL>`sZZR>Cxb!|7CequY>+L!sNH4m9niT$sM~udpG!+MVSrj1-|2mtH z7(3cBPA0;@DT}Y~+WpYh5H#xdE}-Uj*ult{kh6D#ssyVAKa}dB87(63Uq2Td|MnL3 zab)|yA6__Z0QhN$6Z+itVArQafCud%8hR+QVDzAlw)P1{DHp>&V2*nN#Ty-h4TAx< z510b{LaNU=X#Tg$T&{P7sioWtYJf!9-@8TYw-LUNP<3`_rJCdN8-V4}+x<4J201uO zM7#22C8kz*hmgwt0YUI?}ylkn{9V*B3*3QlI!Ie zDRz5F>lEHtW;=}U;!;#tv|o3j8(+gEz3K2~ZBe803=ISsToWEIhUTVG3&hU61Puc!SpRnpgOJ7%@K9u0ftP!7NmN` zT=tXkA)4q=)CYb*OPGicE1%(`@H+<>+=gj$(@O*%=XVlVYyu{xbG}8Hl^wG#NAy)i zG&1_=W$w zjhy=FzA_rNxW-lnW z*^eFWfYFS=6U+px6&nG48Gh!#@!ORJfN?gT1HGvI>p0A-Wo&lEcAI3(+UH&xjHWCw zJ}Uk;`8=G3sqXP+pnRW%4M`-PL4!zP&Nr*j9V|mJnJ`i(Y(aQoE6gm(s#M+z190^x zj;M$5Q;?-e617?*NV6*Qbp6wF>#PeIS8*#MR_4l$6WLIC6A0Afn*3~y!WxKmPVbmF zXw?dT3!*_YiRa@SyoxJ-Q}JHPx6D+6zRHm#4=l~KaW>Phq{A_vc*9TZoR)cC8>n@H zuKVB`atmdM)R~!gnWeRM!8w7Wm1(`Kq35@?{>WxB(c4(9X1`IiIx9@ldIG%~aLPREanOtTWqy3Bet|82jF=rFf%hyWcWma_>zCX&M@vhPYtq51 z1j%YFa8#jRg3e+wdfjE+_>~9Oa8GcRg8;x4!KBbQWbp)`kGqyZB$14nY*it~ za(X>*QE5Z=4vUft7F>I~2IxIhk=i8^8CK8Ji2$rZS8bP)vkrlneAi6%bBW{S5SxM*U3=JLm^fqZ`A0i_0A%b6NA`m|ZE%aa4ekmdJAJI%p$d z6XrJxbI5c0()OrsGVIaOk{ld0nM(nL7vC3d$q8ltek+-2Va2He@d1G8czI9wc$r{v zQzS-O&{+7@z6z+-wz`IH=lV{(hvU9c#Fs))YXC~z8<>_I+)2e$C5bC>Sn@y$=rI!f zo`3d@so$OaWk0H$BYv^mit9*aKi0A{xR4OU#+%d(oHG6bYs1@n^TVC*$*bwoup)%k zNx-5XGf2T}sZ7^pkOYopPSlCPE}6HSvMQUdb3EV`{O1tnv3?IEW2-?Hxk_M#3tE^* zT6t-Ukq1=4&Pd(#ZO&Ca5D)j5{J!%j@uG@3YX6FA0&wbd28w)@hDdLCsN}%OuPBGB+N<^$y)4pSq(p$ynKZ6h>XTmE+)RAOt#fGt*uzb zt@8eHFk|8tJvEz>i372L3+J_EB_j~?8~k+!GUYn$@krd4$Nt@ybiJ$loL6;ZH6?=j zla^?6OR)8xJr*dW(7%Ex-_*4o7yyBx1|W20$J=Yk95m1m*PtgUiQ10Q;-7cU^1Yy__cmpx3YMK|vLuYLAGJ9ZSz%uZCe3 z5LXY#+QO-h{;tYl%{4aI|5gsoJ5N?=@X+8HY9cGYH5O?JggjYvQ2~=#b=4+B%S(Q2 zRRKivb}p#U#=OdThqCU-H>Cf+UYh;iSAP?Df>t*eGGSys6}YbiX{zcztWicq{2%eT ByZZnD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..51f320627e3f619afd864afe1fac216e64ef2da7 GIT binary patch literal 7065 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ce99cfebb89f837b13417638b91d90a59ff380ab GIT binary patch literal 35037 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 i!SpJq1RfJUkx}@Hb(zL9b-(NQ{c0p;%aJ$X> z!%UEGhTLM26YuF6VmHE2+=R#bvVU%?E%P&aCqn@16R$1Ic|i~j%VT>dtSvQSu# z61J>ocYVI;(^DVg-322aUC)`qtLO9lbd_Q+sCe@Ak8sON$OQ@u1G5)rvDN>bGVp&hNPG2IUp9UwhtPLC^_3 z%mDVmG*6tZ^g~mam*$pB%FI`Or&B8#O#JUMYXoJa3O59AO0qaWOFk*}RW^a6H{{*wr3f2v?5~~PdW%n7?Z|f-1zm{Z?Z&#-2Pw^^BIO9pAhZwZFPeEKHrPJjxKj3fOlzkE3lggc=-0yUAu^f zFF^uuVmkCDFOEB9TR!m;=bM$E%w@?VN}VS|B6*(tfXIK`3>`_4w8o9Op${@x?8QBo z8D31?_G6F7txm2Cb}Z9(9FNX%FHJ5{oX%8RsO@f{xH!{)p6+>f7`kbd-3vt9A;+OA`gV`+52IoN(8x(mkk9EBf>zz z7^!yfg86`T=iO)Nhkh$pwsgslA%orbf|zN;>6Z^8YAaUh%96 zj}vzzt6e~s*}41Msb&V%*>~S)AM}!rkWbRCv}TQor%M#fT(0c}%cE!T7tna<4LI#^ zX7IS3E&NB{is|d-r*bJL-{if;id{R#6PG9_BK(A;LG|6(9Giz`b@aZXpbSYyX-XAq zyiEiHt9Guj5Z!m@O>Wa_Z)q8H6=bEL70HS5rS6>W(^4Zhtzw21B3|s!l{G&=GRT@# z%-_*bxyJT(xk80O@ej%MN(F-xc1X<_t5m~Q1Kwqqrsio@W~O?htE(S)dPA6Lrd~;3 ztvvI;)n z{--4+WayM#?uuhMS$edCFFDA8ai&DaVNE%mrG0ys{q=KLA(OOoG;hr^CsPaH*lbZ= zRQ+BH-Aeyelpotr^pDm{9@61GvI2|!_Py)5ygvp zmS8ke4D`9_(t62T#f28Jz{3x-kli<-Y4vc0V_QLqUx{SMu9(Cu&|U3mlh^jF)Nj_( zETV$#^%f{Q-7Q|8G`!f_$(gH%G&+X7`ttJDlmC3FK&t1M^?jpLd^^y&Mxp=>o>)2E*c`U!H=2Wx+2`y|mQGzhcJGwd z-BV%@$jqQVY!X7!2UsBB24N#^9g|hpI%apmQ>z!o$~LjI7S=QJPT#$4>dmsuO$}%* zUp2u&t}Y)wGev5Q(t{X+DOjy{#bkGkRCorp5lS}=sU0%1=aa-rCa$K&#hG8y*}1E1 zzD)&#_dItXX$qkX3f(BRBJhV^^dp$`4EqOUjz-I1PoDXngIPbFfLfQz%O5H(Rb28q z5V*@!eG}Uz3#_;jaNrQP!qF5TF+4FFAqI5937=>!W6ay9dhyTFzSLgpO#J+9|Eg}I z6URZ(ON-z;*x`8?u)>T-;eA%WHrGU&LF6|IBF8fh?Q*S6jM5pAc7|0vfp$_5iUQ}r zS^TR7#ei#i=;P-)q3+=PvjB}oH?fomKD_>GN}vcQBe0b_oPjHORwja_eKwJ?c8s~= zC&ma2%lXmaOpTQCK@8|E(jZ{Bm5j7NWW%yJjUic_4rIe-cp=3fTaO!Ybnx;j^N%AC zZoerWAZR+D2>i)oJX3lON6ul*OqZq~8XKU+iMWyA2#vP!K$ZtmI<>6(HRhmaiH|u} zVjddp@;z>%eVL&FGl*28nu3wo=m|<+1dKGK95kjk+qmf?ZY6_`aFf;>l*_7}*GBT6 z_b`u;qcW|S&0ZO?vMj^2%r2g}l z2E8?~LOf9^I;JIlA0L}rk7YuM3<&Nf2xZWRXMSP*Fg4O9(wU-ykXdK7o9-zh^;t9} zABow>-u9wlI!)xHS z!v_WTS*yzkBTRQ1r$mM)v~Rs+ZSOZ0>W_WrUdC2Uy-(E!<6=4KRctP1h#y_;Og?ph z@+r&rH+imEsdYcfPH)<5@I?&b7Iiz!VdJS*Iw&b6Rt-iKoIoZ4De}9b)zFTCqoF5G z*;8g7KT>)Wz2~iG-Rn9q&(le>`7PO0uhsrkLVVEps}k&Mooe%gXm`<|z8M z170N`j;?7LSDzz~1%Z5c%N7xsQGw=VnO5#7&BK{$0{bzQue?JBv-c3=3L?aS! zm7e-{%BR*)d|Ab+h~ntH7cM_Q4=GN$4>b?8;~n}ux7da~AIsJ&zV<;Id+m(NO3#+z z$*yP;23Y0$-SmWdhK}K7)0D9OL}iOnqT%Gek*vK%i?a(|fy}UW@Kq3Tw*H%9>@I|o%UlK0V z_|iZl`gdxj53oIfkc1LtI$B??htjW!3BzGSmiE@9(dVRWX9FSq2GoSLGl$iW1RIN} zsIv5H2`Qwr;BsAq5+O?YlwhKl5Gix;$0uYKLPEIYLtwt;@NCebg!>P3kmTy=xF*;T zd!~B`7GHeXU0*7ulIpnu_HlwhlZOD~4hpIW9%GUsCKBPOpNqA$7?0)FF`ItVmWYq+ zW)1hP$Jq5)zAs&*W6i-pZ=ervHqT$pz^Bp~G@7CH}uw^|Dtzyh?3>vz~$LrBr zY=-7ivq&dm3J^K-u!1rY+nlw5lTs<3gKa2N{d2{>w^Y_Ao@}|)TYfmY;P;Gcm>e8A z36WM1M2bn1CQbxy^N|sQp%&@I?#(lkHqgw{qf3&nTXN(X#s&)gDmzr~3k}e{J(m-C zFER)*$`$G8nM#=*wbvzEWD^tAZ+Ln1gU};K&5q7QiL2QO!!B;!yQiK8lv8e3+X(i9 zH4|loz*oc#W)hlgt2VvQaYla%XGCQ({+J&wuhJvFbe0<98Y8ES|6AK{Lm>VkHh~XL` z_IK9j?7^Fd6vom;G+Ry9?|OVF(z;3FdKDch=<~D^I*7<;Rr#H|fTzS$d}UBd^D+5i zja3WEYGB>jPSQcB`cc8H*9;6-XEuhEz^ahEmNR~52t9-p`?xl7*Atqt*sI`jZY*fk|i%Mq=;Cbj+e5;PwXKbCAGp6`Kp(o?c%n@J%QI0 z^W1mf*=|{C~#E8V0O}$)K?*jtn6oavmQocx-9fmfD3SlW-(B>rw4ieOpO2=rk965(HRdI zaW&60eogAFdp#~vy3#j4_uP@y*Vx@yVfMHc^*^$c|Gk4HIRi8h4A?dxWm37FLVGnO zHU*jcA9fK8Ns=_ZniMWNvt%jEF$>w7u5kvt11FGR~ zp8#=kF~gx3N-1!BRi_$4jm~ghk8(XAows#wK-Do(@@?BVr#WNo$yX>4hcS=kadUfmzUVc+1-b+KP!^p29c`os;?;84d^h!K10)F3~RH~?f z2a|o&&tP*pMNF3?xO(PnB?mZE$|D>vntQ~F30zGk^Ziz$qCR+ikC?Fy!oj|xW%V7+ zBq`Jnw;I{sQ<3o=`(;{{p^DUp%S&gKK>oA*MW+NO3Cam$H!O|rYi?irn9GG*_SQf* z_<_-+Oc0E^$`8CEEw*jV&weiqe=pJqoAg#eSG{hc?_<%uqjQb%=beN8N)t#9rvXp` z`oLc_;Mv>+)zt7ukfBHivT{czESJ-^`O<}%cf1s| zLSW2|BNr5ywUG_pwBtA7P~lp-Po>eFwaD@%Hxu{NXWDk6B46C^AI+JT=`cPCT-VnW z`dppMT!k1FjWnSjMvZ~K^1iRZ4dRc(tTMhhtnv0 zBAHw5Lq|<5rECO^kOG>#*58%akSY}5txu--qu^aEj7X+Jx&ezqQ&6l3GyJ<~5&t4Q zXLfkWwkemi^x1CCYQffR#=`Wxv#+2MsDh+o}&#KP72Hyg)Js0Vx<*cN##YDjJ`DVRwfd%mPZ`+|9 z__DLg6?q5lVi0NfkW#4h{+eOsfSBFcGK{9TdIape8xTp<8|-0U{*r7ma9mE4VB92oXYq4;SXveMK6U+=G-1N()|oyWZ9u6MIdtQ^&;c zye`f0NuG=L(IDscU`naGT#GVU36C7xq&iFh`M$v~7ifJ$x5F4J$Dcll_rLLoT@`oC zcu!2q0BqxeSJq%a_KpNKoWeVX4Gv`w@}t_rGgkEN=q)l$Gt|)!l0R@-&6(d`*Oh=W zv%cKO&>^6QjVOZ{(MXF(4R>g|y|Isk`+Re1z+jV;!FiAIrVqzLMr2Fg9W$0U&v+7= z*iz|*NAkh-ZiCD9lwv{0cWbiIE))!y{XFdbNuz0(ZE3|M#ylTRcC$v^iF~)xbM7Lp z38@JeBf1h`0KnBxY9EHI-~<=eRm{4Y6@u!*13syUO297B99zCIRrl~1$AwGUa3kO| zPnQ9VGKdq=AkbRX=6qGm^*XU>oSPf659e}l=c$XEI$cAj!gT#j`88}xOe5$9P4y^g znB1uC%7YYF6hS`0p>lm&Ar6EVVjjg}0S5 zOV+waOsOtua4EQyH8=76x7~{1 ztCukN+Q-g5HEP$7*lu(tcS+jEx)~L(_c_t&rk9@5z8ZxV0%7^fqmLaZJ_OLuUm=U> z0|WIWuVJPF-=@iJZ()4>kI5gt29O#PCE=fE&Auom8L&Q^LFsBx%)!(HGX|mbZOe}Q zs40;#iL`*56p`?&{_@lDHQ(L1L$p7eJ|1*T)*~Q>af9?j95=~rP%&$&hr96_YxHQ@ zkJ>7Z`~{67k;3$e8LQ{}y#C!2s`)lqUgsd$VD$B3UL~Gl2_Zm}G2+4qiH>5rZxcW9fG%eg1izW#%CqgLi+Dl zkiu?9TEVNZOc-)BG+%*Zrq0!?W}~h4v1n%Th%ZBMro=nH8lmnJ;_%~iA73ejf#E?C zViZiDM^!09{W?JI1Q0_>MYxt(Q*Gg)V|J4r?bQa1jW#7ZO2HRDX}XJ>=ilKPsFi=* z!};+M`W(o_z{HPrd2~#QDSat{8{)T1JL4_M26GMd1z((W-yiZonICSwP4ette~+#a zu7!}|Nhip*SZEe3S3#`6nQWLQg%p5a(VS+3d3q8}O}EphHD4i9TGrSSg0J7c;2u6l zX{MZoCSgEMnuyX!SdGZ1LxS?twjLW++pLlNoFBKD;V)uP)%8)0YU4leM8%YWk0QSb z&HP;=B@a%XUG2~6m~~u@a5SA30ZDNN2;0>0iIUdlgzFhg%Z_RsCNg}K%40%3QPuG} zq#Y284N93~r0>myAH9S(fVMr074{vbN!m#lgX)xa*1W?jk1Q4%YkkWpO8#it(Z!ro zhf|1zQ&O=wax8|7`~d|c1aa>@Wt&OBk}5C(V5)T71DUHTOEM7l&t{whS6^&1zmhfW z4dP?{WKWN$<6IeoWm&vYAm(g0#^-zCW$iu49NkCOyjJ-iYHQhYX|@XGb0%8M4-l&| zzt{YPYBrV-AY1PD%NA8){ZC==gvT`;zD-&k>aTy>iW3yPOuwUgBJ@hnn_FKqv7uwD zJU=Xfm7vFzI(UsB{msoQZa<)z(XC=U7DXl^AMyrh%tNf@o+?g8Hl`PDAjk=*>_}UX z_x6N5OJFQLNJdbQ1lx8HJL-s?^*Qyaht-WMY#}#Z6Je~uZx6X@F2cHhUhu-kFjNS1 zFgL29vf-JO$KWx0u7qA4f!a+tO)1q>*}n66X|08}lGO3$OB;yYO+Q|;XWC9Glh%E& zv`N7epkm;n@kXk;QMG}$?H{B)jdo$*t8_`!svD(K_O+EM)<>ai*6Fh;HyiC=e$r$7 znCBT70!s-H2>(fm^(^-+@NOC>&b-+6CtxJ+$~EZ}N9l0OTGEX?N=|lM^;GXZ*EI?x zOyYsKI z2njKu5mabI4!5J_-7-00Bw{jvt}sAb-`K`o;VhP_=i(9Z_ULx7I~39EwbIH*D0TFM z2uDiK*Z>`55R%0l%3I@31y_O5-5xgW)Yuk3J#DH7MQeCUwvlcJ#ZR$S+;6{<;Ekpw zsXXRGK_x^Qw|z$5v-shUZ?Y!@7;G=CZ#Z_kpJ(N1@LSuK*1V&&8*)$Z3PL8*V;xoiuFlyr zpgzzA6s3@2cEzB4abvdVB>BP%oVWCT-dVAtty4~!&AdU&(~Kabg>L{=00sAWrplcs zV>TcO2L)`^#`jij$lvTHll&$C^%^sJ9WBThsKtvo+xuLz>O2RuhW zfyl!lNKWXp7P|fdX|^f@MeNv#vk6Uw>CWvp8&MZUh5h20x^D6hy;CbT9Ju**jC&XThB4v>=#J?rM}!0@X@vyUAwdYr_!mqCI(1EC&RqPb`&Onx zQ|$Ia12w$IoN4Q<&j{W-W<>-(`{YZBuu=VV>3oKNY`;nO;mxgzaI~DRO_`TzX=zFl zlP3nA`jj{=?HFtPjcrE?o^vo)vm+gbZ4WLC;cH2`b;Cf@JS!oQLFE}ZXUydOGyp?D zD=pZ=pN$Q6xq+QB(Y-M)3f9~?Zm8QVBTdrq-BYC)o!?$Aho&4mb$wq8&zBt@Q@lV? z2LWQ_D}x31$o;8e@JMTT+?bW;X3G;b0jt2=7DxV!FD+6&Li?p*erF?t;YHoBS&U4R zr)#c`MpU?{&=xOXV7 z_;!d2#S^RY5GeP8>CEo=NO+i7EsVJbKIGNID!z0|bh`E2pWiw($&ERAHKufsGELE2 zg;9o}gB54;0zL(U-6?#2O!B^wjC-_CjTf3rHYpU%njv^X@FsWM^Q98w*;5ZPq$!7> z9scP^F(E93jScT$mBWVG8ui>}BCS)66bj0{56@ndIryeAmZsT*v^e@mcbmsMTGfu~ z6=}DQA}51t7EAXsVf8H&N=>Uq!tMo*olOObPhYp)>WFiz)z>_deL7Tj%R@4;_#7ix z?~kE4tfDAb#ZTa+Y7a;*?d6mrTYZN=+E4z{PCIJ#91jzFtKX6;JWqxe}c7%qOnP^%r|~&zrap zOnqjb0D!@|Kgv%3RciFl>dIerE$^Ie1Rgx62A%l+i{|_(Q}+K|FtU@G+)kby!R06{ zCA`|q$guIFAoQBY8*3DGb&49}M#o!qc_s}PkmdP&yD=X<1yuO**2mYNB5hS<=1FHf? zvhdkK6%A5Zsk@_dNu=GJPWf80m6ovAHPqz3ON*odzdL)+S;0%8fSiDs!rvPc?Tn#( z1Xm&|H+*Mx=Q`Zctg)_s0wcNw(H~!VooF?$nWecg^3W;6ctYF3v{zjO;d<%@@$Z~w z%lqhCz$ic_`xJziuYx4?pbQxs7@m+9kbgl$Mcz>~k?XRN@5TtfyVG7;t7^P`oVcld z@SlVJYn7`1lggHEAo06{(Pf<<0$}&~>c7){{_Ao4)gG|C|5xyWzkWw;uc?UdW9U#l%#i8nE12`GG^)aC!a0h%kueDUE%2&R6&Uv0~k`7cfSzcg2WG0%Up zq5kECQujdj^%-4JRY`qR#qsw4#u4fT_LtU+k)A;)oW>hP)SLT~>+KDz&~-~Z!3gZ0_D{|8oNP89$E literal 0 HcmV?d00001 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'; + } +}); +