/* * 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-2025 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: - allow multiple instances of RomPatcherWeb? - switch to ES6 classes and modules? */ const ROM_PATCHER_JS_PATH = './rom-patcher-js/'; const RomPatcherWeb = (function () { const SCRIPT_DEPENDENCIES = [ 'modules/BinFile.js', 'modules/HashCalculator.js', 'modules/RomPatcher.format.ips.js', 'modules/RomPatcher.format.ups.js', 'modules/RomPatcher.format.aps_n64.js', 'modules/RomPatcher.format.aps_gba.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', 'modules/zip.js/zip.min.js', 'RomPatcher.js' ]; 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, requireValidation: false, allowDropFiles: false, oninitialize: null, onloadrom: null, onvalidaterom: null, onloadpatch: null, onpatch: null }; var romFile, patch; const isBrowserSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); /* Safari userAgent does not include word Chrome, Chrome includes both! */ const isBrowserMobile = /Mobile(\/\S+)? /.test(navigator.userAgent); /* embeded patches */ var currentEmbededPatches = 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; } parsedPatch.optional = !!embededPatchInfo.optional; if (typeof embededPatchInfo.outputName === 'string') { parsedPatch.outputName = embededPatchInfo.outputName; } if (typeof embededPatchInfo.outputExtension === 'string') { parsedPatch.outputExtension = embededPatchInfo.outputExtension; } if (typeof embededPatchInfo.inputMd5 !== 'undefined') { if (!Array.isArray(embededPatchInfo.inputMd5)) embededPatchInfo.inputMd5 = [embededPatchInfo.inputMd5]; const validMd5s = embededPatchInfo.inputMd5.reduce(function (acc, md5) { if (typeof md5 === 'string' && /^(0x)?[0-9a-fA-F]{32}$/i.test(md5.trim())) { acc.push(md5.trim().toLowerCase()); } else { console.warn('Rom Patcher JS: inputMd5 must be a string (32 characters long, characters allowed: 0-9, a-f)'); } return acc; }, []); if (validMd5s.length) { parsedPatch.inputValidation = { 'type': 'MD5', 'value': validMd5s }; } else { console.warn('Rom Patcher JS: invalid inputMd5 for embeded patch', embededPatchInfo.inputMd5); } } if (typeof embededPatchInfo.inputCrc32 !== 'undefined') { if (!parsedPatch.inputValidation) { if (!Array.isArray(embededPatchInfo.inputCrc32)) embededPatchInfo.inputCrc32 = [embededPatchInfo.inputCrc32]; const validCrcs = embededPatchInfo.inputCrc32.reduce(function (acc, crc32) { if (typeof crc32 === 'string' && /^(0x)?[0-9a-fA-F]{8}$/i.test(crc32.trim())) { acc.push(parseInt(crc32.trim().replace('0x', ''), 16)); } else if (typeof crc32 === 'number') { acc.push((crc32 >>> 0) & 0xffffffff); } else { console.warn('Rom Patcher JS: invalid inputCrc32 value'); } return acc; }, []); if (validCrcs.length) { parsedPatch.inputValidation = { 'type': 'CRC32', 'value': validCrcs }; } else { console.warn('Rom Patcher JS: invalid inputCrc32 for embeded patch', embededPatchInfo.inputCrc32); } } else { console.warn('Rom Patcher JS: a valid inputMd5 was provided, inputCrc32 will be ignored', embededPatchInfo); } } return parsedPatch; } var isFetching = false; const _fetchPatchFile = function (embededPatchInfo) { if (isFetching) throw new Error('Rom Patcher JS: already fetching another file'); isFetching = true; htmlElements.disableAll(); const spinnerHtml = ''; htmlElements.hide('select-patch'); htmlElements.empty('select-patch'); htmlElements.removeClass('select-patch', 'single'); htmlElements.removeClass('select-patch', 'multiple'); htmlElements.setEnabled('select-patch', false); htmlElements.empty('container-optional-patches'); htmlElements.setText('span-loading-embeded-patch', _('Downloading...') + ' ' + spinnerHtml); htmlElements.show('span-loading-embeded-patch'); fetch(decodeURI(embededPatchInfo.file)) .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)) { currentEmbededPatches = embededPatchInfo.patches.map((embededPatchInfo) => _parseEmbededPatchInfo(embededPatchInfo)); } else { console.warn('Rom Patcher JS: Invalid patches object for embeded patch', embededPatchInfo); } } else { currentEmbededPatches = [_parseEmbededPatchInfo(embededPatchInfo)]; } htmlElements.setText('span-loading-embeded-patch', _('Unzipping...') + ' ' + spinnerHtml); ZIPManager.unzipEmbededPatches(arrayBuffer, currentEmbededPatches); } else { const parsedPatch = _parseEmbededPatchInfo(embededPatchInfo); currentEmbededPatches = [parsedPatch]; const option = document.createElement('option'); option.innerHTML = parsedPatch.name; htmlElements.get('select-patch').appendChild(option); htmlElements.setEnabled('select-patch', false); htmlElements.addClass('select-patch', 'single'); htmlElements.hide('span-loading-embeded-patch'); htmlElements.show('select-patch'); fetchedFile.fileName = embededPatchInfo.file; RomPatcherWeb.providePatchFile(fetchedFile); } isFetching = false; }) .catch(function (evt) { isFetching = false; _setToastError((_('Error downloading %s') + '
' + evt.message).replace('%s', embededPatchInfo.file.replace(/^.*[\/\\]/g, ''))); }); }; const _getEmbededPatchInfo = function (fileName) { if (currentEmbededPatches) return currentEmbededPatches.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, applyButtonStatus) { htmlElements.setEnabled('input-file-rom', status); htmlElements.setEnabled('input-file-patch', status); if (htmlElements.get('select-patch')) { htmlElements.setEnabled('select-patch', htmlElements.get('select-patch').children.length > 1 ? status : false); } htmlElements.setEnabled('checkbox-alter-header', status); if (romFile && patch && status) { if (settings.requireValidation && typeof applyButtonStatus !== 'undefined') status = !!applyButtonStatus; htmlElements.setEnabled('button-apply', status); } else { htmlElements.setEnabled('button-apply', false); } }; const _setInputFileSpinner = function (inputFileId, status) { const elementId = currentEmbededPatches && inputFileId === 'patch' ? ('select-' + 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) { if (elementId === 'select-patch') { htmlInputFile.parentElement.insertBefore(spinner, htmlElements.get('span-loading-embeded-patch')); } else { 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 (!isBrowserSafari && document.getElementById('rom-patcher-input-file-' + id)) { /* safari does not show fake file name: https://pqina.nl/blog/set-value-to-file-input/#but-safari */ 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-input-file-' + 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; }, empty: function (id) { if (document.getElementById('rom-patcher-' + id)) document.getElementById('rom-patcher-' + id).innerHTML = ''; }, 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) { var patchedRom = new BinFile(event.data.patchedRomU8Array.buffer); patchedRom.fileName = event.data.patchedRomFileName; if (currentEmbededPatches) { const optionalPatches = currentEmbededPatches.filter((embededPatchInfo) => embededPatchInfo.optional); if (optionalPatches.length) { const originalFileName = patchedRom.fileName; for (var i = 0; i < optionalPatches.length; i++) { /* could be improved by using webWorkerApply to apply optional patches */ if (optionalPatches[i].checkbox.checked) patchedRom = RomPatcher.applyPatch(patchedRom, optionalPatches[i].parsedPatch, { requireValidation: false, fixChecksum: true }); } patchedRom.fileName = originalFileName; } } 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'); } validRom = RomPatcherWeb.validateCurrentRom(event.data.checksumStartOffset); _setElementsStatus(true, validRom); }; webWorkerCrc.onerror = event => { // listen for events from the worker _setToastError('webWorkerCrc error: ' + event.message); }; 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; } const _getScriptPath = function () { const currentScripts = document.querySelectorAll('script'); var scriptPath; if (document.currentScript) { scriptPath = document.currentScript.src; } else { for (var i = 0; i < currentScripts.length; i++) { if (currentScripts[i].src.indexOf('RomPatcher.webapp.js') !== -1) { scriptPath = currentScripts[i].src; break; } } if (!scriptPath) scriptPath = './rom-patcher-js/'; } return scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1); } const _getMissingDependencies = function () { const currentScripts = document.querySelectorAll('script'); const scriptPath = _getScriptPath(); var missingDependencies = []; for (var i = 0; i < SCRIPT_DEPENDENCIES.length && !isLoaded; i++) { var isLoaded = false; for (var j = 0; j < currentScripts.length; j++) { if (currentScripts[j].src === scriptPath + SCRIPT_DEPENDENCIES[i]) isLoaded = true; } if (!isLoaded) missingDependencies.push(scriptPath + SCRIPT_DEPENDENCIES[i]); } return missingDependencies; } const _checkEmbededPatchParameter = function (embededPatchInfo) { if (embededPatchInfo) { if (typeof embededPatchInfo === 'string') embededPatchInfo = { file: embededPatchInfo }; if (typeof embededPatchInfo !== 'object') throw new Error('Rom Patcher JS: invalid embeded patch parameter'); else if (typeof embededPatchInfo.file !== 'string') throw new Error('Rom Patcher JS: embeded patch missing file property'); return embededPatchInfo; } return false; } const _dragEventContainsFiles = function (evt) { if (evt.dataTransfer.types) { for (var i = 0; i < evt.dataTransfer.types.length; i++) { if (evt.dataTransfer.types[i] === 'Files') return true; } } return false; } const _initialize = function (newSettings, embededPatchInfo) { /* embeded patches */ var validEmbededPatch = _checkEmbededPatchParameter(embededPatchInfo); if (newSettings && typeof newSettings.file === 'string') { console.warn('Rom Patcher JS: embeded patch info was provided in settings and will be ignored, must be passed as second parameter'); } /* check if Rom Patcher JS core is available */ if (typeof RomPatcher !== 'object') { throw new Error('Rom Patcher JS: core not found'); } /* check if zip-js web worker is available */ if (typeof zip !== 'object' || typeof zip.useWebWorkers !== 'boolean') { console.error('Rom Patcher JS: zip.js web worker not found'); throw new Error('Rom Patcher JS: zip.js web worker not found'); } zip.useWebWorkers = true; zip.workerScriptsPath = ROM_PATCHER_JS_PATH + 'modules/zip.js/'; /* 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) { if (this.files && this.files.length) { htmlElements.disableAll(); new BinFile(this, RomPatcherWeb.provideRomFile); } else if (romFile) { /* Webkit browsers trigger the change event when user cancels file selection and resets the input file value */ /* since we keep a cached copy of ROM file as a BinFile, we do not lose data but the input text, so we try to set it back */ /* Firefox keeps the previously selected file and does not trigger the change event */ htmlElements.setFakeFile('rom', romFile.fileName); } }); if (!isBrowserSafari) htmlInputFileRom.classList.add('no-file-selector-button'); } else { console.error('Rom Patcher JS: input#rom-patcher-input-file-rom[type=file] not found'); throw new Error('Rom Patcher JS: input#rom-patcher-input-file-rom[type=file] not found'); } if (validEmbededPatch) { const htmlSelectPatch = htmlElements.get('select-patch'); if (htmlSelectPatch && htmlSelectPatch.tagName === 'SELECT') { htmlSelectPatch.addEventListener('change', function (evt) { const zippedEntryIndex = parseInt(this.value); this._unzipSelectedPatch(zippedEntryIndex); }); } else { console.error('Rom Patcher JS: select#rom-patcher-select-patch not found'); throw new Error('Rom Patcher JS: select#rom-patcher-select-patch not found'); } const loadingSpan = document.createElement('span'); loadingSpan.id = 'rom-patcher-span-loading-embeded-patch'; loadingSpan.style.display = 'none'; htmlSelectPatch.parentElement.appendChild(loadingSpan); const containerOptionalPatches = document.createElement('div'); containerOptionalPatches.id = 'rom-patcher-container-optional-patches'; containerOptionalPatches.style.display = 'none'; htmlSelectPatch.parentElement.appendChild(containerOptionalPatches); } else { const htmlInputFilePatch = htmlElements.get('input-file-patch'); if (htmlInputFilePatch && htmlInputFilePatch.tagName === 'INPUT' && htmlInputFilePatch.type === 'file') { htmlInputFilePatch.addEventListener('change', function (evt) { if (this.files && this.files.length) { htmlElements.disableAll(); new BinFile(this, RomPatcherWeb.providePatchFile); } else if (patch && patch._originalPatchFile) { /* Webkit browsers trigger the change event when user cancels file selection and resets the input file value */ /* since we keep a cached copy of patch file as a BinFile, we do not lose data but the input text, so we try to set it back */ /* Firefox keeps the previously selected file and does not trigger the change event */ htmlElements.setFakeFile('patch', patch._originalPatchFile.fileName); } }); if (!isBrowserSafari) htmlInputFilePatch.classList.add('no-file-selector-button'); } else { console.error('Rom Patcher JS: input#rom-patcher-input-file-patch[type=file] not found'); throw new Error('Rom Patcher JS: input#rom-patcher-input-file-patch[type=file] not found'); } /* dirty fix for iOS Safari, which only supports mimetypes in accept attribute */ /* accept attribute compatibility: https://caniuse.com/input-file-accept */ if (isBrowserSafari && isBrowserMobile) { htmlInputFilePatch.accept = 'application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip'; } } const htmlButtonApply = htmlElements.get('button-apply'); if (htmlButtonApply && htmlButtonApply.tagName === 'BUTTON') { htmlButtonApply.addEventListener('click', RomPatcherWeb.applyPatch); } else { console.error('Rom Patcher JS: button#rom-patcher-button-apply not found'); 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.disableAll(); /* 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(); }); if (!validEmbededPatch) { htmlElements.get('input-file-patch').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(validEmbededPatch); else htmlElements.enableAll(); if (typeof settings.oninitialize === 'function') settings.oninitialize(this); } /* 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; var loading = 0; return { _: function (str) { /* public localization function for external usage purposes */ return _(str); }, getHtmlElements: function () { return htmlElements; }, isInitialized: function () { return initialized; }, getEmbededPatches: function () { return currentEmbededPatches; }, 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('rom', romFile.fileName); } }, providePatchFile: function (binFile, transferFakeFile) { htmlElements.disableAll(); patch = null; if (binFile) { if (ZIPManager.isZipFile(binFile)) { ZIPManager.unzipPatches(binFile._u8array.buffer); } else { try { const parsedPatch = RomPatcher.parsePatchFile(binFile); if (parsedPatch) { patch = parsedPatch; _setPatchInputSpinner(false); const embededPatchInfo = _getEmbededPatchInfo(binFile.fileName); if (embededPatchInfo) { /* custom input validation */ if (embededPatchInfo.inputValidation) { if (embededPatchInfo.inputValidation.type === 'CRC32') { patch.validateSource = function (romFile, headerSize) { for (var i = 0; i < embededPatchInfo.inputValidation.value.length; i++) { if (embededPatchInfo.inputValidation.value[i] === romFile.hashCRC32(headerSize)) return true; } return false; } } else if (embededPatchInfo.inputValidation.type === 'MD5') { patch.validateSource = function (romFile, headerSize) { for (var i = 0; i < embededPatchInfo.inputValidation.value.length; i++) { if (embededPatchInfo.inputValidation.value[i] === romFile.hashMD5(headerSize)) return true; } return false; } } else { throw new Error('Rom Patcher JS: Invalid inputValidation type'); } patch.getValidationInfo = function () { return embededPatchInfo.inputValidation }; } /* 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 (typeof settings.onloadpatch === 'function') { settings.onloadpatch(binFile, embededPatchInfo, parsedPatch); } if (transferFakeFile) { htmlElements.setFakeFile('patch', binFile.fileName); } } else { _setToastError(_('Invalid patch file')); } } catch (ex) { _setToastError(ex.message); } } } if (patch) { htmlElements.removeClass('input-file-patch', 'invalid'); } else { htmlElements.addClass('input-file-patch', 'invalid'); } htmlElements.enableAll(); }, refreshRomFileName: function () { if (romFile) htmlElements.setFakeFile('rom', romFile.fileName); }, pickEmbededFile: function (fileName) { if (!currentEmbededPatches) throw new Error('No embeded patches available'); else if (typeof fileName !== 'string') throw new Error('Invalid embeded patch file name'); const selectPatch = htmlElements.get('select-patch'); for (var i = 0; i < selectPatch.children.length; i++) { if (selectPatch.children[i].patchFileName === fileName) { if (selectPatch.value != selectPatch.children[i].value) { selectPatch.value = selectPatch.children[i].value; /* create and dispatch change event */ const evt = new Event('change'); selectPatch.dispatchEvent(evt); } break; } } }, initialize: function (newSettings, embededPatchInfo) { if (initialized) throw new Error('Rom Patcher JS was already initialized'); else if (loading) throw new Error('Rom Patcher JS is already loading or has failed to load'); /* 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'); /* queue script dependencies */ const missingDependencies = _getMissingDependencies(); loading = missingDependencies.length; const onLoadScript = function () { loading--; if (loading === 0) { try { _initialize(newSettings, embededPatchInfo); } catch (ex) { _setToastError(ex.message); htmlElements.disableAll(); } } }; const onErrorScript = function () { throw new Error('Rom Patcher JS: error loading script ' + script.src); }; console.log('Rom Patcher JS: loading ' + missingDependencies.length + ' dependencies'); missingDependencies.forEach(function (path) { var script = document.createElement('script'); script.onload = onLoadScript; script.onerror = onErrorScript; script.src = path; document.head.appendChild(script); }); }, setEmbededPatches: function (embededPatchInfo) { if (!currentEmbededPatches) throw new Error('Rom Patcher JS: not in embeded patch mode'); /* embeded patches */ var validEmbededPatch = _checkEmbededPatchParameter(embededPatchInfo); if (!validEmbededPatch) throw new Error('Rom Patcher JS: invalid embeded patch parameter'); _fetchPatchFile(validEmbededPatch); }, applyPatch: function () { if (romFile && patch) { const romPatcherOptions = { requireValidation: settings.requireValidation, 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); return 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); return (romFile && patch); } }, 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 (newSettings && typeof newSettings === 'object') { if (typeof newSettings.language === 'string') settings.language = newSettings.language; if (typeof newSettings.outputSuffix === 'boolean') settings.outputSuffix = newSettings.outputSuffix; if (typeof newSettings.fixChecksum === 'boolean') settings.fixChecksum = newSettings.fixChecksum; if (typeof newSettings.requireValidation === 'boolean') settings.requireValidation = newSettings.requireValidation; if (typeof newSettings.oninitialize === 'function') settings.oninitialize = newSettings.oninitialize; else if (typeof newSettings.oninitialize !== 'undefined') settings.oninitialize = null; 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.onloadpatch === 'function') settings.onloadpatch = newSettings.onloadpatch; else if (typeof newSettings.onloadpatch !== 'undefined') settings.onloadpatch = null; if (typeof newSettings.onpatch === 'function') settings.onpatch = newSettings.onpatch; else if (typeof newSettings.onpatch !== 'undefined') settings.onpatch = null; } RomPatcherWeb.translateUI(); } } }()); /* 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|ebp|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) { const selectablePatches = []; const optionalPatches = []; for (var i = 0; i < filteredEntries.length; i++) { const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === filteredEntries[i].filename); if (embededPatchInfo && embededPatchInfo.optional) optionalPatches.push(filteredEntries[i]); else selectablePatches.push(filteredEntries[i]); } if (!selectablePatches.length) { romPatcherWeb.setErrorMessage(_('No valid non-optional patches found in ZIP'), 'error'); romPatcherWeb.disable(); throw new Error('No valid non-optional patches found in ZIP'); } if (embededPatchesInfo.length && embededPatchesInfo.length === 1 && selectablePatches.length === 1) embededPatchesInfo[0].file = selectablePatches[0].filename; for (var i = 0; i < selectablePatches.length; i++) { const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === selectablePatches[i].filename); const option = document.createElement('option'); option.innerHTML = embededPatchInfo && embededPatchInfo.name ? embededPatchInfo.name : selectablePatches[i].filename; option.value = i; option.patchFileName = selectablePatches[i].filename; htmlElements.get('select-patch').appendChild(option); } htmlElements.get('select-patch')._unzipSelectedPatch = function (fileIndex) { _unzipEntry(selectablePatches[fileIndex], romPatcherWeb.providePatchFile); }; for (var i = 0; i < optionalPatches.length; i++) { const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === optionalPatches[i].filename); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = i; checkbox.checked = false; checkbox.disabled = true; embededPatchInfo.checkbox = checkbox; const label = document.createElement('label'); label.className = 'rom-patcher-checkbox-optional-patch'; label.appendChild(checkbox); label.appendChild(document.createTextNode(embededPatchInfo.name || embededPatchInfo.file)); if (embededPatchInfo.description) label.title = embededPatchInfo.description; htmlElements.get('container-optional-patches').appendChild(label); optionalPatches[i].getData(new zip.BlobWriter(), function (blob) { const fileReader = new FileReader(); fileReader.onload = function () { const binFile = new BinFile(this.result); binFile.fileName = 'optional_patch.unk'; embededPatchInfo.parsedPatch = RomPatcher.parsePatchFile(binFile); checkbox.disabled = false; }; fileReader.readAsArrayBuffer(blob); }); } if (optionalPatches.length === 1) htmlElements.show('container-optional-patches'); if (selectablePatches.length === 1) htmlElements.addClass('select-patch', 'single'); else htmlElements.addClass('select-patch', 'multiple'); htmlElements.setEnabled('select-patch', false); htmlElements.hide('span-loading-embeded-patch'); htmlElements.show('select-patch'); _unzipEntry(selectablePatches[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; const isBrowserSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); /* Safari userAgent does not include word Chrome, Chrome includes both! */ /* 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 _getMetadataFields = function (patchFormat) { if (patchFormat === 'rup') { return ['Description']; } else if (patchFormat === 'ebp') { return ['Author', 'Title', 'Description']; } return []; }; const _buildMetadataObject = function (patchFormat) { return _getMetadataFields(patchFormat).reduce((metadata, field) => { const input = document.getElementById('patch-builder-input-metadata-' + field.toLowerCase().replace(/\s+/g, '-')); if (input && input.value.trim()) metadata[field] = input.value.trim(); return metadata; }, {}); }; var webWorkerCreate; var initialized = false; return { isInitialized: function () { return initialized; }, initialize: function () { if (initialized) throw new Error('Patch Builder JS was already initialized'); else if (!romPatcherWeb.isInitialized()) throw new Error('Rom Patcher JS must be initialized before Patch Builder JS'); if (!document.getElementById('patch-builder-button-create') || document.getElementById('patch-builder-button-create').tagName !== 'BUTTON') { console.error('Patch Builder JS: button#patch-builder-button-create not found'); throw new Error('Patch Builder JS: button#patch-builder-button-create not found'); } if (!document.getElementById('patch-builder-select-patch-type') || document.getElementById('patch-builder-select-patch-type').tagName !== 'SELECT') { console.error('Patch Builder JS: select#patch-builder-select-patch-type not found'); throw new Error('Patch Builder JS: select#patch-builder-select-patch-type not found'); } if (!document.getElementById('patch-builder-input-file-original') || document.getElementById('patch-builder-input-file-original').tagName !== 'INPUT' || document.getElementById('patch-builder-input-file-original').type !== 'file') { console.error('Patch Builder JS: input[type=file]#patch-builder-input-file-original not found'); throw new Error('Patch Builder JS: input[type=file]#patch-builder-input-file-original not found'); } if (!document.getElementById('patch-builder-input-file-modified') || document.getElementById('patch-builder-input-file-modified').tagName !== 'INPUT' || document.getElementById('patch-builder-input-file-modified').type !== 'file') { console.error('Patch Builder JS: input[type=file]#patch-builder-input-file-modified not found'); throw new Error('Patch Builder JS: input[type=file]#patch-builder-input-file-modified not found'); } if (!isBrowserSafari) { document.getElementById('patch-builder-input-file-original').classList.add('no-file-selector-button'); document.getElementById('patch-builder-input-file-modified').classList.add('no-file-selector-button'); } 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); }; document.getElementById('patch-builder-button-create').disabled = true; document.getElementById('patch-builder-input-file-original').addEventListener('change', function () { if (this.files && this.files.length) { _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'); else if (ZIPManager.isZipFile(originalRom)) _setToastError(_('Patch creation is not compatible with zipped ROMs'), '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'); else if (ZIPManager.isZipFile(modifiedRom)) _setToastError(_('Patch creation is not compatible with zipped ROMs'), 'warning'); }); }); document.getElementById('patch-builder-select-patch-type').addEventListener('change', function () { if (!document.getElementById('patch-builder-container-metadata-inputs')) return; document.getElementById('patch-builder-container-metadata-inputs').innerHTML = ''; _getMetadataFields(this.value).forEach(function (field) { const input = document.createElement('input'); input.id = 'patch-builder-input-metadata-' + field.toLowerCase().replace(/\s+/g, '-'); input.className = 'patch-builder-input-metadata'; input.type = 'text'; input.placeholder = _(field); document.getElementById('patch-builder-container-metadata-inputs').appendChild(input); }); }); document.getElementById('patch-builder-button-create').addEventListener('click', function () { const patchFormat=document.getElementById('patch-builder-select-patch-type').value; _setElementsStatus(false); _setCreateButtonSpinner(true); webWorkerCreate.postMessage( { originalRomU8Array: originalRom._u8array, modifiedRomU8Array: modifiedRom._u8array, format: patchFormat, metadata: _buildMetadataObject(patchFormat) }, [ originalRom._u8array.buffer, modifiedRom._u8array.buffer ] ); }); console.log('Patch Builder JS initialized'); initialized = true; _setElementsStatus(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...', 'Author': 'Autor', 'Title': 'Título', 'Description': 'Descripción', '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ç...', 'Author': 'Autor', 'Title': 'Títol', 'Description': 'Descripció', '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': '不建議使用大檔。' } };