mirror of
https://github.com/marcrobledo/RomPatcher.js.git
synced 2025-06-27 16:25:54 +00:00
version 3.0
This commit is contained in:
parent
20a97ae39f
commit
f7014075ec
86 changed files with 7587 additions and 238 deletions
405
rom-patcher-js/RomPatcher.js
Normal file
405
rom-patcher-js/RomPatcher.js
Normal file
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* Rom Patcher JS core
|
||||
* A ROM patcher/builder made in JavaScript, can be implemented as a webapp or a Node.JS CLI tool
|
||||
* By Marc Robledo https://www.marcrobledo.com
|
||||
* Sourcecode: https://github.com/marcrobledo/RomPatcher.js
|
||||
* License:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2016-2024 Marc Robledo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
const RomPatcher = (function () {
|
||||
const TOO_BIG_ROM_SIZE = 67108863;
|
||||
|
||||
const HEADERS_INFO = [
|
||||
{ extensions: ['nes'], size: 16, romSizeMultiple: 1024, name: 'iNES' }, /* https://www.nesdev.org/wiki/INES */
|
||||
{ extensions: ['fds'], size: 16, romSizeMultiple: 65500, name: 'fwNES' }, /* https://www.nesdev.org/wiki/FDS_file_format */
|
||||
{ extensions: ['lnx'], size: 64, romSizeMultiple: 1024, name: 'LNX' },
|
||||
{ extensions: ['sfc', 'smc', 'swc', 'fig'], size: 512, romSizeMultiple: 262144, name: 'SNES copier' },
|
||||
];
|
||||
|
||||
const GAME_BOY_NINTENDO_LOGO = [
|
||||
0xce, 0xed, 0x66, 0x66, 0xcc, 0x0d, 0x00, 0x0b, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0c, 0x00, 0x0d,
|
||||
0x00, 0x08, 0x11, 0x1f, 0x88, 0x89, 0x00, 0x0e, 0xdc, 0xcc, 0x6e, 0xe6, 0xdd, 0xdd, 0xd9, 0x99
|
||||
];
|
||||
|
||||
|
||||
|
||||
const _getRomSystem = function (binFile) {
|
||||
/* to-do: add more systems */
|
||||
const extension = binFile.getExtension().trim();
|
||||
if (binFile.fileSize > 0x0200 && binFile.fileSize % 4 === 0) {
|
||||
if ((extension === 'gb' || extension === 'gbc') && binFile.fileSize % 0x4000 === 0) {
|
||||
binFile.seek(0x0104);
|
||||
var valid = true;
|
||||
for (var i = 0; i < GAME_BOY_NINTENDO_LOGO.length && valid; i++) {
|
||||
if (GAME_BOY_NINTENDO_LOGO[i] !== binFile.readU8())
|
||||
valid = false;
|
||||
}
|
||||
if (valid)
|
||||
return 'gb';
|
||||
} else if (extension === 'md' || extension === 'bin') {
|
||||
binFile.seek(0x0100);
|
||||
if (/SEGA (GENESIS|MEGA DR)/.test(binFile.readString(12)))
|
||||
return 'smd';
|
||||
} else if (extension === 'z64' && binFile.fileSize >= 0x400000) {
|
||||
return 'n64'
|
||||
}
|
||||
} else if (extension === 'fds' && binFile.fileSize % 65500 === 0) {
|
||||
return 'fds'
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const _getRomAdditionalChecksum = function (binFile) {
|
||||
/* to-do: add more systems */
|
||||
const romSystem = _getRomSystem(binFile);
|
||||
if (romSystem === 'n64') {
|
||||
binFile.seek(0x3c);
|
||||
const cartId = binFile.readString(3);
|
||||
|
||||
binFile.seek(0x10);
|
||||
const crc = binFile.readBytes(8).reduce(function (hex, b) {
|
||||
if (b < 16)
|
||||
return hex + '0' + b.toString(16);
|
||||
else
|
||||
return hex + b.toString(16);
|
||||
}, '');
|
||||
return cartId + ' (' + crc + ')';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
parsePatchFile: function (patchFile) {
|
||||
if (!(patchFile instanceof BinFile))
|
||||
throw new Error('Patch file is not an instance of BinFile');
|
||||
|
||||
patchFile.littleEndian = false;
|
||||
patchFile.seek(0);
|
||||
|
||||
var header = patchFile.readString(6);
|
||||
var patch = null;
|
||||
if (header.startsWith(IPS.MAGIC)) {
|
||||
patch = IPS.fromFile(patchFile);
|
||||
} else if (header.startsWith(UPS.MAGIC)) {
|
||||
patch = UPS.fromFile(patchFile);
|
||||
} else if (header.startsWith(APS.MAGIC)) {
|
||||
patch = APS.fromFile(patchFile);
|
||||
} else if (header.startsWith(APSGBA.MAGIC)) {
|
||||
patch = APSGBA.fromFile(patchFile);
|
||||
} else if (header.startsWith(BPS.MAGIC)) {
|
||||
patch = BPS.fromFile(patchFile);
|
||||
} else if (header.startsWith(RUP.MAGIC)) {
|
||||
patch = RUP.fromFile(patchFile);
|
||||
} else if (header.startsWith(PPF.MAGIC)) {
|
||||
patch = PPF.fromFile(patchFile);
|
||||
} else if (header.startsWith(PMSR.MAGIC)) {
|
||||
patch = PMSR.fromFile(patchFile);
|
||||
} else if (header.startsWith(VCDIFF.MAGIC)) {
|
||||
patch = VCDIFF.fromFile(patchFile);
|
||||
}
|
||||
|
||||
if (patch)
|
||||
patch._originalPatchFile = patchFile;
|
||||
|
||||
return patch;
|
||||
},
|
||||
|
||||
validateRom: function (romFile, patch, skipHeaderSize) {
|
||||
if (!(romFile instanceof BinFile))
|
||||
throw new Error('ROM file is not an instance of BinFile');
|
||||
else if (typeof patch !== 'object')
|
||||
throw new Error('Unknown patch format');
|
||||
|
||||
|
||||
if (typeof skipHeaderSize !== 'number' || skipHeaderSize < 0)
|
||||
skipHeaderSize = 0;
|
||||
|
||||
if (
|
||||
typeof patch.validateSource === 'function' && !patch.validateSource(romFile, skipHeaderSize)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
applyPatch: function (romFile, patch, optionsParam) {
|
||||
if (!(romFile instanceof BinFile))
|
||||
throw new Error('ROM file is not an instance of BinFile');
|
||||
else if (typeof patch !== 'object')
|
||||
throw new Error('Unknown patch format');
|
||||
|
||||
|
||||
const options = {
|
||||
requireValidation: false,
|
||||
removeHeader: false,
|
||||
addHeader: false,
|
||||
fixChecksum: false,
|
||||
outputSuffix: true
|
||||
};
|
||||
if (typeof optionsParam === 'object') {
|
||||
if (typeof optionsParam.requireValidation !== 'undefined')
|
||||
options.requireValidation = !!optionsParam.requireValidation;
|
||||
if (typeof optionsParam.removeHeader !== 'undefined')
|
||||
options.removeHeader = !!optionsParam.removeHeader;
|
||||
if (typeof optionsParam.addHeader !== 'undefined')
|
||||
options.addHeader = !!optionsParam.addHeader;
|
||||
if (typeof optionsParam.fixChecksum !== 'undefined')
|
||||
options.fixChecksum = !!optionsParam.fixChecksum;
|
||||
if (typeof optionsParam.outputSuffix !== 'undefined')
|
||||
options.outputSuffix = !!optionsParam.outputSuffix;
|
||||
}
|
||||
|
||||
var extractedHeader = false;
|
||||
var fakeHeaderSize = 0;
|
||||
if (options.removeHeader) {
|
||||
const headerInfo = RomPatcher.isRomHeadered(romFile);
|
||||
if (headerInfo) {
|
||||
const splitData = RomPatcher.removeHeader(romFile);
|
||||
extractedHeader = splitData.header;
|
||||
romFile = splitData.rom;
|
||||
}
|
||||
} else if (options.addHeader) {
|
||||
const headerInfo = RomPatcher.canRomGetHeader(romFile);
|
||||
if (headerInfo) {
|
||||
fakeHeaderSize = headerInfo.fileSize;
|
||||
romFile = RomPatcher.addFakeHeader(romFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.requireValidation && !RomPatcher.validateRom(romFile, patch)) {
|
||||
throw new Error('Invalid input ROM checksum');
|
||||
}
|
||||
|
||||
var patchedRom = patch.apply(romFile);
|
||||
if (extractedHeader) {
|
||||
/* reinsert header */
|
||||
if (options.fixChecksum)
|
||||
RomPatcher.fixRomHeaderChecksum(patchedRom);
|
||||
|
||||
const patchedRomWithHeader = new BinFile(extractedHeader.fileSize + patchedRom.fileSize);
|
||||
patchedRomWithHeader.fileName = patchedRom.fileName;
|
||||
patchedRomWithHeader.fileType = patchedRom.fileType;
|
||||
extractedHeader.copyTo(patchedRomWithHeader, 0, extractedHeader.fileSize);
|
||||
patchedRom.copyTo(patchedRomWithHeader, 0, patchedRom.fileSize, extractedHeader.fileSize);
|
||||
|
||||
patchedRom = patchedRomWithHeader;
|
||||
} else if (fakeHeaderSize) {
|
||||
/* remove fake header */
|
||||
const patchedRomWithoutFakeHeader = patchedRom.slice(fakeHeaderSize);
|
||||
|
||||
if (options.fixChecksum)
|
||||
RomPatcher.fixRomHeaderChecksum(patchedRomWithoutFakeHeader);
|
||||
|
||||
patchedRom = patchedRomWithoutFakeHeader;
|
||||
|
||||
} else if (options.fixChecksum) {
|
||||
RomPatcher.fixRomHeaderChecksum(patchedRom);
|
||||
}
|
||||
|
||||
if (options.outputSuffix) {
|
||||
patchedRom.fileName = romFile.fileName.replace(/\.([^\.]*?)$/, ' (patched).$1');
|
||||
} else if (patch._originalPatchFile) {
|
||||
patchedRom.fileName = patch._originalPatchFile.fileName.replace(/\.\w+$/i, (/\.\w+$/i.test(romFile.fileName) ? romFile.fileName.match(/\.\w+$/i)[0] : ''));
|
||||
} else {
|
||||
patchedRom.fileName = romFile.fileName;
|
||||
}
|
||||
|
||||
return patchedRom;
|
||||
},
|
||||
|
||||
createPatch: function (originalFile, modifiedFile, format) {
|
||||
if (!(originalFile instanceof BinFile))
|
||||
throw new Error('Original ROM file is not an instance of BinFile');
|
||||
else if (!(modifiedFile instanceof BinFile))
|
||||
throw new Error('Modified ROM file is not an instance of BinFile');
|
||||
|
||||
if (typeof format === 'string')
|
||||
format = format.trim().toLowerCase();
|
||||
else if (typeof format === 'undefined')
|
||||
format = 'ips';
|
||||
|
||||
var patch;
|
||||
if (format === 'ips') {
|
||||
patch = IPS.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'bps') {
|
||||
patch = BPS.buildFromRoms(originalFile, modifiedFile, (originalFile.fileSize <= 4194304));
|
||||
} else if (format === 'ppf') {
|
||||
patch = PPF.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'ups') {
|
||||
patch = UPS.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'aps') {
|
||||
patch = APS.buildFromRoms(originalFile, modifiedFile);
|
||||
} else if (format === 'rup') {
|
||||
patch = RUP.buildFromRoms(originalFile, modifiedFile);
|
||||
} else {
|
||||
throw new Error('Invalid patch format');
|
||||
}
|
||||
|
||||
if (
|
||||
!(format === 'ppf' && originalFile.fileSize > modifiedFile.fileSize) && //skip verification if PPF and PPF+modified size>original size
|
||||
modifiedFile.hashCRC32() !== patch.apply(originalFile).hashCRC32()
|
||||
) {
|
||||
//throw new Error('Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.');
|
||||
}
|
||||
return patch;
|
||||
},
|
||||
|
||||
|
||||
/* check if ROM can inject a fake header (for patches that require a headered ROM) */
|
||||
canRomGetHeader: function (romFile) {
|
||||
if (romFile.fileSize <= 0x600000) {
|
||||
const compatibleHeader = HEADERS_INFO.find(headerInfo => headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 && romFile.fileSize % headerInfo.romSizeMultiple === 0);
|
||||
if (compatibleHeader) {
|
||||
return {
|
||||
name: compatibleHeader.name,
|
||||
size: compatibleHeader.size
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* check if ROM has a known header */
|
||||
isRomHeadered: function (romFile) {
|
||||
if (romFile.fileSize <= 0x600200 && romFile.fileSize % 1024 !== 0) {
|
||||
const compatibleHeader = HEADERS_INFO.find(headerInfo => headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 && (romFile.fileSize - headerInfo.size) % headerInfo.romSizeMultiple === 0);
|
||||
if (compatibleHeader) {
|
||||
return {
|
||||
name: compatibleHeader.name,
|
||||
size: compatibleHeader.size
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* remove ROM header */
|
||||
removeHeader: function (romFile) {
|
||||
const headerInfo = RomPatcher.isRomHeadered(romFile);
|
||||
if (headerInfo) {
|
||||
return {
|
||||
header: romFile.slice(0, headerInfo.size),
|
||||
rom: romFile.slice(headerInfo.size)
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* add fake ROM header */
|
||||
addFakeHeader: function (romFile) {
|
||||
const headerInfo = RomPatcher.canRomGetHeader(romFile);
|
||||
if (headerInfo) {
|
||||
const romWithFakeHeader = new BinFile(headerInfo.size + romFile.fileSize);
|
||||
romWithFakeHeader.fileName = romFile.fileName;
|
||||
romWithFakeHeader.fileType = romFile.fileType;
|
||||
romFile.copyTo(romWithFakeHeader, 0, romFile.fileSize, headerInfo.size);
|
||||
|
||||
//add a correct FDS header
|
||||
if (_getRomSystem(romWithFakeHeader) === 'fds') {
|
||||
romWithFakeHeader.seek(0);
|
||||
romWithFakeHeader.writeBytes([0x46, 0x44, 0x53, 0x1a, romFile.fileSize / 65500]);
|
||||
}
|
||||
|
||||
romWithFakeHeader.fakeHeader = true;
|
||||
|
||||
return romWithFakeHeader;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* get ROM internal checksum, if possible */
|
||||
fixRomHeaderChecksum: function (romFile) {
|
||||
const romSystem = _getRomSystem(romFile);
|
||||
|
||||
if (romSystem === 'gb') {
|
||||
/* get current checksum */
|
||||
romFile.seek(0x014d);
|
||||
const currentChecksum = romFile.readU8();
|
||||
|
||||
/* calculate checksum */
|
||||
var newChecksum = 0x00;
|
||||
romFile.seek(0x0134);
|
||||
for (var i = 0; i <= 0x18; i++) {
|
||||
newChecksum = ((newChecksum - romFile.readU8() - 1) >>> 0) & 0xff;
|
||||
}
|
||||
|
||||
/* fix checksum */
|
||||
if (currentChecksum !== newChecksum) {
|
||||
console.log('fixed Game Boy checksum');
|
||||
romFile.seek(0x014d);
|
||||
romFile.writeU8(newChecksum);
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if (romSystem === 'smd') {
|
||||
/* get current checksum */
|
||||
romFile.seek(0x018e);
|
||||
const currentChecksum = romFile.readU16();
|
||||
|
||||
/* calculate checksum */
|
||||
var newChecksum = 0x0000;
|
||||
romFile.seek(0x0200);
|
||||
while (!romFile.isEOF()) {
|
||||
newChecksum = ((newChecksum + romFile.readU16()) >>> 0) & 0xffff;
|
||||
}
|
||||
|
||||
/* fix checksum */
|
||||
if (currentChecksum !== newChecksum) {
|
||||
console.log('fixed Megadrive/Genesis checksum');
|
||||
romFile.seek(0x018e);
|
||||
romFile.writeU16(newChecksum);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/* get ROM additional checksum info, if possible */
|
||||
getRomAdditionalChecksum: function (romFile) {
|
||||
return _getRomAdditionalChecksum(romFile);
|
||||
},
|
||||
|
||||
|
||||
/* check if ROM is too big */
|
||||
isRomTooBig: function (romFile) {
|
||||
return romFile && romFile.fileSize > TOO_BIG_ROM_SIZE;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = RomPatcher;
|
||||
|
||||
IPS = require('./modules/RomPatcher.format.ips');
|
||||
UPS = require('./modules/RomPatcher.format.ups');
|
||||
APS = require('./modules/RomPatcher.format.aps_n64');
|
||||
APSGBA = require('./modules/RomPatcher.format.aps_gba');
|
||||
BPS = require('./modules/RomPatcher.format.bps');
|
||||
RUP = require('./modules/RomPatcher.format.rup');
|
||||
PPF = require('./modules/RomPatcher.format.ppf');
|
||||
PMSR = require('./modules/RomPatcher.format.pmsr');
|
||||
VCDIFF = require('./modules/RomPatcher.format.vcdiff');
|
||||
}
|
1762
rom-patcher-js/RomPatcher.webapp.js
Normal file
1762
rom-patcher-js/RomPatcher.webapp.js
Normal file
File diff suppressed because it is too large
Load diff
82
rom-patcher-js/RomPatcher.webworker.apply.js
Normal file
82
rom-patcher-js/RomPatcher.webworker.apply.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
self.importScripts(
|
||||
'./RomPatcher.js',
|
||||
'./modules/BinFile.js',
|
||||
'./modules/HashCalculator.js',
|
||||
'./modules/RomPatcher.format.ips.js',
|
||||
'./modules/RomPatcher.format.aps_n64.js',
|
||||
'./modules/RomPatcher.format.aps_gba.js',
|
||||
'./modules/RomPatcher.format.ups.js',
|
||||
'./modules/RomPatcher.format.bps.js',
|
||||
'./modules/RomPatcher.format.rup.js',
|
||||
'./modules/RomPatcher.format.ppf.js',
|
||||
'./modules/RomPatcher.format.pmsr.js',
|
||||
'./modules/RomPatcher.format.vcdiff.js'
|
||||
);
|
||||
|
||||
|
||||
self.onmessage = event => { // listen for messages from the main thread
|
||||
const romFile=new BinFile(event.data.romFileU8Array);
|
||||
romFile.fileName=event.data.romFileName;
|
||||
//romFile.fileType.event.data.romFileType;
|
||||
const patchFile=new BinFile(event.data.patchFileU8Array);
|
||||
patchFile.fileName=event.data.patchFileName;
|
||||
|
||||
const patch=RomPatcher.parsePatchFile(patchFile);
|
||||
|
||||
var errorMessage=false;
|
||||
|
||||
|
||||
var patchedRom;
|
||||
if(patch){
|
||||
try{
|
||||
patchedRom=RomPatcher.applyPatch(romFile, patch, event.data.options);
|
||||
}catch(evt){
|
||||
errorMessage=evt.message;
|
||||
}
|
||||
}else{
|
||||
errorMessage='Invalid patch file';
|
||||
}
|
||||
|
||||
//console.log('postMessage');
|
||||
if(patchedRom){
|
||||
/* set custom output name if embeded patch */
|
||||
const patchExtraInfo=event.data.patchExtraInfo;
|
||||
if(patchExtraInfo){
|
||||
if(typeof patchExtraInfo.outputName === 'string')
|
||||
patchedRom.setName(patchExtraInfo.outputName);
|
||||
if(typeof patchExtraInfo.outputExtension === 'string')
|
||||
patchedRom.setExtension(patchExtraInfo.outputExtension);
|
||||
}
|
||||
|
||||
self.postMessage(
|
||||
{
|
||||
success: !!errorMessage,
|
||||
romFileU8Array:event.data.romFileU8Array,
|
||||
patchFileU8Array:event.data.patchFileU8Array,
|
||||
patchedRomU8Array:patchedRom._u8array,
|
||||
patchedRomFileName:patchedRom.fileName,
|
||||
errorMessage:errorMessage
|
||||
},
|
||||
[
|
||||
event.data.romFileU8Array.buffer,
|
||||
event.data.patchFileU8Array.buffer,
|
||||
patchedRom._u8array.buffer
|
||||
]
|
||||
);
|
||||
}else{
|
||||
self.postMessage(
|
||||
{
|
||||
success: false,
|
||||
romFileU8Array:event.data.romFileU8Array,
|
||||
patchFileU8Array:event.data.patchFileU8Array,
|
||||
errorMessage:errorMessage
|
||||
},
|
||||
[
|
||||
event.data.romFileU8Array.buffer,
|
||||
event.data.patchFileU8Array.buffer
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
26
rom-patcher-js/RomPatcher.webworker.crc.js
Normal file
26
rom-patcher-js/RomPatcher.webworker.crc.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
self.importScripts(
|
||||
'./RomPatcher.js',
|
||||
'./modules/BinFile.js',
|
||||
'./modules/HashCalculator.js'
|
||||
);
|
||||
|
||||
|
||||
self.onmessage = event => { // listen for messages from the main thread
|
||||
const binFile=new BinFile(event.data.u8array);
|
||||
binFile.fileName=event.data.fileName;
|
||||
const startOffset=typeof event.data.checksumStartOffset==='number'? event.data.checksumStartOffset : 0;
|
||||
|
||||
self.postMessage(
|
||||
{
|
||||
action: event.data.action,
|
||||
crc32:binFile.hashCRC32(startOffset),
|
||||
md5:binFile.hashMD5(startOffset),
|
||||
checksumStartOffset: startOffset,
|
||||
rom:RomPatcher.getRomAdditionalChecksum(binFile),
|
||||
u8array:event.data.u8array
|
||||
},
|
||||
[event.data.u8array.buffer]
|
||||
);
|
||||
};
|
36
rom-patcher-js/RomPatcher.webworker.create.js
Normal file
36
rom-patcher-js/RomPatcher.webworker.create.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
|
||||
self.importScripts(
|
||||
'./RomPatcher.js',
|
||||
'./modules/BinFile.js',
|
||||
'./modules/HashCalculator.js',
|
||||
'./modules/RomPatcher.format.ips.js',
|
||||
'./modules/RomPatcher.format.aps_n64.js',
|
||||
'./modules/RomPatcher.format.ups.js',
|
||||
'./modules/RomPatcher.format.bps.js',
|
||||
'./modules/RomPatcher.format.rup.js',
|
||||
'./modules/RomPatcher.format.ppf.js'
|
||||
);
|
||||
|
||||
|
||||
self.onmessage = event => { // listen for messages from the main thread
|
||||
const originalFile=new BinFile(event.data.originalRomU8Array);
|
||||
const modifiedFile=new BinFile(event.data.modifiedRomU8Array);
|
||||
const format=event.data.format;
|
||||
|
||||
const patch=RomPatcher.createPatch(originalFile, modifiedFile, format);
|
||||
const patchFile=patch.export('my_patch');
|
||||
|
||||
self.postMessage(
|
||||
{
|
||||
originalRomU8Array:event.data.originalRomU8Array,
|
||||
modifiedRomU8Array:event.data.modifiedRomU8Array,
|
||||
patchFileU8Array:patchFile._u8array
|
||||
},
|
||||
[
|
||||
event.data.originalRomU8Array.buffer,
|
||||
event.data.modifiedRomU8Array.buffer,
|
||||
patchFile._u8array.buffer
|
||||
]
|
||||
);
|
||||
};
|
1
rom-patcher-js/assets/icon_alert_orange.svg
Normal file
1
rom-patcher-js/assets/icon_alert_orange.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#ff7800" d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>
|
After Width: | Height: | Size: 424 B |
1
rom-patcher-js/assets/icon_check_circle_green.svg
Normal file
1
rom-patcher-js/assets/icon_check_circle_green.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#080" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm1.5 0a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm10.28-1.72-4.5 4.5a.75.75 0 0 1-1.06 0l-2-2a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l1.47 1.47 3.97-3.97a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"></path></svg>
|
After Width: | Height: | Size: 364 B |
1
rom-patcher-js/assets/icon_upload.svg
Normal file
1
rom-patcher-js/assets/icon_upload.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path><path d="M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z"></path></svg>
|
After Width: | Height: | Size: 439 B |
1
rom-patcher-js/assets/icon_x_circle_red.svg
Normal file
1
rom-patcher-js/assets/icon_x_circle_red.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#f00" d="M2.344 2.343h-.001a8 8 0 0 1 11.314 11.314A8.002 8.002 0 0 1 .234 10.089a8 8 0 0 1 2.11-7.746Zm1.06 10.253a6.5 6.5 0 1 0 9.108-9.275 6.5 6.5 0 0 0-9.108 9.275ZM6.03 4.97 8 6.94l1.97-1.97a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l1.97 1.97a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-1.97 1.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L6.94 8 4.97 6.03a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018Z"></path></svg>
|
After Width: | Height: | Size: 557 B |
BIN
rom-patcher-js/assets/powered_by_rom_patcher_js.png
Normal file
BIN
rom-patcher-js/assets/powered_by_rom_patcher_js.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 799 B |
475
rom-patcher-js/modules/BinFile.js
Normal file
475
rom-patcher-js/modules/BinFile.js
Normal file
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
* BinFile.js (last update: 2024-02-27)
|
||||
* by Marc Robledo, https://www.marcrobledo.com
|
||||
*
|
||||
* a JS class for reading/writing sequentially binary data from/to a file
|
||||
* that allows much more manipulation than simple DataView
|
||||
* compatible with both browsers and Node.js
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2014-2024 Marc Robledo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
function BinFile(source, onLoad) {
|
||||
this.littleEndian = false;
|
||||
this.offset = 0;
|
||||
this._lastRead = null;
|
||||
this._offsetsStack = [];
|
||||
|
||||
|
||||
if (
|
||||
BinFile.RUNTIME_ENVIROMENT === 'browser' && (
|
||||
source instanceof File ||
|
||||
source instanceof FileList ||
|
||||
(source instanceof HTMLElement && source.tagName === 'INPUT' && source.type === 'file')
|
||||
)
|
||||
) {
|
||||
if (source instanceof HTMLElement)
|
||||
source = source.files;
|
||||
if (source instanceof FileList)
|
||||
source = source[0];
|
||||
|
||||
this.fileName = source.name;
|
||||
this.fileType = source.type;
|
||||
this.fileSize = source.size;
|
||||
|
||||
if (typeof window.FileReader !== 'function')
|
||||
throw new Error('Incompatible browser');
|
||||
|
||||
this._fileReader = new FileReader();
|
||||
this._fileReader.addEventListener('load', function () {
|
||||
this.binFile._u8array = new Uint8Array(this.result);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this.binFile);
|
||||
}, false);
|
||||
|
||||
|
||||
this._fileReader.binFile = this;
|
||||
|
||||
this._fileReader.readAsArrayBuffer(source);
|
||||
|
||||
|
||||
|
||||
} else if (BinFile.RUNTIME_ENVIROMENT === 'node' && typeof source === 'string') {
|
||||
if (!nodeFs.existsSync(source))
|
||||
throw new Error(source + ' does not exist');
|
||||
|
||||
const arrayBuffer = nodeFs.readFileSync(source);
|
||||
|
||||
this.fileName = nodePath.basename(source);
|
||||
this.fileType = nodeFs.statSync(source).type;
|
||||
this.fileSize = arrayBuffer.byteLength;
|
||||
|
||||
this._u8array = new Uint8Array(arrayBuffer);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (source instanceof BinFile) { /* if source is another BinFile, clone it */
|
||||
this.fileName = source.fileName;
|
||||
this.fileType = source.fileType;
|
||||
this.fileSize = source.fileSize;
|
||||
|
||||
this._u8array = new Uint8Array(source._u8array.buffer.slice());
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
this.fileName = 'file.bin';
|
||||
this.fileType = 'application/octet-stream';
|
||||
this.fileSize = source.byteLength;
|
||||
|
||||
this._u8array = new Uint8Array(source);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (ArrayBuffer.isView(source)) { /* source is TypedArray */
|
||||
this.fileName = 'file.bin';
|
||||
this.fileType = 'application/octet-stream';
|
||||
this.fileSize = source.buffer.byteLength;
|
||||
|
||||
this._u8array = new Uint8Array(source.buffer);
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else if (typeof source === 'number') { /* source is integer, create new empty file */
|
||||
this.fileName = 'file.bin';
|
||||
this.fileType = 'application/octet-stream';
|
||||
this.fileSize = source;
|
||||
|
||||
this._u8array = new Uint8Array(new ArrayBuffer(source));
|
||||
|
||||
if (typeof onLoad === 'function')
|
||||
onLoad(this);
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
throw new Error('invalid BinFile source');
|
||||
}
|
||||
}
|
||||
BinFile.RUNTIME_ENVIROMENT = (function () {
|
||||
if (typeof window === 'object' && typeof window.document === 'object')
|
||||
return 'browser';
|
||||
else if (typeof WorkerGlobalScope === 'function' && self instanceof WorkerGlobalScope)
|
||||
return 'webworker';
|
||||
else if (typeof require === 'function' && typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string')
|
||||
return 'node';
|
||||
else
|
||||
return null;
|
||||
}());
|
||||
BinFile.DEVICE_LITTLE_ENDIAN = (function () { /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness */
|
||||
var buffer = new ArrayBuffer(2);
|
||||
new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
|
||||
// Int16Array uses the platform's endianness.
|
||||
return new Int16Array(buffer)[0] === 256;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
BinFile.prototype.push = function () {
|
||||
this._offsetsStack.push(this.offset);
|
||||
}
|
||||
BinFile.prototype.pop = function () {
|
||||
this.seek(this._offsetsStack.pop());
|
||||
}
|
||||
BinFile.prototype.seek = function (offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
BinFile.prototype.skip = function (nBytes) {
|
||||
this.offset += nBytes;
|
||||
}
|
||||
BinFile.prototype.isEOF = function () {
|
||||
return !(this.offset < this.fileSize)
|
||||
}
|
||||
BinFile.prototype.slice = function (offset, len, doNotClone) {
|
||||
if (typeof offset !== 'number' || offset < 0)
|
||||
offset = 0;
|
||||
else if (offset >= this.fileSize)
|
||||
throw new Error('out of bounds slicing');
|
||||
else
|
||||
offset = Math.floor(offset);
|
||||
|
||||
if (typeof len !== 'number' || offset < 0 || (offset + len) >= this.fileSize.length)
|
||||
len = this.fileSize - offset;
|
||||
else if (len === 0)
|
||||
throw new Error('zero length provided for slicing');
|
||||
else
|
||||
offset = Math.floor(offset);
|
||||
|
||||
if (offset === 0 && len === this.fileSize && doNotClone)
|
||||
return this;
|
||||
|
||||
|
||||
var newFile = new BinFile(this._u8array.buffer.slice(offset, offset + len));
|
||||
newFile.fileName = this.fileName;
|
||||
newFile.fileType = this.fileType;
|
||||
newFile.littleEndian = this.littleEndian;
|
||||
return newFile;
|
||||
}
|
||||
BinFile.prototype.prependBytes = function (bytes) {
|
||||
var newFile = new BinFile(this.fileSize + bytes.length);
|
||||
newFile.seek(0);
|
||||
newFile.writeBytes(bytes);
|
||||
this.copyTo(newFile, 0, this.fileSize, bytes.length);
|
||||
|
||||
this.fileSize = newFile.fileSize;
|
||||
this._u8array = newFile._u8array;
|
||||
return this;
|
||||
}
|
||||
BinFile.prototype.removeLeadingBytes = function (nBytes) {
|
||||
this.seek(0);
|
||||
var oldData = this.readBytes(nBytes);
|
||||
var newFile = this.slice(nBytes.length);
|
||||
|
||||
this.fileSize = newFile.fileSize;
|
||||
this._u8array = newFile._u8array;
|
||||
return oldData;
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.copyTo = function (target, offsetSource, len, offsetTarget) {
|
||||
if (!(target instanceof BinFile))
|
||||
throw new Error('target is not a BinFile object');
|
||||
|
||||
if (typeof offsetTarget !== 'number')
|
||||
offsetTarget = offsetSource;
|
||||
|
||||
len = len || (this.fileSize - offsetSource);
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
target._u8array[offsetTarget + i] = this._u8array[offsetSource + i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.save = function () {
|
||||
if (BinFile.RUNTIME_ENVIROMENT === 'browser') {
|
||||
var fileBlob = new Blob([this._u8array], { type: this.fileType });
|
||||
var blobUrl = URL.createObjectURL(fileBlob);
|
||||
var a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = this.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
document.body.removeChild(a);
|
||||
} else if (BinFile.RUNTIME_ENVIROMENT === 'node') {
|
||||
nodeFs.writeFileSync(this.fileName, Buffer.from(this._u8array.buffer));
|
||||
} else {
|
||||
throw new Error('invalid runtime environment, can\'t save file');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.getExtension = function () {
|
||||
var ext = this.fileName ? this.fileName.toLowerCase().match(/\.(\w+)$/) : '';
|
||||
|
||||
return ext ? ext[1] : '';
|
||||
}
|
||||
BinFile.prototype.getName = function () {
|
||||
return this.fileName.replace(new RegExp('\\.' + this.getExtension() + '$', 'i'), '');
|
||||
}
|
||||
BinFile.prototype.setExtension = function (newExtension) {
|
||||
return (this.fileName = this.getName() + '.' + newExtension);
|
||||
}
|
||||
BinFile.prototype.setName = function (newName) {
|
||||
return (this.fileName = newName + '.' + this.getExtension());
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.readU8 = function () {
|
||||
this._lastRead = this._u8array[this.offset++];
|
||||
|
||||
return this._lastRead
|
||||
}
|
||||
BinFile.prototype.readU16 = function () {
|
||||
if (this.littleEndian)
|
||||
this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8);
|
||||
else
|
||||
this._lastRead = (this._u8array[this.offset] << 8) + this._u8array[this.offset + 1];
|
||||
|
||||
this.offset += 2;
|
||||
return this._lastRead >>> 0
|
||||
}
|
||||
BinFile.prototype.readU24 = function () {
|
||||
if (this.littleEndian)
|
||||
this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16);
|
||||
else
|
||||
this._lastRead = (this._u8array[this.offset] << 16) + (this._u8array[this.offset + 1] << 8) + this._u8array[this.offset + 2];
|
||||
|
||||
this.offset += 3;
|
||||
return this._lastRead >>> 0
|
||||
}
|
||||
BinFile.prototype.readU32 = function () {
|
||||
if (this.littleEndian)
|
||||
this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16) + (this._u8array[this.offset + 3] << 24);
|
||||
else
|
||||
this._lastRead = (this._u8array[this.offset] << 24) + (this._u8array[this.offset + 1] << 16) + (this._u8array[this.offset + 2] << 8) + this._u8array[this.offset + 3];
|
||||
|
||||
this.offset += 4;
|
||||
return this._lastRead >>> 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
BinFile.prototype.readBytes = function (len) {
|
||||
this._lastRead = new Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
this._lastRead[i] = this._u8array[this.offset + i];
|
||||
}
|
||||
|
||||
this.offset += len;
|
||||
return this._lastRead
|
||||
}
|
||||
|
||||
BinFile.prototype.readString = function (len) {
|
||||
this._lastRead = '';
|
||||
for (var i = 0; i < len && (this.offset + i) < this.fileSize && this._u8array[this.offset + i] > 0; i++)
|
||||
this._lastRead = this._lastRead + String.fromCharCode(this._u8array[this.offset + i]);
|
||||
|
||||
this.offset += len;
|
||||
return this._lastRead
|
||||
}
|
||||
|
||||
BinFile.prototype.writeU8 = function (u8) {
|
||||
this._u8array[this.offset++] = u8;
|
||||
}
|
||||
BinFile.prototype.writeU16 = function (u16) {
|
||||
if (this.littleEndian) {
|
||||
this._u8array[this.offset] = u16 & 0xff;
|
||||
this._u8array[this.offset + 1] = u16 >> 8;
|
||||
} else {
|
||||
this._u8array[this.offset] = u16 >> 8;
|
||||
this._u8array[this.offset + 1] = u16 & 0xff;
|
||||
}
|
||||
|
||||
this.offset += 2;
|
||||
}
|
||||
BinFile.prototype.writeU24 = function (u24) {
|
||||
if (this.littleEndian) {
|
||||
this._u8array[this.offset] = u24 & 0x0000ff;
|
||||
this._u8array[this.offset + 1] = (u24 & 0x00ff00) >> 8;
|
||||
this._u8array[this.offset + 2] = (u24 & 0xff0000) >> 16;
|
||||
} else {
|
||||
this._u8array[this.offset] = (u24 & 0xff0000) >> 16;
|
||||
this._u8array[this.offset + 1] = (u24 & 0x00ff00) >> 8;
|
||||
this._u8array[this.offset + 2] = u24 & 0x0000ff;
|
||||
}
|
||||
|
||||
this.offset += 3;
|
||||
}
|
||||
BinFile.prototype.writeU32 = function (u32) {
|
||||
if (this.littleEndian) {
|
||||
this._u8array[this.offset] = u32 & 0x000000ff;
|
||||
this._u8array[this.offset + 1] = (u32 & 0x0000ff00) >> 8;
|
||||
this._u8array[this.offset + 2] = (u32 & 0x00ff0000) >> 16;
|
||||
this._u8array[this.offset + 3] = (u32 & 0xff000000) >> 24;
|
||||
} else {
|
||||
this._u8array[this.offset] = (u32 & 0xff000000) >> 24;
|
||||
this._u8array[this.offset + 1] = (u32 & 0x00ff0000) >> 16;
|
||||
this._u8array[this.offset + 2] = (u32 & 0x0000ff00) >> 8;
|
||||
this._u8array[this.offset + 3] = u32 & 0x000000ff;
|
||||
}
|
||||
|
||||
this.offset += 4;
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.writeBytes = function (a) {
|
||||
for (var i = 0; i < a.length; i++)
|
||||
this._u8array[this.offset + i] = a[i]
|
||||
|
||||
this.offset += a.length;
|
||||
}
|
||||
|
||||
BinFile.prototype.writeString = function (str, len) {
|
||||
len = len || str.length;
|
||||
for (var i = 0; i < str.length && i < len; i++)
|
||||
this._u8array[this.offset + i] = str.charCodeAt(i);
|
||||
|
||||
for (; i < len; i++)
|
||||
this._u8array[this.offset + i] = 0x00;
|
||||
|
||||
this.offset += len;
|
||||
}
|
||||
|
||||
|
||||
BinFile.prototype.swapBytes = function (swapSize, newFile) {
|
||||
if (typeof swapSize !== 'number') {
|
||||
swapSize = 4;
|
||||
}
|
||||
|
||||
if (this.fileSize % swapSize !== 0) {
|
||||
throw new Error('file size is not divisible by ' + swapSize);
|
||||
}
|
||||
|
||||
var swappedFile = new BinFile(this.fileSize);
|
||||
this.seek(0);
|
||||
while (!this.isEOF()) {
|
||||
swappedFile.writeBytes(
|
||||
this.readBytes(swapSize).reverse()
|
||||
);
|
||||
}
|
||||
|
||||
if (newFile) {
|
||||
swappedFile.fileName = this.fileName;
|
||||
swappedFile.fileType = this.fileType;
|
||||
|
||||
return swappedFile;
|
||||
} else {
|
||||
this._u8array = swappedFile._u8array;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
BinFile.prototype.hashSHA1 = async function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.sha1 !== 'function')
|
||||
throw new Error('no Hash object found or missing sha1 function');
|
||||
|
||||
return HashCalculator.sha1(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashMD5 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.md5 !== 'function')
|
||||
throw new Error('no Hash object found or missing md5 function');
|
||||
|
||||
return HashCalculator.md5(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashCRC32 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.crc32 !== 'function')
|
||||
throw new Error('no Hash object found or missing crc32 function');
|
||||
|
||||
return HashCalculator.crc32(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashAdler32 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.adler32 !== 'function')
|
||||
throw new Error('no Hash object found or missing adler32 function');
|
||||
|
||||
return HashCalculator.adler32(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
BinFile.prototype.hashCRC16 = function (start, len) {
|
||||
if (typeof HashCalculator !== 'object' || typeof HashCalculator.crc16 !== 'function')
|
||||
throw new Error('no Hash object found or missing crc16 function');
|
||||
|
||||
return HashCalculator.crc16(this.slice(start, len, true)._u8array.buffer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (BinFile.RUNTIME_ENVIROMENT === 'node' && typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = BinFile;
|
||||
HashCalculator = require('./HashCalculator');
|
||||
nodePath = require('path');
|
||||
nodeFs = require('fs');
|
||||
}
|
179
rom-patcher-js/modules/HashCalculator.js
Normal file
179
rom-patcher-js/modules/HashCalculator.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* HashCalculator.js (last update: 2021-08-15)
|
||||
* by Marc Robledo, https://www.marcrobledo.com
|
||||
*
|
||||
* data hash calculator (CRC32, MD5, SHA1, ADLER-32, CRC16)
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2016-2021 Marc Robledo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
const HashCalculator = (function () {
|
||||
const HEX_CHR = '0123456789abcdef'.split('');
|
||||
|
||||
/* MD5 helpers */
|
||||
const _add32 = function (a, b) { return (a + b) & 0xffffffff }
|
||||
const _md5cycle = function (x, k) { var a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = _add32(a, x[0]); x[1] = _add32(b, x[1]); x[2] = _add32(c, x[2]); x[3] = _add32(d, x[3]) }
|
||||
const _md5blk = function (d) { var md5blks = [], i; for (i = 0; i < 64; i += 4)md5blks[i >> 2] = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + (d[i + 3] << 24); return md5blks }
|
||||
const _cmn = function (q, a, b, x, s, t) { a = _add32(_add32(a, q), _add32(x, t)); return _add32((a << s) | (a >>> (32 - s)), b) }
|
||||
const ff = function (a, b, c, d, x, s, t) { return _cmn((b & c) | ((~b) & d), a, b, x, s, t) }
|
||||
const gg = function (a, b, c, d, x, s, t) { return _cmn((b & d) | (c & (~d)), a, b, x, s, t) }
|
||||
const hh = function (a, b, c, d, x, s, t) { return _cmn(b ^ c ^ d, a, b, x, s, t) }
|
||||
const ii = function (a, b, c, d, x, s, t) { return _cmn(c ^ (b | (~d)), a, b, x, s, t) }
|
||||
|
||||
/* CRC32 helpers */
|
||||
const CRC32_TABLE = (function () {
|
||||
var c, crcTable = [];
|
||||
for (var n = 0; n < 256; n++) {
|
||||
c = n;
|
||||
for (var k = 0; k < 8; k++)
|
||||
c = ((c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1));
|
||||
crcTable[n] = c;
|
||||
}
|
||||
return crcTable;
|
||||
}());
|
||||
|
||||
/* Adler-32 helpers */
|
||||
const ADLER32_MOD = 0xfff1;
|
||||
|
||||
|
||||
|
||||
|
||||
const generateUint8Array = function (arrayBuffer, offset, len) {
|
||||
if (typeof offset !== 'number' || offset < 0)
|
||||
offset = 0;
|
||||
else if (offset < arrayBuffer.byteLength)
|
||||
offset = Math.floor(offset);
|
||||
else
|
||||
throw new Error('out of bounds slicing');
|
||||
|
||||
if (typeof len !== 'number' || len < 0 || (offset + len) >= arrayBuffer.byteLength.length)
|
||||
len = arrayBuffer.byteLength - offset;
|
||||
else if (len > 0)
|
||||
len = Math.floor(len);
|
||||
else
|
||||
throw new Error('zero length provided for slicing');
|
||||
|
||||
return new Uint8Array(arrayBuffer, offset, len);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
/* SHA-1 using WebCryptoAPI */
|
||||
sha1: async function sha1(arrayBuffer, offset, len) {
|
||||
if(typeof window === 'undefined' || typeof window.crypto === 'undefined')
|
||||
throw new Error('Web Crypto API is not available');
|
||||
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
if (u8array.byteLength !== arrayBuffer.byteLength) {
|
||||
arrayBuffer = arrayBuffer.slice(u8array.byteOffset, u8array.byteOffset + u8array.byteLength);
|
||||
}
|
||||
|
||||
const hash = await window.crypto.subtle.digest('SHA-1', arrayBuffer);
|
||||
|
||||
const bytes = new Uint8Array(hash);
|
||||
let hexString = '';
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
hexString += bytes[i] < 16 ? '0' + bytes[i].toString(16) : bytes[i].toString(16);
|
||||
return hexString;
|
||||
},
|
||||
|
||||
/* MD5 - from Joseph's Myers - http://www.myersdaily.org/joseph/javascript/md5.js */
|
||||
md5: function (arrayBuffer, offset, len) {
|
||||
let u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var n = u8array.byteLength, state = [1732584193, -271733879, -1732584194, 271733878], i;
|
||||
for (i = 64; i <= u8array.byteLength; i += 64)
|
||||
_md5cycle(state, _md5blk(u8array.slice(i - 64, i)));
|
||||
u8array = u8array.slice(i - 64);
|
||||
var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for (i = 0; i < u8array.byteLength; i++)
|
||||
tail[i >> 2] |= u8array[i] << ((i % 4) << 3);
|
||||
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
||||
if (i > 55) {
|
||||
_md5cycle(state, tail);
|
||||
for (i = 0; i < 16; i++)tail[i] = 0;
|
||||
}
|
||||
tail[14] = n * 8;
|
||||
tail[15] = Math.floor(n / 536870912) >>> 0; //if file is bigger than 512Mb*8, value is bigger than 32 bits, so it needs two words to store its length
|
||||
_md5cycle(state, tail);
|
||||
|
||||
for (var i = 0; i < state.length; i++) {
|
||||
var s = '', j = 0;
|
||||
for (; j < 4; j++)
|
||||
s += HEX_CHR[(state[i] >> (j * 8 + 4)) & 0x0f] + HEX_CHR[(state[i] >> (j * 8)) & 0x0f];
|
||||
state[i] = s;
|
||||
}
|
||||
return state.join('')
|
||||
},
|
||||
|
||||
/* CRC32 - from Alex - https://stackoverflow.com/a/18639999 */
|
||||
crc32: function (arrayBuffer, offset, len) {
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var crc = 0 ^ (-1);
|
||||
|
||||
for (var i = 0; i < u8array.byteLength; i++)
|
||||
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ u8array[i]) & 0xff];
|
||||
|
||||
return ((crc ^ (-1)) >>> 0);
|
||||
},
|
||||
|
||||
/* Adler-32 - https://en.wikipedia.org/wiki/Adler-32#Example_implementation */
|
||||
adler32: function (arrayBuffer, offset, len) {
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var a = 1, b = 0;
|
||||
|
||||
for (var i = 0; i < u8array.byteLength; i++) {
|
||||
a = (a + u8array[i]) % ADLER32_MOD;
|
||||
b = (b + a) % ADLER32_MOD;
|
||||
}
|
||||
|
||||
return ((b << 16) | a) >>> 0;
|
||||
},
|
||||
|
||||
/* CRC16/CCITT-FALSE */
|
||||
crc16: function (arrayBuffer, offset, len) {
|
||||
const u8array = generateUint8Array(arrayBuffer, offset, len);
|
||||
|
||||
var crc = 0xffff;
|
||||
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < u8array.byteLength; i++) {
|
||||
crc ^= u8array[offset++] << 8;
|
||||
for (j = 0; j < 8; ++j) {
|
||||
crc = (crc & 0x8000) >>> 0 ? (crc << 1) ^ 0x1021 : crc << 1;
|
||||
}
|
||||
}
|
||||
|
||||
return crc & 0xffff;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HashCalculator;
|
||||
}
|
114
rom-patcher-js/modules/RomPatcher.format.aps_gba.js
Normal file
114
rom-patcher-js/modules/RomPatcher.format.aps_gba.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
/* APS (GBA) module for Rom Patcher JS v20230331 - Marc Robledo 2017-2023 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(GBA) */
|
||||
|
||||
const APS_GBA_MAGIC='APS1';
|
||||
const APS_GBA_BLOCK_SIZE=0x010000; //64Kb
|
||||
const APS_GBA_RECORD_SIZE=4 + 2 + 2 + APS_GBA_BLOCK_SIZE;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = APSGBA;
|
||||
}
|
||||
function APSGBA(){
|
||||
this.sourceSize=0;
|
||||
this.targetSize=0;
|
||||
this.records=[];
|
||||
}
|
||||
APSGBA.prototype.addRecord=function(offset, sourceCrc16, targetCrc16, xorBytes){
|
||||
this.records.push({
|
||||
offset:offset,
|
||||
sourceCrc16:sourceCrc16,
|
||||
targetCrc16:targetCrc16,
|
||||
xorBytes:xorBytes}
|
||||
);
|
||||
}
|
||||
APSGBA.prototype.toString=function(){
|
||||
var s='Total records: '+this.records.length;
|
||||
s+='\nInput file size: '+this.sourceSize;
|
||||
s+='\nOutput file size: '+this.targetSize;
|
||||
return s
|
||||
}
|
||||
APSGBA.prototype.validateSource=function(sourceFile){
|
||||
if(sourceFile.fileSize!==this.sourceSize)
|
||||
return false;
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
sourceFile.seek(this.records[i].offset);
|
||||
var bytes=sourceFile.readBytes(APS_GBA_BLOCK_SIZE);
|
||||
if(sourceFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE) !== this.records[i].sourceCrc16)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
APSGBA.prototype.export=function(fileName){
|
||||
var patchFileSize=12 + (this.records.length * APS_GBA_RECORD_SIZE);
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.fileName=fileName+'.aps';
|
||||
tempFile.writeString(APS_GBA_MAGIC, APS_GBA_MAGIC.length);
|
||||
tempFile.writeU32(this.sourceSize);
|
||||
tempFile.writeU32(this.targetSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeU32(this.records[i].offset);
|
||||
tempFile.writeU16(this.records[i].sourceCrc16);
|
||||
tempFile.writeU16(this.records[i].targetCrc16);
|
||||
tempFile.writeBytes(this.records[i].xorBytes);
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APSGBA.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
romFile.copyTo(tempFile, 0, romFile.fileSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
romFile.seek(this.records[i].offset);
|
||||
tempFile.seek(this.records[i].offset);
|
||||
for(var j=0; j<APS_GBA_BLOCK_SIZE; j++){
|
||||
tempFile.writeU8(romFile.readU8() ^ this.records[i].xorBytes[j]);
|
||||
}
|
||||
|
||||
if(validate && tempFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE)!==this.records[i].targetCrc16){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APSGBA.MAGIC=APS_GBA_MAGIC;
|
||||
|
||||
APSGBA.fromFile=function(patchFile){
|
||||
patchFile.seek(0);
|
||||
patchFile.littleEndian=true;
|
||||
|
||||
if(
|
||||
patchFile.readString(APS_GBA_MAGIC.length)!==APS_GBA_MAGIC ||
|
||||
patchFile.fileSize < (12 + APS_GBA_RECORD_SIZE) ||
|
||||
(patchFile.fileSize-12)%APS_GBA_RECORD_SIZE!==0
|
||||
)
|
||||
return null;
|
||||
|
||||
var patch=new APSGBA();
|
||||
|
||||
patch.sourceSize=patchFile.readU32();
|
||||
patch.targetSize=patchFile.readU32();
|
||||
|
||||
while(!patchFile.isEOF()){
|
||||
var offset=patchFile.readU32();
|
||||
var sourceCrc16=patchFile.readU16();
|
||||
var targetCrc16=patchFile.readU16();
|
||||
var xorBytes=patchFile.readBytes(APS_GBA_BLOCK_SIZE);
|
||||
|
||||
patch.addRecord(offset, sourceCrc16, targetCrc16, xorBytes);
|
||||
}
|
||||
return patch;
|
||||
}
|
212
rom-patcher-js/modules/RomPatcher.format.aps_n64.js
Normal file
212
rom-patcher-js/modules/RomPatcher.format.aps_n64.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
/* APS (N64) module for Rom Patcher JS v20180930 - Marc Robledo 2017-2018 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(N64) */
|
||||
|
||||
const APS_N64_MAGIC='APS10';
|
||||
const APS_RECORD_RLE=0x0000;
|
||||
const APS_RECORD_SIMPLE=0x01;
|
||||
const APS_N64_MODE=0x01;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = APS;
|
||||
}
|
||||
function APS(){
|
||||
this.records=[];
|
||||
this.headerType=0;
|
||||
this.encodingMethod=0;
|
||||
this.description='no description';
|
||||
|
||||
this.header={};
|
||||
}
|
||||
APS.prototype.addRecord=function(o, d){
|
||||
this.records.push({offset:o, type:APS_RECORD_SIMPLE, data:d})
|
||||
}
|
||||
APS.prototype.addRLERecord=function(o, b, l){
|
||||
this.records.push({offset:o, type:APS_RECORD_RLE, length:l, byte:b})
|
||||
}
|
||||
APS.prototype.toString=function(){
|
||||
var s='Total records: '+this.records.length;
|
||||
s+='\nHeader type: '+this.headerType;
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
s+=' (N64)';
|
||||
}
|
||||
s+='\nEncoding method: '+this.encodingMethod;
|
||||
s+='\nDescription: '+this.description;
|
||||
s+='\nHeader: '+JSON.stringify(this.header);
|
||||
return s
|
||||
}
|
||||
APS.prototype.validateSource=function(sourceFile){
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
sourceFile.seek(0x3c);
|
||||
if(sourceFile.readString(3)!==this.header.cartId)
|
||||
return false;
|
||||
|
||||
sourceFile.seek(0x10);
|
||||
var crc=sourceFile.readBytes(8);
|
||||
for(var i=0; i<8; i++){
|
||||
if(crc[i]!==this.header.crc[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
APS.prototype.getValidationInfo=function(){
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
return this.header.cartId + ' (' + this.header.crc.reduce(function(hex, b){
|
||||
if(b<16)
|
||||
return hex + '0' + b.toString(16);
|
||||
else
|
||||
return hex + b.toString(16);
|
||||
}, '') + ')';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
APS.prototype.export=function(fileName){
|
||||
var patchFileSize=61;
|
||||
if(this.headerType===APS_N64_MODE)
|
||||
patchFileSize+=17;
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===APS_RECORD_RLE)
|
||||
patchFileSize+=7;
|
||||
else
|
||||
patchFileSize+=5+this.records[i].data.length; //offset+length+data
|
||||
}
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.fileName=fileName+'.aps';
|
||||
tempFile.writeString(APS_N64_MAGIC, APS_N64_MAGIC.length);
|
||||
tempFile.writeU8(this.headerType);
|
||||
tempFile.writeU8(this.encodingMethod);
|
||||
tempFile.writeString(this.description, 50);
|
||||
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
tempFile.writeU8(this.header.originalN64Format);
|
||||
tempFile.writeString(this.header.cartId, 3);
|
||||
tempFile.writeBytes(this.header.crc);
|
||||
tempFile.writeBytes(this.header.pad);
|
||||
}
|
||||
tempFile.writeU32(this.header.sizeOutput);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
tempFile.writeU32(rec.offset);
|
||||
if(rec.type===APS_RECORD_RLE){
|
||||
tempFile.writeU8(0x00);
|
||||
tempFile.writeU8(rec.byte);
|
||||
tempFile.writeU8(rec.length);
|
||||
}else{
|
||||
tempFile.writeU8(rec.data.length);
|
||||
tempFile.writeBytes(rec.data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
tempFile=new BinFile(this.header.sizeOutput);
|
||||
romFile.copyTo(tempFile, 0, tempFile.fileSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
if(this.records[i].type===APS_RECORD_RLE){
|
||||
for(var j=0; j<this.records[i].length; j++)
|
||||
tempFile.writeU8(this.records[i].byte);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APS.MAGIC=APS_N64_MAGIC;
|
||||
|
||||
|
||||
|
||||
APS.fromFile=function(patchFile){
|
||||
var patch=new APS();
|
||||
patchFile.littleEndian=true;
|
||||
|
||||
patchFile.seek(5);
|
||||
patch.headerType=patchFile.readU8();
|
||||
patch.encodingMethod=patchFile.readU8();
|
||||
patch.description=patchFile.readString(50);
|
||||
|
||||
var seek;
|
||||
if(patch.headerType===APS_N64_MODE){
|
||||
patch.header.originalN64Format=patchFile.readU8();
|
||||
patch.header.cartId=patchFile.readString(3);
|
||||
patch.header.crc=patchFile.readBytes(8);
|
||||
patch.header.pad=patchFile.readBytes(5);
|
||||
}
|
||||
patch.header.sizeOutput=patchFile.readU32();
|
||||
|
||||
while(!patchFile.isEOF()){
|
||||
var offset=patchFile.readU32();
|
||||
var length=patchFile.readU8();
|
||||
|
||||
if(length===APS_RECORD_RLE)
|
||||
patch.addRLERecord(offset, patchFile.readU8(seek), patchFile.readU8(seek+1));
|
||||
else
|
||||
patch.addRecord(offset, patchFile.readBytes(length));
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
APS.buildFromRoms=function(original, modified){
|
||||
var patch=new APS();
|
||||
|
||||
|
||||
|
||||
if(original.readU32()===0x80371240){ //is N64 ROM
|
||||
patch.headerType=APS_N64_MODE;
|
||||
|
||||
patch.header.originalN64Format=/\.v64$/i.test(original.fileName)?0:1;
|
||||
original.seek(0x3c);
|
||||
patch.header.cartId=original.readString(3);
|
||||
original.seek(0x10);
|
||||
patch.header.crc=original.readBytes(8);
|
||||
patch.header.pad=[0,0,0,0,0];
|
||||
}
|
||||
patch.header.sizeOutput=modified.fileSize;
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var RLERecord=true;
|
||||
var differentBytes=[];
|
||||
var offset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentBytes.length<0xff){
|
||||
differentBytes.push(b2);
|
||||
if(b2!==differentBytes[0])
|
||||
RLERecord=false;
|
||||
|
||||
if(modified.isEOF() || differentBytes.length===0xff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
if(RLERecord && differentBytes.length>2){
|
||||
patch.addRLERecord(offset, differentBytes[0], differentBytes.length);
|
||||
}else{
|
||||
patch.addRecord(offset, differentBytes);
|
||||
}
|
||||
//NO se puede comentar??? why????
|
||||
}
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
463
rom-patcher-js/modules/RomPatcher.format.bps.js
Normal file
463
rom-patcher-js/modules/RomPatcher.format.bps.js
Normal file
|
@ -0,0 +1,463 @@
|
|||
/* BPS module for Rom Patcher JS v20240721 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://www.romhacking.net/documents/746/ */
|
||||
|
||||
const BPS_MAGIC='BPS1';
|
||||
const BPS_ACTION_SOURCE_READ=0;
|
||||
const BPS_ACTION_TARGET_READ=1;
|
||||
const BPS_ACTION_SOURCE_COPY=2;
|
||||
const BPS_ACTION_TARGET_COPY=3;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = BPS;
|
||||
}
|
||||
|
||||
function BPS(){
|
||||
this.sourceSize=0;
|
||||
this.targetSize=0;
|
||||
this.metaData='';
|
||||
this.actions=[];
|
||||
this.sourceChecksum=0;
|
||||
this.targetChecksum=0;
|
||||
this.patchChecksum=0;
|
||||
}
|
||||
BPS.prototype.toString=function(){
|
||||
var s='Source size: '+this.sourceSize;
|
||||
s+='\nTarget size: '+this.targetSize;
|
||||
s+='\nMetadata: '+this.metaData;
|
||||
s+='\n#Actions: '+this.actions.length;
|
||||
return s
|
||||
}
|
||||
BPS.prototype.validateSource=function(romFile,headerSize){return this.sourceChecksum===romFile.hashCRC32(headerSize)}
|
||||
BPS.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':this.sourceChecksum
|
||||
}
|
||||
}
|
||||
BPS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
|
||||
|
||||
//patch
|
||||
var sourceRelativeOffset=0;
|
||||
var targetRelativeOffset=0;
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
|
||||
if(action.type===BPS_ACTION_SOURCE_READ){
|
||||
romFile.copyTo(tempFile, tempFile.offset, action.length);
|
||||
tempFile.skip(action.length);
|
||||
|
||||
}else if(action.type===BPS_ACTION_TARGET_READ){
|
||||
tempFile.writeBytes(action.bytes);
|
||||
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY){
|
||||
sourceRelativeOffset+=action.relativeOffset;
|
||||
var actionLength=action.length;
|
||||
while(actionLength--){
|
||||
tempFile.writeU8(romFile._u8array[sourceRelativeOffset]);
|
||||
sourceRelativeOffset++;
|
||||
}
|
||||
}else if(action.type===BPS_ACTION_TARGET_COPY){
|
||||
targetRelativeOffset+=action.relativeOffset;
|
||||
var actionLength=action.length;
|
||||
while(actionLength--) {
|
||||
tempFile.writeU8(tempFile._u8array[targetRelativeOffset]);
|
||||
targetRelativeOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(validate && this.targetChecksum!==tempFile.hashCRC32()){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
BPS.MAGIC=BPS_MAGIC;
|
||||
|
||||
|
||||
BPS.fromFile=function(file){
|
||||
file.readVLV=BPS_readVLV;
|
||||
|
||||
file.littleEndian=true;
|
||||
var patch=new BPS();
|
||||
|
||||
|
||||
file.seek(4); //skip BPS1
|
||||
|
||||
patch.sourceSize=file.readVLV();
|
||||
patch.targetSize=file.readVLV();
|
||||
|
||||
var metaDataLength=file.readVLV();
|
||||
if(metaDataLength){
|
||||
patch.metaData=file.readString(metaDataLength);
|
||||
}
|
||||
|
||||
|
||||
var endActionsOffset=file.fileSize-12;
|
||||
while(file.offset<endActionsOffset){
|
||||
var data=file.readVLV();
|
||||
var action={type: data & 3, length: (data >> 2)+1};
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
action.bytes=file.readBytes(action.length);
|
||||
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
var relativeOffset=file.readVLV();
|
||||
action.relativeOffset=(relativeOffset & 1? -1 : +1) * (relativeOffset >> 1)
|
||||
}
|
||||
|
||||
patch.actions.push(action);
|
||||
}
|
||||
|
||||
//file.seek(endActionsOffset);
|
||||
patch.sourceChecksum=file.readU32();
|
||||
patch.targetChecksum=file.readU32();
|
||||
patch.patchChecksum=file.readU32();
|
||||
|
||||
if(patch.patchChecksum!==file.hashCRC32(0, file.fileSize - 4)){
|
||||
throw new Error('Patch checksum mismatch');
|
||||
}
|
||||
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function BPS_readVLV(){
|
||||
var data=0, shift=1;
|
||||
while(true){
|
||||
var x = this.readU8();
|
||||
data += (x & 0x7f) * shift;
|
||||
if(x & 0x80)
|
||||
break;
|
||||
shift <<= 7;
|
||||
data += shift;
|
||||
}
|
||||
|
||||
this._lastRead=data;
|
||||
return data;
|
||||
}
|
||||
function BPS_writeVLV(data){
|
||||
while(true){
|
||||
var x = data & 0x7f;
|
||||
data >>= 7;
|
||||
if(data === 0){
|
||||
this.writeU8(0x80 | x);
|
||||
break;
|
||||
}
|
||||
this.writeU8(x);
|
||||
data--;
|
||||
}
|
||||
}
|
||||
function BPS_getVLVLen(data){
|
||||
var len=0;
|
||||
while(true){
|
||||
var x = data & 0x7f;
|
||||
data >>= 7;
|
||||
if(data === 0){
|
||||
len++;
|
||||
break;
|
||||
}
|
||||
len++;
|
||||
data--;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
BPS.prototype.export=function(fileName){
|
||||
var patchFileSize=BPS_MAGIC.length;
|
||||
patchFileSize+=BPS_getVLVLen(this.sourceSize);
|
||||
patchFileSize+=BPS_getVLVLen(this.targetSize);
|
||||
patchFileSize+=BPS_getVLVLen(this.metaData.length);
|
||||
patchFileSize+=this.metaData.length;
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
patchFileSize+=BPS_getVLVLen(((action.length-1)<<2) + action.type);
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
patchFileSize+=action.length;
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
patchFileSize+=BPS_getVLVLen((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
|
||||
}
|
||||
}
|
||||
patchFileSize+=12;
|
||||
|
||||
var patchFile=new BinFile(patchFileSize);
|
||||
patchFile.fileName=fileName+'.bps';
|
||||
patchFile.littleEndian=true;
|
||||
patchFile.writeVLV=BPS_writeVLV;
|
||||
|
||||
patchFile.writeString(BPS_MAGIC);
|
||||
patchFile.writeVLV(this.sourceSize);
|
||||
patchFile.writeVLV(this.targetSize);
|
||||
patchFile.writeVLV(this.metaData.length);
|
||||
patchFile.writeString(this.metaData, this.metaData.length);
|
||||
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
patchFile.writeVLV(((action.length-1)<<2) + action.type);
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
patchFile.writeBytes(action.bytes);
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
patchFile.writeVLV((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
|
||||
}
|
||||
}
|
||||
patchFile.writeU32(this.sourceChecksum);
|
||||
patchFile.writeU32(this.targetChecksum);
|
||||
patchFile.writeU32(this.patchChecksum);
|
||||
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
function BPS_Node(){
|
||||
this.offset=0;
|
||||
this.next=null;
|
||||
};
|
||||
BPS_Node.prototype.delete=function(){
|
||||
if(this.next)
|
||||
delete this.next;
|
||||
}
|
||||
BPS.buildFromRoms=function(original, modified, deltaMode){
|
||||
var patch=new BPS();
|
||||
patch.sourceSize=original.fileSize;
|
||||
patch.targetSize=modified.fileSize;
|
||||
|
||||
if(deltaMode){
|
||||
patch.actions=createBPSFromFilesDelta(original, modified);
|
||||
}else{
|
||||
patch.actions=createBPSFromFilesLinear(original, modified);
|
||||
}
|
||||
|
||||
var patchFile=patch.export();
|
||||
patch.sourceChecksum=original.hashCRC32();
|
||||
patch.targetChecksum=modified.hashCRC32();
|
||||
patch.patchChecksum=patchFile.hashCRC32(0, patchFile.fileSize - 4);
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/linear.hpp */
|
||||
function createBPSFromFilesLinear(original, modified){
|
||||
var patchActions=[];
|
||||
|
||||
/* references to match original beat code */
|
||||
var sourceData=original._u8array;
|
||||
var targetData=modified._u8array;
|
||||
var sourceSize=original.fileSize;
|
||||
var targetSize=modified.fileSize;
|
||||
var Granularity=1;
|
||||
|
||||
|
||||
|
||||
var targetRelativeOffset=0;
|
||||
var outputOffset=0;
|
||||
var targetReadLength=0;
|
||||
|
||||
function targetReadFlush(){
|
||||
if(targetReadLength){
|
||||
//encode(TargetRead | ((targetReadLength - 1) << 2));
|
||||
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
|
||||
patchActions.push(action);
|
||||
var offset = outputOffset - targetReadLength;
|
||||
while(targetReadLength){
|
||||
//write(targetData[offset++]);
|
||||
action.bytes.push(targetData[offset++]);
|
||||
targetReadLength--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while(outputOffset < targetSize) {
|
||||
var sourceLength = 0;
|
||||
for(var n = 0; outputOffset + n < Math.min(sourceSize, targetSize); n++) {
|
||||
if(sourceData[outputOffset + n] != targetData[outputOffset + n]) break;
|
||||
sourceLength++;
|
||||
}
|
||||
|
||||
var rleLength = 0;
|
||||
for(var n = 1; outputOffset + n < targetSize; n++) {
|
||||
if(targetData[outputOffset] != targetData[outputOffset + n]) break;
|
||||
rleLength++;
|
||||
}
|
||||
|
||||
if(rleLength >= 4) {
|
||||
//write byte to repeat
|
||||
targetReadLength++;
|
||||
outputOffset++;
|
||||
targetReadFlush();
|
||||
|
||||
//copy starting from repetition byte
|
||||
//encode(TargetCopy | ((rleLength - 1) << 2));
|
||||
var relativeOffset = (outputOffset - 1) - targetRelativeOffset;
|
||||
//encode(relativeOffset << 1);
|
||||
patchActions.push({type:BPS_ACTION_TARGET_COPY, length:rleLength, relativeOffset:relativeOffset});
|
||||
outputOffset += rleLength;
|
||||
targetRelativeOffset = outputOffset - 1;
|
||||
} else if(sourceLength >= 4) {
|
||||
targetReadFlush();
|
||||
//encode(SourceRead | ((sourceLength - 1) << 2));
|
||||
patchActions.push({type:BPS_ACTION_SOURCE_READ, length:sourceLength});
|
||||
outputOffset += sourceLength;
|
||||
} else {
|
||||
targetReadLength += Granularity;
|
||||
outputOffset += Granularity;
|
||||
}
|
||||
}
|
||||
|
||||
targetReadFlush();
|
||||
|
||||
|
||||
|
||||
return patchActions;
|
||||
}
|
||||
|
||||
/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/delta.hpp */
|
||||
function createBPSFromFilesDelta(original, modified){
|
||||
var patchActions=[];
|
||||
|
||||
|
||||
/* references to match original beat code */
|
||||
var sourceData=original._u8array;
|
||||
var targetData=modified._u8array;
|
||||
var sourceSize=original.fileSize;
|
||||
var targetSize=modified.fileSize;
|
||||
var Granularity=1;
|
||||
|
||||
|
||||
|
||||
var sourceRelativeOffset=0;
|
||||
var targetRelativeOffset=0;
|
||||
var outputOffset=0;
|
||||
|
||||
|
||||
|
||||
var sourceTree=new Array(65536);
|
||||
var targetTree=new Array(65536);
|
||||
for(var n=0; n<65536; n++){
|
||||
sourceTree[n]=null;
|
||||
targetTree[n]=null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//source tree creation
|
||||
for(var offset=0; offset<sourceSize; offset++) {
|
||||
var symbol = sourceData[offset + 0];
|
||||
//sourceChecksum = crc32_adjust(sourceChecksum, symbol);
|
||||
if(offset < sourceSize - 1)
|
||||
symbol |= sourceData[offset + 1] << 8;
|
||||
var node=new BPS_Node();
|
||||
node.offset=offset;
|
||||
node.next=sourceTree[symbol];
|
||||
sourceTree[symbol] = node;
|
||||
}
|
||||
|
||||
var targetReadLength=0;
|
||||
|
||||
function targetReadFlush(){
|
||||
if(targetReadLength) {
|
||||
//encode(TargetRead | ((targetReadLength - 1) << 2));
|
||||
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
|
||||
patchActions.push(action);
|
||||
var offset = outputOffset - targetReadLength;
|
||||
while(targetReadLength){
|
||||
//write(targetData[offset++]);
|
||||
action.bytes.push(targetData[offset++]);
|
||||
targetReadLength--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while(outputOffset<modified.fileSize){
|
||||
var maxLength = 0, maxOffset = 0, mode = BPS_ACTION_TARGET_READ;
|
||||
|
||||
var symbol = targetData[outputOffset + 0];
|
||||
if(outputOffset < targetSize - 1) symbol |= targetData[outputOffset + 1] << 8;
|
||||
|
||||
{ //source read
|
||||
var length = 0, offset = outputOffset;
|
||||
while(offset < sourceSize && offset < targetSize && sourceData[offset] == targetData[offset]) {
|
||||
length++;
|
||||
offset++;
|
||||
}
|
||||
if(length > maxLength) maxLength = length, mode = BPS_ACTION_SOURCE_READ;
|
||||
}
|
||||
|
||||
{ //source copy
|
||||
var node = sourceTree[symbol];
|
||||
while(node) {
|
||||
var length = 0, x = node.offset, y = outputOffset;
|
||||
while(x < sourceSize && y < targetSize && sourceData[x++] == targetData[y++]) length++;
|
||||
if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_SOURCE_COPY;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
|
||||
{ //target copy
|
||||
var node = targetTree[symbol];
|
||||
while(node) {
|
||||
var length = 0, x = node.offset, y = outputOffset;
|
||||
while(y < targetSize && targetData[x++] == targetData[y++]) length++;
|
||||
if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_TARGET_COPY;
|
||||
node = node.next;
|
||||
}
|
||||
|
||||
//target tree append
|
||||
node = new BPS_Node();
|
||||
node.offset = outputOffset;
|
||||
node.next = targetTree[symbol];
|
||||
targetTree[symbol] = node;
|
||||
}
|
||||
|
||||
{ //target read
|
||||
if(maxLength < 4) {
|
||||
maxLength = Math.min(Granularity, targetSize - outputOffset);
|
||||
mode = BPS_ACTION_TARGET_READ;
|
||||
}
|
||||
}
|
||||
|
||||
if(mode != BPS_ACTION_TARGET_READ) targetReadFlush();
|
||||
|
||||
switch(mode) {
|
||||
case BPS_ACTION_SOURCE_READ:
|
||||
//encode(BPS_ACTION_SOURCE_READ | ((maxLength - 1) << 2));
|
||||
patchActions.push({type:BPS_ACTION_SOURCE_READ, length:maxLength});
|
||||
break;
|
||||
case BPS_ACTION_TARGET_READ:
|
||||
//delay write to group sequential TargetRead commands into one
|
||||
targetReadLength += maxLength;
|
||||
break;
|
||||
case BPS_ACTION_SOURCE_COPY:
|
||||
case BPS_ACTION_TARGET_COPY:
|
||||
//encode(mode | ((maxLength - 1) << 2));
|
||||
var relativeOffset;
|
||||
if(mode == BPS_ACTION_SOURCE_COPY) {
|
||||
relativeOffset = maxOffset - sourceRelativeOffset;
|
||||
sourceRelativeOffset = maxOffset + maxLength;
|
||||
} else {
|
||||
relativeOffset = maxOffset - targetRelativeOffset;
|
||||
targetRelativeOffset = maxOffset + maxLength;
|
||||
}
|
||||
//encode((relativeOffset < 0) | (abs(relativeOffset) << 1));
|
||||
patchActions.push({type:mode, length:maxLength, relativeOffset:relativeOffset});
|
||||
break;
|
||||
}
|
||||
|
||||
outputOffset += maxLength;
|
||||
}
|
||||
|
||||
targetReadFlush();
|
||||
|
||||
|
||||
return patchActions;
|
||||
}
|
235
rom-patcher-js/modules/RomPatcher.format.ips.js
Normal file
235
rom-patcher-js/modules/RomPatcher.format.ips.js
Normal file
|
@ -0,0 +1,235 @@
|
|||
/* IPS module for Rom Patcher JS v20230924 - Marc Robledo 2016-2023 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */
|
||||
|
||||
const IPS_MAGIC='PATCH';
|
||||
const IPS_MAX_ROM_SIZE=0x1000000; //16 megabytes
|
||||
const IPS_RECORD_RLE=0x0000;
|
||||
const IPS_RECORD_SIMPLE=0x01;
|
||||
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = IPS;
|
||||
}
|
||||
|
||||
|
||||
function IPS(){
|
||||
this.records=[];
|
||||
this.truncate=false;
|
||||
}
|
||||
IPS.prototype.addSimpleRecord=function(o, d){
|
||||
this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d})
|
||||
}
|
||||
IPS.prototype.addRLERecord=function(o, l, b){
|
||||
this.records.push({offset:o, type:IPS_RECORD_RLE, length:l, byte:b})
|
||||
}
|
||||
IPS.prototype.toString=function(){
|
||||
nSimpleRecords=0;
|
||||
nRLERecords=0;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===IPS_RECORD_RLE)
|
||||
nRLERecords++;
|
||||
else
|
||||
nSimpleRecords++;
|
||||
}
|
||||
var s='Simple records: '+nSimpleRecords;
|
||||
s+='\nRLE records: '+nRLERecords;
|
||||
s+='\nTotal records: '+this.records.length;
|
||||
if(this.truncate)
|
||||
s+='\nTruncate at: 0x'+this.truncate.toString(16);
|
||||
return s
|
||||
}
|
||||
IPS.prototype.export=function(fileName){
|
||||
var patchFileSize=5; //PATCH string
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===IPS_RECORD_RLE)
|
||||
patchFileSize+=(3+2+2+1); //offset+0x0000+length+RLE byte to be written
|
||||
else
|
||||
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
|
||||
}
|
||||
patchFileSize+=3; //EOF string
|
||||
if(this.truncate)
|
||||
patchFileSize+=3; //truncate
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.fileName=fileName+'.ips';
|
||||
tempFile.writeString(IPS_MAGIC);
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
tempFile.writeU24(rec.offset);
|
||||
if(rec.type===IPS_RECORD_RLE){
|
||||
tempFile.writeU16(0x0000);
|
||||
tempFile.writeU16(rec.length);
|
||||
tempFile.writeU8(rec.byte);
|
||||
}else{
|
||||
tempFile.writeU16(rec.data.length);
|
||||
tempFile.writeBytes(rec.data);
|
||||
}
|
||||
}
|
||||
|
||||
tempFile.writeString('EOF');
|
||||
if(this.truncate)
|
||||
tempFile.writeU24(this.truncate);
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
IPS.prototype.apply=function(romFile){
|
||||
if(this.truncate){
|
||||
if(this.truncate>romFile.fileSize){ //expand (discussed here: https://github.com/marcrobledo/RomPatcher.js/pull/46)
|
||||
tempFile=new BinFile(this.truncate);
|
||||
romFile.copyTo(tempFile, 0, romFile.fileSize, 0);
|
||||
}else{ //truncate
|
||||
tempFile=romFile.slice(0, this.truncate);
|
||||
}
|
||||
}else{
|
||||
//calculate target ROM size, expanding it if any record offset is beyond target ROM size
|
||||
var newFileSize=romFile.fileSize;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
if(rec.type===IPS_RECORD_RLE){
|
||||
if(rec.offset+rec.length>newFileSize){
|
||||
newFileSize=rec.offset+rec.length;
|
||||
}
|
||||
}else{
|
||||
if(rec.offset+rec.data.length>newFileSize){
|
||||
newFileSize=rec.offset+rec.data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newFileSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(newFileSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
romFile.seek(0);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
if(this.records[i].type===IPS_RECORD_RLE){
|
||||
for(var j=0; j<this.records[i].length; j++)
|
||||
tempFile.writeU8(this.records[i].byte);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
IPS.MAGIC=IPS_MAGIC;
|
||||
|
||||
|
||||
IPS.fromFile=function(file){
|
||||
var patchFile=new IPS();
|
||||
file.seek(5);
|
||||
|
||||
while(!file.isEOF()){
|
||||
var offset=file.readU24();
|
||||
|
||||
if(offset===0x454f46){ /* EOF */
|
||||
if(file.isEOF()){
|
||||
break;
|
||||
}else if((file.offset+3)===file.fileSize){
|
||||
patchFile.truncate=file.readU24();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var length=file.readU16();
|
||||
|
||||
if(length===IPS_RECORD_RLE){
|
||||
patchFile.addRLERecord(offset, file.readU16(), file.readU8());
|
||||
}else{
|
||||
patchFile.addSimpleRecord(offset, file.readBytes(length));
|
||||
}
|
||||
}
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
|
||||
IPS.buildFromRoms=function(original, modified){
|
||||
var patch=new IPS();
|
||||
|
||||
if(modified.fileSize<original.fileSize){
|
||||
patch.truncate=modified.fileSize;
|
||||
}
|
||||
|
||||
//solucion: guardar startOffset y endOffset (ir mirando de 6 en 6 hacia atrás)
|
||||
var previousRecord={type:0xdeadbeef,startOffset:0,length:0};
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var RLEmode=true;
|
||||
var differentData=[];
|
||||
var startOffset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentData.length<0xffff){
|
||||
differentData.push(b2);
|
||||
if(b2!==differentData[0])
|
||||
RLEmode=false;
|
||||
|
||||
if(modified.isEOF() || differentData.length===0xffff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
|
||||
//check if this record is near the previous one
|
||||
var distance=startOffset-(previousRecord.offset+previousRecord.length);
|
||||
if(
|
||||
previousRecord.type===IPS_RECORD_SIMPLE &&
|
||||
distance<6 && (previousRecord.length+distance+differentData.length)<0xffff
|
||||
){
|
||||
if(RLEmode && differentData.length>6){
|
||||
// separate a potential RLE record
|
||||
original.seek(startOffset);
|
||||
modified.seek(startOffset);
|
||||
previousRecord={type:0xdeadbeef,startOffset:0,length:0};
|
||||
}else{
|
||||
// merge both records
|
||||
while(distance--){
|
||||
previousRecord.data.push(modified._u8array[previousRecord.offset+previousRecord.length]);
|
||||
previousRecord.length++;
|
||||
}
|
||||
previousRecord.data=previousRecord.data.concat(differentData);
|
||||
previousRecord.length=previousRecord.data.length;
|
||||
}
|
||||
}else{
|
||||
if(startOffset>=IPS_MAX_ROM_SIZE){
|
||||
throw new Error('Files are too big for IPS format');
|
||||
return null;
|
||||
}
|
||||
|
||||
if(RLEmode && differentData.length>2){
|
||||
patch.addRLERecord(startOffset, differentData.length, differentData[0]);
|
||||
}else{
|
||||
patch.addSimpleRecord(startOffset, differentData);
|
||||
}
|
||||
previousRecord=patch.records[patch.records.length-1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if(modified.fileSize>original.fileSize){
|
||||
var lastRecord=patch.records[patch.records.length-1];
|
||||
var lastOffset=lastRecord.offset+lastRecord.length;
|
||||
|
||||
if(lastOffset<modified.fileSize){
|
||||
patch.addSimpleRecord(modified.fileSize-1, [0x00]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return patch
|
||||
}
|
97
rom-patcher-js/modules/RomPatcher.format.pmsr.js
Normal file
97
rom-patcher-js/modules/RomPatcher.format.pmsr.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/* PMSR (Paper Mario Star Rod) module for Rom Patcher JS v20240721 - Marc Robledo 2020-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://origami64.net/attachment.php?aid=790 (dead link) */
|
||||
|
||||
const PMSR_MAGIC='PMSR';
|
||||
const YAY0_MAGIC='Yay0';
|
||||
const PAPER_MARIO_USA10_CRC32=0xa7f5cd7e;
|
||||
const PAPER_MARIO_USA10_FILE_SIZE=41943040;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = PMSR;
|
||||
}
|
||||
|
||||
function PMSR(){
|
||||
this.targetSize=0;
|
||||
this.records=[];
|
||||
}
|
||||
PMSR.prototype.addRecord=function(offset, data){
|
||||
this.records.push({offset:offset, data:data})
|
||||
}
|
||||
PMSR.prototype.toString=function(){
|
||||
var s='Star Rod patch';
|
||||
s+='\nTarget file size: '+this.targetSize;
|
||||
s+='\n#Records: '+this.records.length;
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
PMSR.prototype.validateSource=function(romFile){
|
||||
return romFile.fileSize===PAPER_MARIO_USA10_FILE_SIZE && romFile.hashCRC32()===PAPER_MARIO_USA10_CRC32;
|
||||
}
|
||||
PMSR.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':PAPER_MARIO_USA10_CRC32
|
||||
}
|
||||
}
|
||||
PMSR.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
console.log('a');
|
||||
if(this.targetSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
|
||||
console.log('b');
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
PMSR.MAGIC=PMSR_MAGIC;
|
||||
|
||||
PMSR.fromFile=function(file){
|
||||
var patch=new PMSR();
|
||||
|
||||
/*file.seek(0);
|
||||
if(file.readString(YAY0_MAGIC.length)===YAY0_MAGIC){
|
||||
file=PMSR.YAY0_decode(file);
|
||||
}*/
|
||||
|
||||
patch.targetSize=PAPER_MARIO_USA10_FILE_SIZE;
|
||||
|
||||
file.seek(4);
|
||||
var nRecords=file.readU32();
|
||||
|
||||
for(var i=0; i<nRecords; i++){
|
||||
var offset=file.readU32();
|
||||
var length=file.readU32();
|
||||
patch.addRecord(offset, file.readBytes(length));
|
||||
|
||||
if((offset+length)>patch.targetSize)
|
||||
patch.targetSize=offset+length;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* to-do */
|
||||
//PMSR.prototype.export=function(fileName){return null}
|
||||
//PMSR.buildFromRoms=function(original, modified){return null}
|
||||
|
||||
|
||||
/* https://github.com/pho/WindViewer/wiki/Yaz0-and-Yay0 */
|
||||
PMSR.YAY0_decode=function(file){
|
||||
/* to-do */
|
||||
}
|
269
rom-patcher-js/modules/RomPatcher.format.ppf.js
Normal file
269
rom-patcher-js/modules/RomPatcher.format.ppf.js
Normal file
|
@ -0,0 +1,269 @@
|
|||
/* PPF module for Rom Patcher JS v20200221 - Marc Robledo 2019-2020 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://www.romhacking.net/utilities/353/ */
|
||||
|
||||
|
||||
const PPF_MAGIC='PPF';
|
||||
const PPF_IMAGETYPE_BIN=0x00;
|
||||
const PPF_IMAGETYPE_GI=0x01;
|
||||
const PPF_BEGIN_FILE_ID_DIZ_MAGIC='@BEG';//@BEGIN_FILE_ID.DIZ
|
||||
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = PPF;
|
||||
}
|
||||
function PPF(){
|
||||
this.version=3;
|
||||
this.imageType=PPF_IMAGETYPE_BIN;
|
||||
this.blockCheck=false;
|
||||
this.undoData=false;
|
||||
this.records=[];
|
||||
}
|
||||
PPF.prototype.addRecord=function(offset, data, undoData){
|
||||
if(this.undoData){
|
||||
this.records.push({offset:offset, data:data, undoData:undoData});
|
||||
}else{
|
||||
this.records.push({offset:offset, data:data});
|
||||
}
|
||||
}
|
||||
PPF.prototype.toString=function(){
|
||||
var s=this.description;
|
||||
s+='\nPPF version: '+this.version;
|
||||
s+='\n#Records: '+this.records.length;
|
||||
s+='\nImage type: '+this.imageType;
|
||||
s+='\nBlock check: '+!!this.blockCheck;
|
||||
s+='\nUndo data: '+this.undoData;
|
||||
if(this.fileIdDiz)
|
||||
s+='\nFILE_ID.DIZ: '+this.fileIdDiz;
|
||||
return s
|
||||
}
|
||||
PPF.prototype.export=function(fileName){
|
||||
var patchFileSize=5+1+50; //PPFx0
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
patchFileSize+=4+1+this.records[i].data.length;
|
||||
if(this.version===3)
|
||||
patchFileSize+=4; //offsets are u64
|
||||
}
|
||||
|
||||
if(this.version===3 || this.version===2){
|
||||
patchFileSize+=4;
|
||||
}
|
||||
if(this.blockCheck){
|
||||
patchFileSize+=1024;
|
||||
}
|
||||
if(this.fileIdDiz){
|
||||
patchFileSize+=18+this.fileIdDiz.length+16+4;
|
||||
}
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.fileName=fileName+'.ppf';
|
||||
tempFile.writeString(PPF_MAGIC);
|
||||
tempFile.writeString((this.version*10).toString());
|
||||
tempFile.writeU8(parseInt(this.version)-1);
|
||||
tempFile.writeString(this.description, 50);
|
||||
|
||||
|
||||
if(this.version===3){
|
||||
tempFile.writeU8(this.imageType);
|
||||
tempFile.writeU8(this.blockCheck?0x01:0x00);
|
||||
tempFile.writeU8(this.undoData?0x01:0x00);
|
||||
tempFile.writeU8(0x00); //dummy
|
||||
|
||||
}else if(this.version===2){
|
||||
tempFile.writeU32(this.inputFileSize);
|
||||
}
|
||||
|
||||
if(this.blockCheck){
|
||||
tempFile.writeBytes(this.blockCheck);
|
||||
}
|
||||
|
||||
|
||||
|
||||
tempFile.littleEndian=true;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeU32(this.records[i].offset & 0xffffffff);
|
||||
|
||||
if(this.version===3){
|
||||
var offset2=this.records[i].offset;
|
||||
for(var j=0; j<32; j++)
|
||||
offset2=parseInt((offset2/2)>>>0);
|
||||
tempFile.writeU32(offset2);
|
||||
}
|
||||
tempFile.writeU8(this.records[i].data.length);
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
if(this.undoData)
|
||||
tempFile.writeBytes(this.records[i].undoData);
|
||||
}
|
||||
|
||||
if(this.fileIdDiz){
|
||||
tempFile.writeString('@BEGIN_FILE_ID.DIZ');
|
||||
tempFile.writeString(this.fileIdDiz);
|
||||
tempFile.writeString('@END_FILE_ID.DIZ');
|
||||
tempFile.writeU16(this.fileIdDiz.length);
|
||||
tempFile.writeU16(0x00);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
PPF.prototype.apply=function(romFile){
|
||||
var newFileSize=romFile.fileSize;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].offset+this.records[i].data.length>newFileSize)
|
||||
newFileSize=this.records[i].offset+this.records[i].data.length;
|
||||
}
|
||||
if(newFileSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(newFileSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
|
||||
//check if undoing
|
||||
var undoingData=false;
|
||||
if(this.undoData){
|
||||
tempFile.seek(this.records[0].offset);
|
||||
var originalBytes=tempFile.readBytes(this.records[0].data.length);
|
||||
var foundDifferences=false;
|
||||
for(var i=0; i<originalBytes.length && !foundDifferences; i++){
|
||||
if(originalBytes[i]!==this.records[0].data[i]){
|
||||
foundDifferences=true;
|
||||
}
|
||||
}
|
||||
if(!foundDifferences){
|
||||
undoingData=true;
|
||||
}
|
||||
}
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
|
||||
if(undoingData){
|
||||
tempFile.writeBytes(this.records[i].undoData);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
PPF.MAGIC=PPF_MAGIC;
|
||||
|
||||
|
||||
|
||||
PPF.fromFile=function(patchFile){
|
||||
var patch=new PPF();
|
||||
|
||||
|
||||
patchFile.seek(3);
|
||||
var version1=parseInt(patchFile.readString(2))/10;
|
||||
var version2=patchFile.readU8()+1;
|
||||
if(version1!==version2 || version1>3){
|
||||
throw new Error('invalid PPF version');
|
||||
}
|
||||
|
||||
patch.version=version1;
|
||||
patch.description=patchFile.readString(50).replace(/ +$/,'');
|
||||
|
||||
|
||||
|
||||
|
||||
if(patch.version===3){
|
||||
patch.imageType=patchFile.readU8();
|
||||
if(patchFile.readU8())
|
||||
patch.blockCheck=true;
|
||||
if(patchFile.readU8())
|
||||
patch.undoData=true;
|
||||
|
||||
patchFile.skip(1);
|
||||
}else if(patch.version===2){
|
||||
patch.blockCheck=true;
|
||||
patch.inputFileSize=patchFile.readU32();
|
||||
}
|
||||
|
||||
if(patch.blockCheck){
|
||||
patch.blockCheck=patchFile.readBytes(1024);
|
||||
}
|
||||
|
||||
|
||||
|
||||
patchFile.littleEndian=true;
|
||||
while(!patchFile.isEOF()){
|
||||
|
||||
if(patchFile.readString(4)===PPF_BEGIN_FILE_ID_DIZ_MAGIC){
|
||||
patchFile.skip(14);
|
||||
//console.log('found file_id.diz begin');
|
||||
patch.fileIdDiz=patchFile.readString(3072);
|
||||
patch.fileIdDiz=patch.fileIdDiz.substr(0, patch.fileIdDiz.indexOf('@END_FILE_ID.DIZ'));
|
||||
break;
|
||||
}
|
||||
patchFile.skip(-4);
|
||||
|
||||
var offset;
|
||||
if(patch.version===3){
|
||||
var u64_1=patchFile.readU32();
|
||||
var u64_2=patchFile.readU32();
|
||||
offset=u64_1+(u64_2*0x100000000);
|
||||
}else
|
||||
offset=patchFile.readU32();
|
||||
|
||||
var len=patchFile.readU8();
|
||||
var data=patchFile.readBytes(len);
|
||||
|
||||
var undoData=false;
|
||||
if(patch.undoData){
|
||||
undoData=patchFile.readBytes(len);
|
||||
}
|
||||
|
||||
patch.addRecord(offset, data, undoData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
PPF.buildFromRoms=function(original, modified){
|
||||
var patch=new PPF();
|
||||
|
||||
patch.description='Patch description';
|
||||
|
||||
if(original.fileSize>modified.fileSize){
|
||||
var expandedModified=new BinFile(original.fileSize);
|
||||
modified.copyTo(expandedModified,0);
|
||||
modified=expandedModified;
|
||||
}
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var differentData=[];
|
||||
var offset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentData.length<0xff){
|
||||
differentData.push(b2);
|
||||
|
||||
if(modified.isEOF() || differentData.length===0xff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
patch.addRecord(offset, differentData);
|
||||
}
|
||||
}
|
||||
|
||||
if(original.fileSize<modified.fileSize){
|
||||
modified.seek(modified.fileSize-1);
|
||||
if(modified.readU8()===0x00)
|
||||
patch.addRecord(modified.fileSize-1, [0x00]);
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
350
rom-patcher-js/modules/RomPatcher.format.rup.js
Normal file
350
rom-patcher-js/modules/RomPatcher.format.rup.js
Normal file
|
@ -0,0 +1,350 @@
|
|||
/* RUP module for Rom Patcher JS v20240721 - Marc Robledo 2018-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.romhacking.net/documents/288/ */
|
||||
|
||||
const RUP_MAGIC='NINJA2';
|
||||
const RUP_COMMAND_END=0x00;
|
||||
const RUP_COMMAND_OPEN_NEW_FILE=0x01;
|
||||
const RUP_COMMAND_XOR_RECORD=0x02;
|
||||
const RUP_ROM_TYPES=['raw','nes','fds','snes','n64','gb','sms','mega','pce','lynx'];
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = RUP;
|
||||
}
|
||||
|
||||
function RUP(){
|
||||
this.author='';
|
||||
this.version='';
|
||||
this.title='';
|
||||
this.genre='';
|
||||
this.language='';
|
||||
this.date='';
|
||||
this.web='';
|
||||
this.description='';
|
||||
this.files=[];
|
||||
}
|
||||
RUP.prototype.toString=function(){
|
||||
var s='Author: '+this.author;
|
||||
s+='\nVersion: '+this.version;
|
||||
s+='\nTitle: '+this.title;
|
||||
s+='\nGenre: '+this.genre;
|
||||
s+='\nLanguage: '+this.language;
|
||||
s+='\nDate: '+this.date;
|
||||
s+='\nWeb: '+this.web;
|
||||
s+='\nDescription: '+this.description;
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
s+='\n---------------';
|
||||
s+='\nFile '+i+':';
|
||||
s+='\nFile name: '+file.fileName;
|
||||
s+='\nRom type: '+RUP_ROM_TYPES[file.romType];
|
||||
s+='\nSource file size: '+file.sourceFileSize;
|
||||
s+='\nTarget file size: '+file.targetFileSize;
|
||||
s+='\nSource MD5: '+file.sourceMD5;
|
||||
s+='\nTarget MD5: '+file.targetMD5;
|
||||
s+='\nOverflow text: '+file.overflowText;
|
||||
s+='\n#records: '+file.records.length;
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
RUP.prototype.validateSource=function(romFile,headerSize){
|
||||
var md5string=romFile.hashMD5(headerSize);
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
if(this.files[i].sourceMD5===md5string){
|
||||
return this.files[i];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
RUP.prototype.getValidationInfo=function(){
|
||||
var values=[];
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
values.push(this.files[i].sourceMD5);
|
||||
}
|
||||
return {
|
||||
'type':'MD5',
|
||||
'value':values
|
||||
};
|
||||
}
|
||||
RUP.prototype.getDescription=function(){
|
||||
return this.description? this.description : null;
|
||||
}
|
||||
RUP.prototype.apply=function(romFile, validate){
|
||||
var validFile;
|
||||
if(validate){
|
||||
validFile=this.validateSource(romFile);
|
||||
|
||||
if(!validFile)
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}else{
|
||||
validFile=this.files[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
tempFile=new BinFile(validFile.targetFileSize);
|
||||
/* copy original file */
|
||||
romFile.copyTo(tempFile, 0);
|
||||
|
||||
|
||||
for(var i=0; i<validFile.records.length; i++){
|
||||
var offset=validFile.records[i].offset;
|
||||
romFile.seek(offset);
|
||||
tempFile.seek(offset);
|
||||
for(var j=0; j<validFile.records[i].xor.length; j++){
|
||||
tempFile.writeU8(
|
||||
(romFile.isEOF()?0x00:romFile.readU8()) ^ validFile.records[i].xor[j]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(validate && tempFile.hashMD5()!==validFile.targetMD5){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
RUP.MAGIC=RUP_MAGIC;
|
||||
|
||||
RUP.padZeroes=function(intVal, nBytes){
|
||||
var hexString=intVal.toString(16);
|
||||
while(hexString.length<nBytes*2)
|
||||
hexString='0'+hexString;
|
||||
return hexString
|
||||
};
|
||||
RUP.fromFile=function(file){
|
||||
var patch=new RUP();
|
||||
file.readVLV=RUP_readVLV;
|
||||
|
||||
file.seek(RUP_MAGIC.length);
|
||||
|
||||
patch.textEncoding=file.readU8();
|
||||
patch.author=file.readString(84);
|
||||
patch.version=file.readString(11);
|
||||
patch.title=file.readString(256);
|
||||
patch.genre=file.readString(48);
|
||||
patch.language=file.readString(48);
|
||||
patch.date=file.readString(8);
|
||||
patch.web=file.readString(512);
|
||||
patch.description=file.readString(1074).replace(/\\n/g,'\n');
|
||||
|
||||
|
||||
file.seek(0x800);
|
||||
var nextFile;
|
||||
while(!file.isEOF()){
|
||||
var command=file.readU8();
|
||||
|
||||
if(command===RUP_COMMAND_OPEN_NEW_FILE){
|
||||
if(nextFile)
|
||||
patch.files.push(nextFile)
|
||||
|
||||
nextFile={
|
||||
records:[]
|
||||
};
|
||||
|
||||
|
||||
nextFile.fileName=file.readString(file.readVLV());
|
||||
nextFile.romType=file.readU8();
|
||||
nextFile.sourceFileSize=file.readVLV();
|
||||
nextFile.targetFileSize=file.readVLV();
|
||||
|
||||
nextFile.sourceMD5='';
|
||||
for(var i=0; i<16; i++)
|
||||
nextFile.sourceMD5+=RUP.padZeroes(file.readU8(),1);
|
||||
|
||||
nextFile.targetMD5='';
|
||||
for(var i=0; i<16; i++)
|
||||
nextFile.targetMD5+=RUP.padZeroes(file.readU8(),1);
|
||||
|
||||
|
||||
|
||||
if(nextFile.sourceFileSize!==nextFile.targetFileSize){
|
||||
file.skip(1); //skip 'M' (source>target) or 'A' (source<target)
|
||||
nextFile.overflowText=file.readString(file.readVLV());
|
||||
}
|
||||
|
||||
}else if(command===RUP_COMMAND_XOR_RECORD){
|
||||
nextFile.records.push({
|
||||
offset:file.readVLV(),
|
||||
xor:file.readBytes(file.readVLV())
|
||||
});
|
||||
}else if(command===RUP_COMMAND_END){
|
||||
if(nextFile)
|
||||
patch.files.push(nextFile);
|
||||
break;
|
||||
}else{
|
||||
throw new Error('invalid RUP command');
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
function RUP_readVLV(){
|
||||
var nBytes=this.readU8();
|
||||
var data=0;
|
||||
for(var i=0; i<nBytes; i++){
|
||||
data+=this.readU8() << i*8;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
function RUP_writeVLV(data){
|
||||
var len=RUP_getVLVLen(data)-1;
|
||||
|
||||
this.writeU8(len);
|
||||
|
||||
while(data){
|
||||
this.writeU8(data & 0xff);
|
||||
data>>=8;
|
||||
}
|
||||
}
|
||||
function RUP_getVLVLen(data){
|
||||
var ret=1;
|
||||
while(data){
|
||||
ret++;
|
||||
data>>=8;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
RUP.prototype.export=function(fileName){
|
||||
var patchFileSize=2048;
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
patchFileSize++; //command 0x01
|
||||
|
||||
patchFileSize+=RUP_getVLVLen(file.fileName.length);
|
||||
patchFileSize+=file.fileName.length;
|
||||
patchFileSize++; //rom type
|
||||
patchFileSize+=RUP_getVLVLen(file.sourceFileSize);
|
||||
patchFileSize+=RUP_getVLVLen(file.targetFileSize);
|
||||
patchFileSize+=32; //MD5s
|
||||
|
||||
if(file.sourceFileSize!==file.targetFileSize){
|
||||
patchFileSize++; // M or A
|
||||
patchFileSize+=RUP_getVLVLen(file.overflowText);
|
||||
patchFileSize+=file.overflowText;
|
||||
}
|
||||
for(var j=0; j<file.records.length; j++){
|
||||
patchFileSize++; //command 0x01
|
||||
patchFileSize+=RUP_getVLVLen(file.records[j].offset);
|
||||
patchFileSize+=RUP_getVLVLen(file.records[j].xor.length);
|
||||
patchFileSize+=file.records[j].xor.length;
|
||||
}
|
||||
}
|
||||
patchFileSize++; //command 0x00
|
||||
|
||||
|
||||
var patchFile=new BinFile(patchFileSize);
|
||||
patchFile.fileName=fileName+'.rup';
|
||||
patchFile.writeVLV=RUP_writeVLV;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
patchFile.writeString(RUP_MAGIC);
|
||||
patchFile.writeU8(this.textEncoding);
|
||||
patchFile.writeString(this.author, 84);
|
||||
patchFile.writeString(this.version, 11);
|
||||
patchFile.writeString(this.title, 256);
|
||||
patchFile.writeString(this.genre, 48);
|
||||
patchFile.writeString(this.language, 48);
|
||||
patchFile.writeString(this.date, 8);
|
||||
patchFile.writeString(this.web, 512);
|
||||
patchFile.writeString(this.description.replace(/\n/g,'\\n'), 1074);
|
||||
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
patchFile.writeU8(RUP_COMMAND_OPEN_NEW_FILE);
|
||||
|
||||
patchFile.writeVLV(file.fileName);
|
||||
patchFile.writeU8(file.romType);
|
||||
patchFile.writeVLV(file.sourceFileSize);
|
||||
patchFile.writeVLV(file.targetFileSize);
|
||||
|
||||
for(var j=0; j<16; j++)
|
||||
patchFile.writeU8(parseInt(file.sourceMD5.substr(j*2,2), 16));
|
||||
for(var j=0; j<16; j++)
|
||||
patchFile.writeU8(parseInt(file.targetMD5.substr(j*2,2), 16));
|
||||
|
||||
|
||||
|
||||
if(file.sourceFileSize!==file.targetFileSize){
|
||||
patchFile.writeString(file.sourceFileSize>file.targetFileSize?'M':'A');
|
||||
patchFile.writeVLV(file.overflowText.length);
|
||||
patchFile.writeString(file.overflowText);
|
||||
}
|
||||
|
||||
for(var j=0; j<file.records.length; j++){
|
||||
patchFile.writeU8(RUP_COMMAND_XOR_RECORD);
|
||||
|
||||
patchFile.writeVLV(file.records[j].offset);
|
||||
patchFile.writeVLV(file.records[j].xor.length);
|
||||
patchFile.writeBytes(file.records[j].xor);
|
||||
}
|
||||
}
|
||||
|
||||
patchFile.writeU8(RUP_COMMAND_END);
|
||||
|
||||
|
||||
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
|
||||
|
||||
RUP.buildFromRoms=function(original, modified){
|
||||
var patch=new RUP();
|
||||
|
||||
var today=new Date();
|
||||
patch.date=(today.getYear()+1900)+RUP.padZeroes(today.getMonth()+1, 1)+RUP.padZeroes(today.getDate(), 1);
|
||||
|
||||
var file={
|
||||
fileName:'',
|
||||
romType:0,
|
||||
sourceFileSize:original.fileSize,
|
||||
targetFileSize:modified.fileSize,
|
||||
sourceMD5:original.hashMD5(),
|
||||
targetMD5:modified.hashMD5(),
|
||||
overflowText:'',
|
||||
records:[]
|
||||
};
|
||||
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var originalOffset=modified.offset-1;
|
||||
var xorDifferences=[];
|
||||
|
||||
while(b1!==b2){
|
||||
xorDifferences.push(b1^b2);
|
||||
|
||||
if(modified.isEOF())
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
file.records.push({offset:originalOffset, xor:xorDifferences});
|
||||
}
|
||||
}
|
||||
|
||||
patch.files.push(file);
|
||||
|
||||
return patch
|
||||
}
|
224
rom-patcher-js/modules/RomPatcher.format.ups.js
Normal file
224
rom-patcher-js/modules/RomPatcher.format.ups.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/* UPS module for Rom Patcher JS v20240721 - Marc Robledo 2017-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.romhacking.net/documents/392/ */
|
||||
|
||||
const UPS_MAGIC='UPS1';
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = UPS;
|
||||
}
|
||||
function UPS(){
|
||||
this.records=[];
|
||||
this.sizeInput=0;
|
||||
this.sizeOutput=0;
|
||||
this.checksumInput=0;
|
||||
this.checksumOutput=0;
|
||||
}
|
||||
UPS.prototype.addRecord=function(relativeOffset, d){
|
||||
this.records.push({offset:relativeOffset, XORdata:d})
|
||||
}
|
||||
UPS.prototype.toString=function(){
|
||||
var s='Records: '+this.records.length;
|
||||
s+='\nInput file size: '+this.sizeInput;
|
||||
s+='\nOutput file size: '+this.sizeOutput;
|
||||
s+='\nInput file checksum: '+this.checksumInput.toString(16);
|
||||
s+='\nOutput file checksum: '+this.checksumOutput.toString(16);
|
||||
return s
|
||||
}
|
||||
UPS.prototype.export=function(fileName){
|
||||
var patchFileSize=UPS_MAGIC.length;//UPS1 string
|
||||
patchFileSize+=UPS_getVLVLength(this.sizeInput); //input file size
|
||||
patchFileSize+=UPS_getVLVLength(this.sizeOutput); //output file size
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
patchFileSize+=UPS_getVLVLength(this.records[i].offset);
|
||||
patchFileSize+=this.records[i].XORdata.length+1;
|
||||
}
|
||||
patchFileSize+=12; //input/output/patch checksums
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.writeVLV=UPS_writeVLV;
|
||||
tempFile.fileName=fileName+'.ups';
|
||||
tempFile.writeString(UPS_MAGIC);
|
||||
|
||||
tempFile.writeVLV(this.sizeInput);
|
||||
tempFile.writeVLV(this.sizeOutput);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeVLV(this.records[i].offset);
|
||||
tempFile.writeBytes(this.records[i].XORdata);
|
||||
tempFile.writeU8(0x00);
|
||||
}
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.writeU32(this.checksumInput);
|
||||
tempFile.writeU32(this.checksumOutput);
|
||||
tempFile.writeU32(tempFile.hashCRC32(0, tempFile.fileSize - 4));
|
||||
|
||||
return tempFile
|
||||
}
|
||||
UPS.prototype.validateSource=function(romFile,headerSize){return romFile.hashCRC32(headerSize)===this.checksumInput}
|
||||
UPS.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':this.checksumInput
|
||||
};
|
||||
}
|
||||
UPS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
/* fix the glitch that cut the end of the file if it's larger than the changed file patch was originally created with */
|
||||
/* more info: https://github.com/marcrobledo/RomPatcher.js/pull/40#issuecomment-1069087423 */
|
||||
sizeOutput = this.sizeOutput;
|
||||
sizeInput = this.sizeInput;
|
||||
if(!validate && sizeInput < romFile.fileSize){
|
||||
sizeInput = romFile.fileSize;
|
||||
if(sizeOutput < sizeInput){
|
||||
sizeOutput = sizeInput;
|
||||
}
|
||||
}
|
||||
|
||||
/* copy original file */
|
||||
tempFile=new BinFile(sizeOutput);
|
||||
romFile.copyTo(tempFile, 0, sizeInput);
|
||||
|
||||
romFile.seek(0);
|
||||
|
||||
|
||||
var nextOffset=0;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var record=this.records[i];
|
||||
tempFile.skip(record.offset);
|
||||
romFile.skip(record.offset);
|
||||
|
||||
for(var j=0; j<record.XORdata.length; j++){
|
||||
tempFile.writeU8((romFile.isEOF()?0x00:romFile.readU8()) ^ record.XORdata[j]);
|
||||
}
|
||||
tempFile.skip(1);
|
||||
romFile.skip(1);
|
||||
}
|
||||
|
||||
if(validate && tempFile.hashCRC32()!==this.checksumOutput){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
UPS.MAGIC=UPS_MAGIC;
|
||||
|
||||
|
||||
/* encode/decode variable length values, used by UPS file structure */
|
||||
function UPS_writeVLV(data){
|
||||
while(1){
|
||||
var x=data & 0x7f;
|
||||
data=data>>7;
|
||||
if(data===0){
|
||||
this.writeU8(0x80 | x);
|
||||
break;
|
||||
}
|
||||
this.writeU8(x);
|
||||
data=data-1;
|
||||
}
|
||||
}
|
||||
function UPS_readVLV(){
|
||||
var data=0;
|
||||
|
||||
var shift=1;
|
||||
while(1){
|
||||
var x=this.readU8();
|
||||
|
||||
if(x==-1)
|
||||
throw new Error('Can\'t read UPS VLV at 0x'+(this.offset-1).toString(16));
|
||||
|
||||
data+=(x&0x7f)*shift;
|
||||
if((x&0x80)!==0)
|
||||
break;
|
||||
shift=shift<<7;
|
||||
data+=shift;
|
||||
}
|
||||
return data
|
||||
}
|
||||
function UPS_getVLVLength(data){
|
||||
var len=0;
|
||||
while(1){
|
||||
var x=data & 0x7f;
|
||||
data=data>>7;
|
||||
len++;
|
||||
if(data===0){
|
||||
break;
|
||||
}
|
||||
data=data-1;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
UPS.fromFile=function(file){
|
||||
var patch=new UPS();
|
||||
file.readVLV=UPS_readVLV;
|
||||
|
||||
file.seek(UPS_MAGIC.length);
|
||||
|
||||
patch.sizeInput=file.readVLV();
|
||||
patch.sizeOutput=file.readVLV();
|
||||
|
||||
|
||||
var nextOffset=0;
|
||||
while(file.offset<(file.fileSize-12)){
|
||||
var relativeOffset=file.readVLV();
|
||||
|
||||
|
||||
var XORdifferences=[];
|
||||
while(file.readU8()){
|
||||
XORdifferences.push(file._lastRead);
|
||||
}
|
||||
patch.addRecord(relativeOffset, XORdifferences);
|
||||
}
|
||||
|
||||
file.littleEndian=true;
|
||||
patch.checksumInput=file.readU32();
|
||||
patch.checksumOutput=file.readU32();
|
||||
|
||||
if(file.readU32()!==file.hashCRC32(0, file.fileSize - 4)){
|
||||
throw new Error('Patch checksum mismatch');
|
||||
}
|
||||
|
||||
file.littleEndian=false;
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
UPS.buildFromRoms=function(original, modified){
|
||||
var patch=new UPS();
|
||||
patch.sizeInput=original.fileSize;
|
||||
patch.sizeOutput=modified.fileSize;
|
||||
|
||||
|
||||
var previousSeek=1;
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var currentSeek=modified.offset;
|
||||
var XORdata=[];
|
||||
|
||||
while(b1!==b2){
|
||||
XORdata.push(b1 ^ b2);
|
||||
|
||||
if(modified.isEOF())
|
||||
break;
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
patch.addRecord(currentSeek-previousSeek, XORdata);
|
||||
previousSeek=currentSeek+XORdata.length+1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
patch.checksumInput=original.hashCRC32();
|
||||
patch.checksumOutput=modified.hashCRC32();
|
||||
return patch
|
||||
}
|
377
rom-patcher-js/modules/RomPatcher.format.vcdiff.js
Normal file
377
rom-patcher-js/modules/RomPatcher.format.vcdiff.js
Normal file
|
@ -0,0 +1,377 @@
|
|||
/* VCDIFF module for RomPatcher.js v20181021 - Marc Robledo 2018 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://tools.ietf.org/html/rfc3284 */
|
||||
/*
|
||||
Mostly based in:
|
||||
https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
|
||||
some code and ideas borrowed from:
|
||||
https://hack64.net/jscripts/libpatch.js?6
|
||||
*/
|
||||
//const VCDIFF_MAGIC=0xd6c3c400;
|
||||
const VCDIFF_MAGIC='\xd6\xc3\xc4';
|
||||
/*
|
||||
const XDELTA_014_MAGIC='%XDELTA';
|
||||
const XDELTA_018_MAGIC='%XDZ000';
|
||||
const XDELTA_020_MAGIC='%XDZ001';
|
||||
const XDELTA_100_MAGIC='%XDZ002';
|
||||
const XDELTA_104_MAGIC='%XDZ003';
|
||||
const XDELTA_110_MAGIC='%XDZ004';
|
||||
*/
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = VCDIFF;
|
||||
BinFile = require("./BinFile");
|
||||
}
|
||||
|
||||
function VCDIFF(patchFile){
|
||||
this.file=patchFile;
|
||||
}
|
||||
VCDIFF.prototype.toString=function(){
|
||||
return 'VCDIFF patch'
|
||||
}
|
||||
|
||||
VCDIFF.prototype.apply=function(romFile, validate){
|
||||
//romFile._u8array=new Uint8Array(romFile._dataView.buffer);
|
||||
|
||||
//var t0=performance.now();
|
||||
var parser=new VCDIFF_Parser(this.file);
|
||||
|
||||
//read header
|
||||
parser.seek(4);
|
||||
var headerIndicator=parser.readU8();
|
||||
|
||||
if(headerIndicator & VCD_DECOMPRESS){
|
||||
//has secondary decompressor, read its id
|
||||
var secondaryDecompressorId=parser.readU8();
|
||||
|
||||
if(secondaryDecompressorId!==0)
|
||||
throw new Error('not implemented: secondary decompressor');
|
||||
}
|
||||
|
||||
|
||||
if(headerIndicator & VCD_CODETABLE){
|
||||
var codeTableDataLength=parser.read7BitEncodedInt();
|
||||
|
||||
if(codeTableDataLength!==0)
|
||||
throw new Error('not implemented: custom code table'); // custom code table
|
||||
}
|
||||
|
||||
if(headerIndicator & VCD_APPHEADER){
|
||||
// ignore app header data
|
||||
var appDataLength=parser.read7BitEncodedInt();
|
||||
parser.skip(appDataLength);
|
||||
}
|
||||
var headerEndOffset=parser.offset;
|
||||
|
||||
//calculate target file size
|
||||
var newFileSize=0;
|
||||
while(!parser.isEOF()){
|
||||
var winHeader=parser.decodeWindowHeader();
|
||||
newFileSize+=winHeader.targetWindowLength;
|
||||
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
|
||||
}
|
||||
tempFile=new BinFile(newFileSize);
|
||||
|
||||
|
||||
|
||||
|
||||
parser.seek(headerEndOffset);
|
||||
|
||||
|
||||
|
||||
var cache = new VCD_AdressCache(4,3);
|
||||
var codeTable = VCD_DEFAULT_CODE_TABLE;
|
||||
|
||||
var targetWindowPosition = 0; //renombrar
|
||||
|
||||
while(!parser.isEOF()){
|
||||
var winHeader = parser.decodeWindowHeader();
|
||||
|
||||
var addRunDataStream = new VCDIFF_Parser(this.file, parser.offset);
|
||||
var instructionsStream = new VCDIFF_Parser(this.file, addRunDataStream.offset + winHeader.addRunDataLength);
|
||||
var addressesStream = new VCDIFF_Parser(this.file, instructionsStream.offset + winHeader.instructionsLength);
|
||||
|
||||
var addRunDataIndex = 0;
|
||||
|
||||
cache.reset(addressesStream);
|
||||
|
||||
var addressesStreamEndOffset = addressesStream.offset;
|
||||
while(instructionsStream.offset<addressesStreamEndOffset){
|
||||
/*
|
||||
var instructionIndex=instructionsStream.readS8();
|
||||
if(instructionIndex===-1){
|
||||
break;
|
||||
}
|
||||
*/
|
||||
var instructionIndex = instructionsStream.readU8();
|
||||
|
||||
|
||||
for(var i=0; i<2; i++){
|
||||
var instruction=codeTable[instructionIndex][i];
|
||||
var size=instruction.size;
|
||||
|
||||
if(size===0 && instruction.type!==VCD_NOOP){
|
||||
size=instructionsStream.read7BitEncodedInt()
|
||||
}
|
||||
|
||||
if(instruction.type===VCD_NOOP){
|
||||
continue;
|
||||
|
||||
}else if(instruction.type===VCD_ADD){
|
||||
addRunDataStream.copyToFile2(tempFile, addRunDataIndex+targetWindowPosition, size);
|
||||
addRunDataIndex += size;
|
||||
|
||||
}else if(instruction.type===VCD_COPY){
|
||||
var addr = cache.decodeAddress(addRunDataIndex+winHeader.sourceLength, instruction.mode);
|
||||
var absAddr = 0;
|
||||
|
||||
// source segment and target segment are treated as if they're concatenated
|
||||
var sourceData = null;
|
||||
if(addr < winHeader.sourceLength){
|
||||
absAddr = winHeader.sourcePosition + addr;
|
||||
if(winHeader.indicator & VCD_SOURCE){
|
||||
sourceData = romFile;
|
||||
}else if(winHeader.indicator & VCD_TARGET){
|
||||
sourceData = tempFile;
|
||||
}
|
||||
}else{
|
||||
absAddr = targetWindowPosition + (addr - winHeader.sourceLength);
|
||||
sourceData = tempFile;
|
||||
}
|
||||
|
||||
while(size--){
|
||||
tempFile._u8array[targetWindowPosition + addRunDataIndex++]=sourceData._u8array[absAddr++];
|
||||
//targetU8[targetWindowPosition + targetWindowOffs++] = copySourceU8[absAddr++];
|
||||
}
|
||||
//to-do: test
|
||||
//sourceData.copyToFile2(tempFile, absAddr, size, targetWindowPosition + addRunDataIndex);
|
||||
//addRunDataIndex += size;
|
||||
}else if(instruction.type===VCD_RUN){
|
||||
var runByte = addRunDataStream.readU8();
|
||||
var offset = targetWindowPosition + addRunDataIndex;
|
||||
for(var j=0; j<size; j++){
|
||||
tempFile._u8array[offset+j]=runByte;
|
||||
}
|
||||
|
||||
addRunDataIndex += size;
|
||||
}else{
|
||||
throw new Error('invalid instruction type found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(validate && winHeader.adler32 && (winHeader.adler32 !== adler32(tempFile, targetWindowPosition, winHeader.targetWindowLength))){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
|
||||
targetWindowPosition += winHeader.targetWindowLength;
|
||||
}
|
||||
|
||||
//console.log((performance.now()-t0)/1000);
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
VCDIFF.MAGIC=VCDIFF_MAGIC;
|
||||
|
||||
VCDIFF.fromFile=function(file){
|
||||
return new VCDIFF(file);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function VCDIFF_Parser(binFile, offset)
|
||||
{
|
||||
BinFile.call(this, 0);
|
||||
this.fileSize=binFile.fileSize;
|
||||
this._u8array=binFile._u8array;
|
||||
this._dataView=binFile._dataView;
|
||||
this.offset=offset || 0;
|
||||
}
|
||||
|
||||
VCDIFF_Parser.prototype=Object.create(BinFile.prototype);
|
||||
VCDIFF_Parser.prototype.read7BitEncodedInt=function(){
|
||||
var num=0, bits = 0;
|
||||
|
||||
do {
|
||||
bits = this.readU8();
|
||||
num = (num << 7) + (bits & 0x7f);
|
||||
} while(bits & 0x80);
|
||||
|
||||
return num;
|
||||
}
|
||||
VCDIFF_Parser.prototype.decodeWindowHeader=function(){
|
||||
var windowHeader={
|
||||
indicator:this.readU8(),
|
||||
sourceLength:0,
|
||||
sourcePosition:0,
|
||||
adler32:false
|
||||
};
|
||||
|
||||
|
||||
if(windowHeader.indicator & (VCD_SOURCE | VCD_TARGET)){
|
||||
windowHeader.sourceLength = this.read7BitEncodedInt();
|
||||
windowHeader.sourcePosition = this.read7BitEncodedInt();
|
||||
}
|
||||
|
||||
windowHeader.deltaLength = this.read7BitEncodedInt();
|
||||
windowHeader.targetWindowLength = this.read7BitEncodedInt();
|
||||
windowHeader.deltaIndicator = this.readU8(); // secondary compression: 1=VCD_DATACOMP,2=VCD_INSTCOMP,4=VCD_ADDRCOMP
|
||||
if(windowHeader.deltaIndicator!==0){
|
||||
throw new Error('unimplemented windowHeader.deltaIndicator:'+windowHeader.deltaIndicator);
|
||||
}
|
||||
|
||||
windowHeader.addRunDataLength = this.read7BitEncodedInt();
|
||||
windowHeader.instructionsLength = this.read7BitEncodedInt();
|
||||
windowHeader.addressesLength = this.read7BitEncodedInt();
|
||||
|
||||
if(windowHeader.indicator & VCD_ADLER32){
|
||||
windowHeader.adler32 = this.readU32();
|
||||
}
|
||||
|
||||
|
||||
return windowHeader;
|
||||
}
|
||||
|
||||
|
||||
VCDIFF_Parser.prototype.copyToFile2=function(target, targetOffset, len){
|
||||
for(var i=0; i<len; i++){
|
||||
target._u8array[targetOffset+i]=this._u8array[this.offset+i];
|
||||
}
|
||||
//this.file.copyToFile(target, this.offset, len, targetOffset);
|
||||
this.skip(len);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// hdrIndicator
|
||||
const VCD_DECOMPRESS = 0x01;
|
||||
const VCD_CODETABLE = 0x02;
|
||||
const VCD_APPHEADER = 0x04; // nonstandard?
|
||||
|
||||
// winIndicator
|
||||
const VCD_SOURCE = 0x01;
|
||||
const VCD_TARGET = 0x02;
|
||||
const VCD_ADLER32 = 0x04;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function VCD_Instruction(instruction, size, mode){
|
||||
this.type=instruction;
|
||||
this.size=size;
|
||||
this.mode=mode;
|
||||
}
|
||||
|
||||
/*
|
||||
build the default code table (used to encode/decode instructions) specified in RFC 3284
|
||||
heavily based on
|
||||
https://github.com/vic-alexiev/TelerikAcademy/blob/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff/CodeTable.cs
|
||||
*/
|
||||
const VCD_NOOP=0;
|
||||
const VCD_ADD=1;
|
||||
const VCD_RUN=2;
|
||||
const VCD_COPY=3;
|
||||
const VCD_DEFAULT_CODE_TABLE=(function(){
|
||||
var entries=[];
|
||||
|
||||
var empty = {type: VCD_NOOP, size: 0, mode: 0};
|
||||
|
||||
// 0
|
||||
entries.push([{type: VCD_RUN, size: 0, mode: 0}, empty]);
|
||||
|
||||
// 1,18
|
||||
for(var size=0; size<18; size++){
|
||||
entries.push([{type: VCD_ADD, size: size, mode: 0}, empty]);
|
||||
}
|
||||
|
||||
// 19,162
|
||||
for(var mode=0; mode<9; mode++){
|
||||
entries.push([{type: VCD_COPY, size: 0, mode: mode}, empty]);
|
||||
|
||||
for(var size=4; size<19; size++){
|
||||
entries.push([{type: VCD_COPY, size: size, mode: mode}, empty]);
|
||||
}
|
||||
}
|
||||
|
||||
// 163,234
|
||||
for(var mode=0; mode<6; mode++){
|
||||
for(var addSize=1; addSize<5; addSize++){
|
||||
for(var copySize=4; copySize<7; copySize++){
|
||||
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
|
||||
{type: VCD_COPY, size: copySize, mode: mode}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 235,246
|
||||
for(var mode=6; mode<9; mode++){
|
||||
for(var addSize=1; addSize<5; addSize++){
|
||||
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
|
||||
{type: VCD_COPY, size: 4, mode: mode}]);
|
||||
}
|
||||
}
|
||||
|
||||
// 247,255
|
||||
for(var mode=0; mode<9; mode++){
|
||||
entries.push([{type: VCD_COPY, size: 4, mode: mode},
|
||||
{type: VCD_ADD, size: 1, mode: 0}]);
|
||||
}
|
||||
|
||||
return entries;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
/*
|
||||
ported from https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
|
||||
by Victor Alexiev (https://github.com/vic-alexiev)
|
||||
*/
|
||||
const VCD_MODE_SELF=0;
|
||||
const VCD_MODE_HERE=1;
|
||||
function VCD_AdressCache(nearSize, sameSize){
|
||||
this.nearSize=nearSize;
|
||||
this.sameSize=sameSize;
|
||||
|
||||
this.near=new Array(nearSize);
|
||||
this.same=new Array(sameSize*256);
|
||||
}
|
||||
VCD_AdressCache.prototype.reset=function(addressStream){
|
||||
this.nextNearSlot=0;
|
||||
this.near.fill(0);
|
||||
this.same.fill(0);
|
||||
|
||||
this.addressStream=addressStream;
|
||||
}
|
||||
VCD_AdressCache.prototype.decodeAddress=function(here, mode){
|
||||
var address=0;
|
||||
|
||||
if(mode===VCD_MODE_SELF){
|
||||
address=this.addressStream.read7BitEncodedInt();
|
||||
}else if(mode===VCD_MODE_HERE){
|
||||
address=here-this.addressStream.read7BitEncodedInt();
|
||||
}else if(mode-2<this.nearSize){ //near cache
|
||||
address=this.near[mode-2]+this.addressStream.read7BitEncodedInt();
|
||||
}else{ //same cache
|
||||
var m=mode-(2+this.nearSize);
|
||||
address=this.same[m*256+this.addressStream.readU8()];
|
||||
}
|
||||
|
||||
this.update(address);
|
||||
return address;
|
||||
}
|
||||
VCD_AdressCache.prototype.update=function(address){
|
||||
if(this.nearSize>0){
|
||||
this.near[this.nextNearSlot]=address;
|
||||
this.nextNearSlot=(this.nextNearSlot+1)%this.nearSize;
|
||||
}
|
||||
|
||||
if(this.sameSize>0){
|
||||
this.same[address%(this.sameSize*256)]=address;
|
||||
}
|
||||
}
|
28
rom-patcher-js/modules/zip.js/LICENSE
Normal file
28
rom-patcher-js/modules/zip.js/LICENSE
Normal file
|
@ -0,0 +1,28 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2013, Gildas Lormeau
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
36
rom-patcher-js/modules/zip.js/inflate.js
Normal file
36
rom-patcher-js/modules/zip.js/inflate.js
Normal file
File diff suppressed because one or more lines are too long
2
rom-patcher-js/modules/zip.js/z-worker.js
Normal file
2
rom-patcher-js/modules/zip.js/z-worker.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
/* jshint worker:true */
|
||||
!function(c){"use strict";if(c.zWorkerInitialized)throw new Error("z-worker.js should be run only once");c.zWorkerInitialized=!0,addEventListener("message",function(t){var e,r,c=t.data,n=c.type,s=c.sn,p=o[n];if(p)try{p(c)}catch(t){e={type:n,sn:s,error:(r=t,{message:r.message,stack:r.stack})},postMessage(e)}});var o={importScripts:function(t){t.scripts&&0<t.scripts.length&&importScripts.apply(void 0,t.scripts);postMessage({type:"importScripts"})},newTask:h,append:t,flush:t},f={};function h(t){var e=c[t.codecClass],r=t.sn;if(f[r])throw Error("duplicated sn");f[r]={codec:new e(t.options),crcInput:"input"===t.crcType,crcOutput:"output"===t.crcType,crc:new n},postMessage({type:"newTask",sn:r})}var l=c.performance?c.performance.now.bind(c.performance):Date.now;function t(t){var e=t.sn,r=t.type,c=t.data,n=f[e];!n&&t.codecClass&&(h(t),n=f[e]);var s,p="append"===r,o=l();if(p)try{s=n.codec.append(c,function(t){postMessage({type:"progress",sn:e,loaded:t})})}catch(t){throw delete f[e],t}else delete f[e],s=n.codec.flush();var a=l()-o;o=l(),c&&n.crcInput&&n.crc.append(c),s&&n.crcOutput&&n.crc.append(s);var i=l()-o,u={type:r,sn:e,codecTime:a,crcTime:i},d=[];s&&(u.data=s,d.push(s.buffer)),p||!n.crcInput&&!n.crcOutput||(u.crc=n.crc.get());try{postMessage(u,d)}catch(t){postMessage(u)}}function n(){this.crc=-1}function e(){}n.prototype.append=function(t){for(var e=0|this.crc,r=this.table,c=0,n=0|t.length;c<n;c++)e=e>>>8^r[255&(e^t[c])];this.crc=e},n.prototype.get=function(){return~this.crc},n.prototype.table=function(){var t,e,r,c=[];for(t=0;t<256;t++){for(r=t,e=0;e<8;e++)1&r?r=r>>>1^3988292384:r>>>=1;c[t]=r}return c}(),(c.NOOP=e).prototype.append=function(t,e){return t},e.prototype.flush=function(){}}(this);
|
28
rom-patcher-js/modules/zip.js/zip.min.js
vendored
Normal file
28
rom-patcher-js/modules/zip.js/zip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
502
rom-patcher-js/style.css
Normal file
502
rom-patcher-js/style.css
Normal file
|
@ -0,0 +1,502 @@
|
|||
/* Rom Patcher JS template CSS stylesheet */
|
||||
/* customize it to your taste! */
|
||||
|
||||
|
||||
/* @FONT-FACES */
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700');
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300');
|
||||
|
||||
|
||||
/* Rom Patcher JS - container */
|
||||
#rom-patcher-container {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
line-height: 1.8;
|
||||
|
||||
background-color: #f9fafa;
|
||||
padding: 30px 15px;
|
||||
border-radius: 3px;
|
||||
color: black;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Rom Pacher JS - text classes */
|
||||
#rom-patcher-container .text-mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-muted {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-right {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
#rom-patcher-container .text-selectable {
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
-o-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* Rom Patcher JS - rows */
|
||||
#rom-patcher-container .rom-patcher-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-row>div:first-child {
|
||||
width: 25%
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-row>div:last-child {
|
||||
width: 73%
|
||||
}
|
||||
|
||||
#rom-patcher-container .margin-bottom {
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
|
||||
|
||||
#rom-patcher-container #rom-patcher-row-info-rom,
|
||||
#rom-patcher-container #rom-patcher-row-alter-header,
|
||||
#rom-patcher-container #rom-patcher-row-patch-description,
|
||||
#rom-patcher-container #rom-patcher-row-patch-requirements,
|
||||
#rom-patcher-container #rom-patcher-row-error-message {
|
||||
display: none
|
||||
}
|
||||
|
||||
#rom-patcher-container #rom-patcher-row-info-rom.show,
|
||||
#rom-patcher-container #rom-patcher-row-alter-header.show,
|
||||
#rom-patcher-container #rom-patcher-row-patch-description.show,
|
||||
#rom-patcher-container #rom-patcher-row-patch-requirements.show {
|
||||
display: flex
|
||||
}
|
||||
|
||||
#rom-patcher-container #rom-patcher-row-error-message.show {
|
||||
display: block
|
||||
}
|
||||
|
||||
#rom-patcher-patch-description {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
#rom-patcher-row-apply {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
|
||||
#rom-patcher-span-crc32 span.clickable {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
#rom-patcher-span-crc32 span.clickable:hover {
|
||||
cursor: pointer;
|
||||
color: black
|
||||
}
|
||||
|
||||
#rom-patcher-error-message {
|
||||
color: #ff0030;
|
||||
padding-left: 20px;
|
||||
background-image: url(assets/icon_x_circle_red.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
}
|
||||
#rom-patcher-error-message.warning {
|
||||
color: #ff7800;
|
||||
background-image: url(assets/icon_alert_orange.svg);
|
||||
}
|
||||
|
||||
/* Rom Patcher JS - form elements */
|
||||
#rom-patcher-container input[type=file],
|
||||
#rom-patcher-container input[type=checkbox],
|
||||
#rom-patcher-container select {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background-color: #edefef;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]:focus:not(:disabled),
|
||||
#rom-patcher-container select:focus:not(:disabled),
|
||||
#rom-patcher-container input[type=checkbox]:focus:not(:disabled) {
|
||||
box-shadow: #a8fff3 0 0 0 2px;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file] {
|
||||
width: 100%;
|
||||
padding: 6px 10px
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]::file-selector-button {
|
||||
display: none
|
||||
}
|
||||
|
||||
#rom-patcher-container select {
|
||||
width: 100%;
|
||||
padding: 6px 18px 6px 10px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
text-overflow: '';
|
||||
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMTJweCIgeT0iMHB4IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIzcHgiIHZpZXdCb3g9IjAgMCA2IDMiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYgMyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBvbHlnb24gcG9pbnRzPSI1Ljk5MiwwIDIuOTkyLDMgLTAuMDA4LDAgIi8+PC9zdmc+");
|
||||
background-position: 100% center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#rom-patcher-container select::-ms-expand {
|
||||
display: none
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]:hover:not(:disabled),
|
||||
#rom-patcher-container select:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
background-color: #dee1e1
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file]:disabled,
|
||||
#rom-patcher-container select:disabled {
|
||||
color: #888
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#rom-patcher-container input[type=file].empty,
|
||||
#rom-patcher-container input[type=file]:not(:disabled):not(.empty):hover {
|
||||
padding-left: 32px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
background-size: 16px;
|
||||
background-image: url(assets/icon_upload.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].valid {
|
||||
background-color: #d6ffc8;
|
||||
padding-right: 28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
background-image: url(assets/icon_check_circle_green.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].valid:not(:disabled):not(.empty):hover {
|
||||
background-color: #adf795;
|
||||
background-position: 8px center, right 12px center;
|
||||
background-size: 16px, 16px;
|
||||
background-image: url(assets/icon_upload.svg), url(assets/icon_check_circle_green.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].invalid {
|
||||
background-color: #ffc8c8;
|
||||
padding-right: 28px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
background-image: url(assets/icon_x_circle_red.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].invalid:not(:disabled):not(.empty):hover {
|
||||
background-color: #ffa3a3;
|
||||
background-position: 8px center, right 12px center;
|
||||
background-size: 16px, 16px;
|
||||
background-image: url(assets/icon_upload.svg), url(assets/icon_x_circle_red.svg);
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=file].icon-upload {
|
||||
padding-left: 32px;
|
||||
background-image: url(assets/app_icon_16.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
}
|
||||
|
||||
|
||||
#rom-patcher-container input[type=checkbox] {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: transparent;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
border: 2px solid #2a9ca5;
|
||||
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMuMiIgZmlsbD0ibm9uZSIgZD0iTSAxLjE5LDcuMTAgNi4wNywxMi4wNiAxNC44OCwzLjMyIi8+PC9zdmc+');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 0px;
|
||||
transition: background-color .2s, background-size .2s;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=checkbox]:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
border-color: #3aacb5;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=checkbox]:hover:checked:not(:disabled) {
|
||||
background-color: #3aacb5;
|
||||
}
|
||||
|
||||
#rom-patcher-container input[type=checkbox]:checked {
|
||||
background-color: #2a9ca5;
|
||||
background-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* buttons */
|
||||
#rom-patcher-container button {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
min-width: 120px;
|
||||
border-radius: 3px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
|
||||
padding: 10px 20px;
|
||||
margin: 0 5px;
|
||||
|
||||
background-color: #2a9ca5;
|
||||
color: white;
|
||||
|
||||
transition: background-color .15s;
|
||||
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
#rom-patcher-container button:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#rom-patcher-container button:disabled {
|
||||
opacity: .35 !important;
|
||||
cursor: not-allowed
|
||||
}
|
||||
|
||||
#rom-patcher-container button:not(:disabled):hover {
|
||||
background-color: #3aacb5;
|
||||
}
|
||||
|
||||
#rom-patcher-container button:not(:disabled):active {
|
||||
background-color: #297b81;
|
||||
transform: translateY(1px)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* loading spinner */
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rom-patcher-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rom-patcher-spinner:before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #41bdc7;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
#patch-builder-button-create .rom-patcher-spinner:before,
|
||||
#rom-patcher-button-apply .rom-patcher-spinner:before {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#rom-patcher-container .rom-patcher-container-input {
|
||||
position: relative
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-container-input input.loading,
|
||||
#rom-patcher-container .rom-patcher-container-input select.loading {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
#rom-patcher-container .rom-patcher-container-input input.loading+.rom-patcher-spinner,
|
||||
#rom-patcher-container .rom-patcher-container-input select.loading+.rom-patcher-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
/* ZIP dialog */
|
||||
#rom-patcher-dialog-zip::backdrop,
|
||||
#rom-patcher-dialog-zip-backdrop {
|
||||
background-color: rgba(0, 0, 0, .75);
|
||||
backdrop-filter: blur(3px);
|
||||
/*
|
||||
transition: overlay 0.35s allow-discrete, display 0.35s allow-discrete, opacity 0.35s;
|
||||
opacity: 0;
|
||||
}
|
||||
#rom-patcher-dialog-zip[open]::backdrop {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
#rom-patcher-dialog-zip-backdrop {
|
||||
/* fallback for browsers not compatible with <dialog> */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip {
|
||||
min-width: 420px;
|
||||
vertical-align: middle;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
color: #999;
|
||||
box-sizing: border-box;
|
||||
box-shadow: rgba(0, 0, 0, .7) 0 0 24px;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
/*
|
||||
transition: overlay 0.35s allow-discrete, display 0.35s allow-discrete, opacity 0.35s;
|
||||
opacity: 0;
|
||||
}
|
||||
#rom-patcher-dialog-zip[open] {
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-message {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-file-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-file-list li {
|
||||
color: #3c3c3c;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip-file-list li:hover {
|
||||
background-color: #eee;
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* responsive */
|
||||
@media only screen and (max-width:641px) {
|
||||
#rom-patcher-container {
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
#rom-patcher-rom-info {
|
||||
font-size: 11px
|
||||
}
|
||||
|
||||
#rom-patcher-dialog-zip {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#rom-patcher-powered {
|
||||
margin-top: 16px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#rom-patcher-powered a {
|
||||
color: #cce;
|
||||
padding: 4px 8px;
|
||||
text-decoration: none;
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
#rom-patcher-powered a>img {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#rom-patcher-powered a:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 1;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue