/* * 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-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. */ 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(8); 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(BDF.MAGIC)) { patch = BDF.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'); if(patchedRom.unpatched) patchedRom.fileName = patchedRom.fileName.replace(' (patched)', ' (unpatched)'); } 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, metadata) { 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, metadata && metadata.Description? metadata.Description : null); } else if (format === 'ebp') { patch = IPS.buildFromRoms(originalFile, modifiedFile, metadata); } 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'); BDF = require('./modules/RomPatcher.format.bdf'); PMSR = require('./modules/RomPatcher.format.pmsr'); VCDIFF = require('./modules/RomPatcher.format.vcdiff'); }