1
0
Fork 0
mirror of https://github.com/marcrobledo/RomPatcher.js.git synced 2025-07-17 16:38:31 +00:00
RomPatcher.js/rom-patcher-js/RomPatcher.js
2024-08-09 19:30:49 +02:00

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');
}