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

refactor and added EBP description showing in frontend

This commit is contained in:
Marc Robledo 2025-04-30 01:11:55 +02:00
parent 198c046424
commit 844cfcb5a6
5 changed files with 88 additions and 55 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, EBP and xdelta."/>
<meta name="keywords" content="ips,ebp,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, EBP 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">
@ -152,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

@ -242,8 +242,8 @@ const RomPatcher = (function () {
format = 'ips';
var patch;
if (format === 'ips' || format === 'ebp') {
patch = IPS.buildFromRoms(originalFile, modifiedFile, format === 'ebp');
if (format === 'ips') {
patch = IPS.buildFromRoms(originalFile, modifiedFile);
} else if (format === 'bps') {
patch = BPS.buildFromRoms(originalFile, modifiedFile, (originalFile.fileSize <= 4194304));
} else if (format === 'ppf') {
@ -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|ebp|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,21 +1,14 @@
/* 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;
/* There is also support for the EBP (EarthBound Patch) format here. */
/* EBP has no real specification, but an implementation is here: https://github.com/Lyrositor/EBPatcher */
/* EBP is actually just IPS with some JSON metadata stuck on the end. */
/* We can safely ignore this data when applying patches. */
/* When creating patches, insert this data after everything else. */
/* Finally, EBP doesn't seem to support truncation metadata. */
const EBP_MAGIC_META_OPENER = 0x7B //UTF-8 '{'
/* EBPatcher (linked above) expects the "patcher" field to be EBPatcher to read the metadata. Can't imagine why... */
/* CoilSnake (EB modding tool) inserts this manually too. */
const EBP_META_DEFAULT={"patcher": "EBPatcher", "author": "Unknown", "title": "Untitled", "description": "No description"}
if(typeof module !== "undefined" && module.exports){
module.exports = IPS;
@ -25,8 +18,7 @@ if(typeof module !== "undefined" && module.exports){
function IPS(){
this.records=[];
this.truncate=false;
this.isEBP=false;
this.EBPmetadata=JSON.stringify(EBP_META_DEFAULT)
this.EBPmetadata=null;
}
IPS.prototype.addSimpleRecord=function(o, d){
this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d})
@ -34,9 +26,31 @@ 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.addEBPMetadata=function(author, title, description){
/* currently not used - no frontend support */
this.EBPmetadata=JSON.stringify({"patcher": "EBPatcher", "author": author, "title": title, "description": description})
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'){
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;
@ -50,9 +64,10 @@ IPS.prototype.toString=function(){
var s='Simple records: '+nSimpleRecords;
s+='\nRLE records: '+nRLERecords;
s+='\nTotal records: '+this.records.length;
if(this.truncate && !this.isEBP)
if(this.truncate && !this.EBPmetadata)
s+='\nTruncate at: 0x'+this.truncate.toString(16);
s+='\nIs EBP: '+this.isEBP
else if(this.EBPmetadata)
s+='\nEBP Metadata: '+JSON.stringify(this.EBPmetadata);
return s
}
IPS.prototype.export=function(fileName){
@ -64,13 +79,13 @@ IPS.prototype.export=function(fileName){
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
}
patchFileSize+=3; //EOF string
if(this.truncate && !this.isEBP)
if(this.truncate && !this.EBPmetadata)
patchFileSize+=3; //truncate
if(this.isEBP)
patchFileSize+=this.EBPmetadata.length
else if(this.EBPmetadata)
patchFileSize+=JSON.stringify(this.EBPmetadata);
tempFile=new BinFile(patchFileSize);
tempFile.fileName=fileName+('.ebp' ? this.isEBP : '.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];
@ -86,17 +101,15 @@ IPS.prototype.export=function(fileName){
}
tempFile.writeString('EOF');
if(this.truncate && !this.isEBP)
if(this.truncate && !this.EBPmetadata)
tempFile.writeU24(this.truncate);
if(this.isEBP) {
tempFile.writeString(this.EBPmetadata)
}
else if(this.EBPmetadata)
tempFile.writeString(JSON.stringify(this.EBPmetadata));
return tempFile
}
IPS.prototype.apply=function(romFile){
if(this.truncate && !this.isEBP){
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);
@ -159,7 +172,9 @@ IPS.fromFile=function(file){
}else if((file.offset+3)===file.fileSize){
patchFile.truncate=file.readU24();
break;
}else if (file.readU8()===EBP_MAGIC_META_OPENER) {
}else if (file.readU8()==='{'.charCodeAt(0)) {
file.skip(-1);
patchFile.setEBPMetadata(JSON.parse(file.readString(file.fileSize-file.offset)));
break;
}
}
@ -179,10 +194,14 @@ IPS.fromFile=function(file){
IPS.buildFromRoms=function(original, modified, asEBP=false){
var patch=new IPS();
patch.isEBP=asEBP
if(modified.fileSize<original.fileSize && !patch.isEBP){
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)
@ -231,7 +250,7 @@ IPS.buildFromRoms=function(original, modified, asEBP=false){
}
}else{
if(startOffset>=IPS_MAX_ROM_SIZE){
throw new Error(`Files are too big for ${'EBP' ? patch.isEBP : 'IPS'} format`);
throw new Error(`Files are too big for ${patch.EBPmetadata? 'EBP' : 'IPS'} format`);
return null;
}

42
test.js
View file

@ -14,9 +14,6 @@
- IPS test
- Patch: https://www.romhacking.net/hacks/3784/
- ROM: Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb [CRC32=d5ec24e4]
- EBP test
- Patch: https://forum.starmen.net/forum/Community/PKHack/NickBound/page/1#post2333521
- ROM: EarthBound (USA).sfc [CRC32=dc9bb451]
- BPS test
- Patch: https://www.romhacking.net/translations/6297/
- ROM: Samurai Kid (Japan).gbc [CRC32=44a9ddfb]
@ -26,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]
@ -53,14 +56,6 @@ const TEST_PATCHES = [
patchCrc32: 0x0b742316,
patchDownload: 'https://www.romhacking.net/hacks/3784/',
outputCrc32: 0xf0799017
}, {
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: 'BPS - Samurai Kid translation',
romFile: 'Samurai Kid (Japan).gbc',
@ -85,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',
@ -92,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,
@ -139,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);
@ -165,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;
}
@ -181,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);