mirror of
https://github.com/marcrobledo/RomPatcher.js.git
synced 2025-07-17 16:38:31 +00:00
405 lines
No EOL
13 KiB
JavaScript
405 lines
No EOL
13 KiB
JavaScript
/*
|
|
* 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');
|
|
} |