1
0
Fork 0
mirror of https://github.com/marcrobledo/RomPatcher.js.git synced 2025-08-06 16:50:54 +00:00

Merge pull request #88 from Supremekirb/EarthBoundPatch

Support for EBP format
This commit is contained in:
Marc Robledo 2025-04-30 08:56:08 +02:00 committed by GitHub
commit bd43cffc10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 94 additions and 21 deletions

View file

@ -3,8 +3,8 @@
<head>
<title>Rom Patcher JS</title>
<meta http-equiv="content-Type" content="text/html; charset=UTF-8"/>
<meta name="description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, PPF and xdelta."/>
<meta name="keywords" content="ips,ups,aps,bps,rup,ninja,ppf,xdelta,patcher,online,html5,web,rom,patch,hack,translation"/>
<meta name="description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, EBP, PPF and xdelta."/>
<meta name="keywords" content="ips,ups,aps,bps,rup,ninja,ebp,ppf,xdelta,patcher,online,html5,web,rom,patch,hack,translation"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<link rel="manifest" href="./manifest.json"/>
<link rel="shortcut icon" href="./webapp/app_icon_16.png" type="image/png" sizes="16x16"/>
@ -27,7 +27,7 @@
<meta name="twitter:domain" content="marcrobledo.com">
<meta property="og:title" content="Rom Patcher JS">
<meta name="twitter:title" content="Rom Patcher JS">
<meta name="twitter:description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, PPF and xdelta.">
<meta name="twitter:description" content="An online web-based ROM patcher. Supported formats: IPS, BPS, UPS, APS, RUP, EBP, PPF and xdelta.">
<meta property="og:image" content="https://www.marcrobledo.com/RomPatcher.js/webapp/thumbnail.jpg">
<meta name="twitter:image" content="https://www.marcrobledo.com/RomPatcher.js/webapp/thumbnail.jpg">
<meta name="twitter:card" content="photo">
@ -83,7 +83,7 @@
<div class="row m-b" id="rom-patcher-row-file-patch">
<div class="text-right"><label for="rom-patcher-input-file-patch" data-localize="yes">Patch file:</label></div>
<div class="rom-patcher-container-input">
<input type="file" id="rom-patcher-input-file-patch" class="empty" accept=".ips,.ups,.bps,.aps,.rup,.ppf,.mod,.xdelta,.vcdiff,.zip" disabled />
<input type="file" id="rom-patcher-input-file-patch" class="empty" accept=".ips,.ups,.bps,.aps,.rup,.ppf,.mod,.ebp,.xdelta,.vcdiff,.zip" disabled />
</div>
</div>
<div class="row m-b" id="rom-patcher-row-patch-description">
@ -126,6 +126,7 @@
<option value="ups">UPS</option>
<option value="aps">APS</option>
<option value="rup">RUP</option>
<option value="ebp">EBP</option>
</select>
</div>
</div>
@ -151,7 +152,7 @@
<button id="button-settings" class="btn-transparent"><img src="./webapp/icon_settings.svg" loading="lazy" class="icon settings" /> <span data-localize="yes">Settings</span></button>
</div>
Rom Patcher JS <small><a href="legacy/" rel="nofollow">v3.1</a></small> by <a href="/">Marc Robledo</a>
Rom Patcher JS <small><a href="legacy/" rel="nofollow">v3.2</a></small> by <a href="/">Marc Robledo</a>
<br />
<img src="./webapp/icon_github.svg" loading="lazy" class="icon github" /> <a href="https://github.com/marcrobledo/RomPatcher.js/" target="_blank">See on GitHub</a>
<img src="./webapp/icon_heart.svg" loading="lazy" class="icon heart" /> <a href="https://www.paypal.me/marcrobledo/5" target="_blank" rel="nofollow">Donate</a>

View file

@ -58,7 +58,7 @@ program
.description('creates a patch based on two ROMs')
.argument('<original_rom_file>', 'the original ROM')
.argument('<modified_rom_file>','the modified ROM')
.option('-f, --format <format>','patch format (allowed values: ips [default], bps, ppf, ups, aps, rup)')
.option('-f, --format <format>','patch format (allowed values: ips [default], bps, ppf, ups, aps, rup, ebp)')
.action(function(originalRomPath, modifiedRomPath, options) {
console.log(options);
try{

View file

@ -254,6 +254,8 @@ const RomPatcher = (function () {
patch = APS.buildFromRoms(originalFile, modifiedFile);
} else if (format === 'rup') {
patch = RUP.buildFromRoms(originalFile, modifiedFile);
} else if (format === 'ebp') {
patch = IPS.buildFromRoms(originalFile, modifiedFile, true);
} else {
throw new Error('Invalid patch format');
}

View file

@ -1175,7 +1175,7 @@ const ZIPManager = (function (romPatcherWeb) {
const ZIP_MAGIC = '\x50\x4b\x03\x04';
const FILTER_PATCHES = /\.(ips|ups|bps|aps|rup|ppf|mod|xdelta|vcdiff)$/i;
const FILTER_PATCHES = /\.(ips|ups|bps|aps|rup|ppf|mod|ebp|xdelta|vcdiff)$/i;
//const FILTER_ROMS=/(?<!\.(txt|diz|rtf|docx?|xlsx?|html?|pdf|jpe?g|gif|png|bmp|webp|zip|rar|7z))$/i; //negative lookbehind is not compatible with Safari https://stackoverflow.com/a/51568859
const FILTER_NON_ROMS = /(\.(txt|diz|rtf|docx?|xlsx?|html?|pdf|jpe?g|gif|png|bmp|webp|zip|rar|7z))$/i;

View file

@ -1,11 +1,15 @@
/* IPS module for Rom Patcher JS v20230924 - Marc Robledo 2016-2023 - http://www.marcrobledo.com/license */
/* IPS module for Rom Patcher JS v20250430 - Marc Robledo 2016-2025 - http://www.marcrobledo.com/license */
/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */
/* This file also acts as EBP (EarthBound Patch) module */
/* EBP is actually just IPS with some JSON metadata stuck on the end (implementation: https://github.com/Lyrositor/EBPatcher) */
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;
}
@ -14,6 +18,7 @@ if(typeof module !== "undefined" && module.exports){
function IPS(){
this.records=[];
this.truncate=false;
this.EBPmetadata=null;
}
IPS.prototype.addSimpleRecord=function(o, d){
this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d})
@ -21,6 +26,33 @@ IPS.prototype.addSimpleRecord=function(o, d){
IPS.prototype.addRLERecord=function(o, l, b){
this.records.push({offset:o, type:IPS_RECORD_RLE, length:l, byte:b})
}
IPS.prototype.setEBPMetadata=function(metadataObject){
if(typeof metadataObject !== 'object')
throw new TypeError('metadataObject must be an object');
for(var key in metadataObject){
if(typeof metadataObject[key] !== 'string')
throw new TypeError('metadataObject values must be strings');
}
/* EBPatcher (linked above) expects the "patcher" field to be EBPatcher to read the metadata */
/* CoilSnake (EB modding tool) inserts this manually too */
/* So we also add it here for compatibility purposes */
this.EBPmetadata={patcher:'EBPatcher', ...metadataObject};
}
IPS.prototype.getDescription=function(){
if(this.EBPmetadata){
var description='';
for(var key in this.EBPmetadata){
if(key==='patcher')
continue;
const keyPretty=key.charAt(0).toUpperCase() + key.slice(1);
description+=keyPretty+': '+this.EBPmetadata[key]+'\n';
}
return description.trim();
}
return null;
}
IPS.prototype.toString=function(){
nSimpleRecords=0;
nRLERecords=0;
@ -33,8 +65,10 @@ IPS.prototype.toString=function(){
var s='Simple records: '+nSimpleRecords;
s+='\nRLE records: '+nRLERecords;
s+='\nTotal records: '+this.records.length;
if(this.truncate)
if(this.truncate && !this.EBPmetadata)
s+='\nTruncate at: 0x'+this.truncate.toString(16);
else if(this.EBPmetadata)
s+='\nEBP Metadata: '+JSON.stringify(this.EBPmetadata);
return s
}
IPS.prototype.export=function(fileName){
@ -46,11 +80,13 @@ IPS.prototype.export=function(fileName){
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
}
patchFileSize+=3; //EOF string
if(this.truncate)
if(this.truncate && !this.EBPmetadata)
patchFileSize+=3; //truncate
else if(this.EBPmetadata)
patchFileSize+=JSON.stringify(this.EBPmetadata);
tempFile=new BinFile(patchFileSize);
tempFile.fileName=fileName+'.ips';
tempFile.fileName=fileName+(this.EBPmetadata? '.ebp' : '.ips');
tempFile.writeString(IPS_MAGIC);
for(var i=0; i<this.records.length; i++){
var rec=this.records[i];
@ -66,14 +102,15 @@ IPS.prototype.export=function(fileName){
}
tempFile.writeString('EOF');
if(this.truncate)
if(this.truncate && !this.EBPmetadata)
tempFile.writeU24(this.truncate);
else if(this.EBPmetadata)
tempFile.writeString(JSON.stringify(this.EBPmetadata));
return tempFile
}
IPS.prototype.apply=function(romFile){
if(this.truncate){
if(this.truncate && !this.EBPmetadata){
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);
@ -136,6 +173,10 @@ IPS.fromFile=function(file){
}else if((file.offset+3)===file.fileSize){
patchFile.truncate=file.readU24();
break;
}else if (file.readU8()==='{'.charCodeAt(0)) {
file.skip(-1);
patchFile.setEBPMetadata(JSON.parse(file.readString(file.fileSize-file.offset)));
break;
}
}
@ -151,11 +192,17 @@ IPS.fromFile=function(file){
}
IPS.buildFromRoms=function(original, modified){
IPS.buildFromRoms=function(original, modified, asEBP=false){
var patch=new IPS();
if(modified.fileSize<original.fileSize){
if(!asEBP && modified.fileSize<original.fileSize){
patch.truncate=modified.fileSize;
}else if(asEBP){
patch.setEBPMetadata(typeof asEBP==='object'? asEBP : {
'Author':'Unknown',
'Title':'Untitled',
'Description':'No description',
});
}
//solucion: guardar startOffset y endOffset (ir mirando de 6 en 6 hacia atrás)
@ -204,7 +251,7 @@ IPS.buildFromRoms=function(original, modified){
}
}else{
if(startOffset>=IPS_MAX_ROM_SIZE){
throw new Error('Files are too big for IPS format');
throw new Error(`Files are too big for ${patch.EBPmetadata? 'EBP' : 'IPS'} format`);
return null;
}

31
test.js
View file

@ -23,9 +23,15 @@
- APS test
- Patch: http://dorando.emuverse.com/projects/eduardo_a2j/zelda-ocarina-of-time.html
- ROM: Legend of Zelda, The - Ocarina of Time (USA).z64 [CRC32=7e107c35]
- APS (GBA) test
- Patch: http://ngplus.net/InsaneDifficultyArchive/www.insanedifficulty.com/board/index9837.html?/files/file/65-final-fantasy-tactics-advance-x/
- ROM: Final Fantasy Tactics Advance (USA).gba [CRC32=5645e56c]
- RUP test
- Patch: https://www.romhacking.net/translations/843/
- ROM: Uchuu no Kishi - Tekkaman Blade (Japan).sfc [CRC32=cd16c529]
- EBP test
- Patch: https://forum.starmen.net/forum/Community/PKHack/NickBound/page/1#post2333521
- ROM: EarthBound (USA).sfc [CRC32=dc9bb451]
- xdelta test
- Patch: https://www.romhacking.net/hacks/2871/
- ROM: New Super Mario Bros. (USA, Australia).nds [CRC32=0197576a]
@ -74,6 +80,14 @@ const TEST_PATCHES = [
patchCrc32: 0x7b70119d,
patchDownload: 'http://dorando.emuverse.com/projects/eduardo_a2j/zelda-ocarina-of-time.html',
outputCrc32: 0x7866f1ca
}, {
title: 'APS (GBA) - Final Fantasy Tactics Advance X',
romFile: 'Final Fantasy Tactics Advance (USA).gba',
romCrc32: 0x5645e56c,
patchFile: 'FFTA_X_1.0.3.1.aps',
patchCrc32: 0x77e5f2ae,
patchDownload: 'http://ngplus.net/InsaneDifficultyArchive/www.insanedifficulty.com/board/index9837.html?/files/file/65-final-fantasy-tactics-advance-x/',
outputCrc32: 0x49a5539a
}, {
title: 'Tekkaman Blade translation',
romFile: 'Uchuu no Kishi - Tekkaman Blade (Japan).sfc',
@ -81,8 +95,17 @@ const TEST_PATCHES = [
patchFile: 'Tekkaman Blade v1.0.rup',
patchCrc32: 0x621ab323,
patchDownload: 'https://www.romhacking.net/hacks/4633/',
outputCrc32: 0xe83e9b0a
//outputCrc32: 0xe83e9b0a //Headerless
outputCrc32: 0xda833bce //Headered
}, {
title: 'EBP - Mother Rebound',
romFile: 'EarthBound (USA).sfc',
romCrc32: 0xdc9bb451,
patchFile: 'Mother_Rebound.ebp',
patchCrc32: 0x271719e1,
patchDownload: 'https://forum.starmen.net/forum/Community/PKHack/NickBound/page/1#post2333521',
outputCrc32: 0x5065b02f
}, {
title: 'NSMB Hack Domain Infusion',
romFile: 'New Super Mario Bros. (USA, Australia).nds',
romCrc32: 0x0197576a,
@ -128,7 +151,7 @@ _test('HashCalculator integrity', function () {
const MODIFIED_TEST_DATA = (new Uint8Array([
98, 91, 64, 8, 35, 53, 122, 167, 52, 253, 222, 156, 247, 82, 227, 213, 22, 221, 17, 247, 107, 102, 164, 254, 221, 8, 207, 63, 117, 164, 223, 10, 1, 77, 87, 123, 48, 9, 111, 64, 233, 118, 1, 36, 1, 60, 208, 245, 136, 126, 29, 231, 168, 18, 125, 172, 11, 184, 81, 20, 16, 30, 154, 16, 236, 21, 5, 74, 255, 112, 171, 198, 185, 89, 2, 98, 45, 164, 214, 55, 103, 15, 217, 95, 212, 133, 184, 21, 67, 144, 198, 163, 76, 35, 248, 229, 163, 37, 103, 33, 193, 96, 77, 255, 117, 89, 193, 61, 64, 253, 119, 82, 49, 187, 195, 165, 205, 140, 222, 134, 249, 68, 224, 248, 144, 207, 18, 126
])).buffer;
['ips', 'bps', 'ppf', 'ups', 'aps', 'rup'].forEach(function (patchFormat) {
['ips', 'bps', 'ppf', 'ups', 'aps', 'rup', 'ebp'].forEach(function (patchFormat) {
_test('create and apply ' + patchFormat.toUpperCase(), function () {
const originalFile = new BinFile(TEST_DATA);
const modifiedFile = new BinFile(MODIFIED_TEST_DATA);
@ -154,7 +177,7 @@ TEST_PATCHES.forEach(function (patchInfo) {
const patchPath = TEST_PATH + 'patches/' + patchInfo.patchFile;
if (!existsSync(patchPath)) {
console.log(chalk.yellow('! skipping patch ' + patchInfo.title));
console.log(chalk.yellow(' patch file not found: ' + patchInfo.patchFile));
console.log(chalk.yellow(' patch file not found: ' + TEST_PATH + 'patches/' + patchInfo.patchFile));
console.log(chalk.yellow(' download patch at ' + patchInfo.patchDownload));
return false;
}
@ -170,7 +193,7 @@ TEST_PATCHES.forEach(function (patchInfo) {
const romPath = TEST_PATH + 'roms/' + patchInfo.romFile;
if (!existsSync(romPath)) {
console.log(chalk.yellow('! skipping patch ' + patchInfo.title));
console.log(chalk.yellow(' ROM file not found: ' + patchInfo.romFile));
console.log(chalk.yellow(' ROM file not found: ' + TEST_PATH + 'roms/' + patchInfo.romFile));
return false;
}
const romFile = new BinFile(romPath);