2024-08-09 19:30:49 +02:00
|
|
|
/* IPS module for Rom Patcher JS v20230924 - Marc Robledo 2016-2023 - http://www.marcrobledo.com/license */
|
2017-07-22 11:58:52 +02:00
|
|
|
/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */
|
2017-07-22 19:57:15 +02:00
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
const IPS_MAGIC='PATCH';
|
2024-08-09 19:30:49 +02:00
|
|
|
const IPS_MAX_ROM_SIZE=0x1000000; //16 megabytes
|
2019-04-17 21:43:53 +02:00
|
|
|
const IPS_RECORD_RLE=0x0000;
|
|
|
|
const IPS_RECORD_SIMPLE=0x01;
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2025-04-27 17:43:07 +09:30
|
|
|
/* 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 '{'
|
2025-04-27 17:52:30 +09:30
|
|
|
/* 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"}
|
2025-04-27 17:43:07 +09:30
|
|
|
|
2024-08-09 19:30:49 +02:00
|
|
|
if(typeof module !== "undefined" && module.exports){
|
|
|
|
module.exports = IPS;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-07-22 11:58:52 +02:00
|
|
|
function IPS(){
|
|
|
|
this.records=[];
|
|
|
|
this.truncate=false;
|
2025-04-27 17:43:07 +09:30
|
|
|
this.isEBP=false;
|
|
|
|
this.EBPmetadata=JSON.stringify(EBP_META_DEFAULT)
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
IPS.prototype.addSimpleRecord=function(o, d){
|
2019-04-17 21:43:53 +02:00
|
|
|
this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d})
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
IPS.prototype.addRLERecord=function(o, l, b){
|
2019-04-17 21:43:53 +02:00
|
|
|
this.records.push({offset:o, type:IPS_RECORD_RLE, length:l, byte:b})
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
2025-04-27 17:43:07 +09:30
|
|
|
IPS.prototype.addEBPMetadata=function(author, title, description){
|
|
|
|
/* currently not used - no frontend support */
|
2025-04-27 17:52:30 +09:30
|
|
|
this.EBPmetadata=JSON.stringify({"patcher": "EBPatcher", "author": author, "title": title, "description": description})
|
2025-04-27 17:43:07 +09:30
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
IPS.prototype.toString=function(){
|
|
|
|
nSimpleRecords=0;
|
|
|
|
nRLERecords=0;
|
|
|
|
for(var i=0; i<this.records.length; i++){
|
2019-04-17 21:43:53 +02:00
|
|
|
if(this.records[i].type===IPS_RECORD_RLE)
|
2017-07-22 11:58:52 +02:00
|
|
|
nRLERecords++;
|
|
|
|
else
|
|
|
|
nSimpleRecords++;
|
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
var s='Simple records: '+nSimpleRecords;
|
2017-07-22 11:58:52 +02:00
|
|
|
s+='\nRLE records: '+nRLERecords;
|
|
|
|
s+='\nTotal records: '+this.records.length;
|
2025-04-27 17:43:07 +09:30
|
|
|
if(this.truncate && !this.isEBP)
|
2017-07-22 11:58:52 +02:00
|
|
|
s+='\nTruncate at: 0x'+this.truncate.toString(16);
|
2025-04-27 17:43:07 +09:30
|
|
|
s+='\nIs EBP: '+this.isEBP
|
2017-07-22 11:58:52 +02:00
|
|
|
return s
|
|
|
|
}
|
2018-04-28 15:57:54 +02:00
|
|
|
IPS.prototype.export=function(fileName){
|
2019-04-17 21:43:53 +02:00
|
|
|
var patchFileSize=5; //PATCH string
|
2017-07-22 11:58:52 +02:00
|
|
|
for(var i=0; i<this.records.length; i++){
|
2019-04-17 21:43:53 +02:00
|
|
|
if(this.records[i].type===IPS_RECORD_RLE)
|
|
|
|
patchFileSize+=(3+2+2+1); //offset+0x0000+length+RLE byte to be written
|
2017-07-22 11:58:52 +02:00
|
|
|
else
|
2019-04-17 21:43:53 +02:00
|
|
|
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
patchFileSize+=3; //EOF string
|
2025-04-27 17:43:07 +09:30
|
|
|
if(this.truncate && !this.isEBP)
|
2019-04-17 21:43:53 +02:00
|
|
|
patchFileSize+=3; //truncate
|
2025-04-27 17:43:07 +09:30
|
|
|
if(this.isEBP)
|
|
|
|
patchFileSize+=this.EBPmetadata.length
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2024-08-09 19:30:49 +02:00
|
|
|
tempFile=new BinFile(patchFileSize);
|
2025-04-27 17:43:07 +09:30
|
|
|
tempFile.fileName=fileName+('.ebp' ? this.isEBP : '.ips');
|
2019-04-17 21:43:53 +02:00
|
|
|
tempFile.writeString(IPS_MAGIC);
|
2017-07-22 11:58:52 +02:00
|
|
|
for(var i=0; i<this.records.length; i++){
|
|
|
|
var rec=this.records[i];
|
2019-04-17 21:43:53 +02:00
|
|
|
tempFile.writeU24(rec.offset);
|
|
|
|
if(rec.type===IPS_RECORD_RLE){
|
|
|
|
tempFile.writeU16(0x0000);
|
|
|
|
tempFile.writeU16(rec.length);
|
|
|
|
tempFile.writeU8(rec.byte);
|
2017-07-22 11:58:52 +02:00
|
|
|
}else{
|
2019-04-17 21:43:53 +02:00
|
|
|
tempFile.writeU16(rec.data.length);
|
|
|
|
tempFile.writeBytes(rec.data);
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
tempFile.writeString('EOF');
|
2025-04-27 17:43:07 +09:30
|
|
|
if(this.truncate && !this.isEBP)
|
2023-09-23 16:11:49 +07:00
|
|
|
tempFile.writeU24(this.truncate);
|
2019-04-17 21:43:53 +02:00
|
|
|
|
2025-04-27 17:43:07 +09:30
|
|
|
if(this.isEBP) {
|
|
|
|
tempFile.writeString(this.EBPmetadata)
|
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
|
|
|
|
return tempFile
|
|
|
|
}
|
|
|
|
IPS.prototype.apply=function(romFile){
|
2025-04-27 17:43:07 +09:30
|
|
|
if(this.truncate && !this.isEBP){
|
2022-04-17 19:10:24 +02:00
|
|
|
if(this.truncate>romFile.fileSize){ //expand (discussed here: https://github.com/marcrobledo/RomPatcher.js/pull/46)
|
2024-08-09 19:30:49 +02:00
|
|
|
tempFile=new BinFile(this.truncate);
|
|
|
|
romFile.copyTo(tempFile, 0, romFile.fileSize, 0);
|
2022-04-17 19:10:24 +02:00
|
|
|
}else{ //truncate
|
|
|
|
tempFile=romFile.slice(0, this.truncate);
|
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
}else{
|
2022-04-17 19:10:24 +02:00
|
|
|
//calculate target ROM size, expanding it if any record offset is beyond target ROM size
|
2019-04-17 21:43:53 +02:00
|
|
|
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;
|
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if(newFileSize===romFile.fileSize){
|
|
|
|
tempFile=romFile.slice(0, romFile.fileSize);
|
2017-07-22 11:58:52 +02:00
|
|
|
}else{
|
2024-08-09 19:30:49 +02:00
|
|
|
tempFile=new BinFile(newFileSize);
|
|
|
|
romFile.copyTo(tempFile,0);
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
romFile.seek(0);
|
2017-07-22 11:58:52 +02:00
|
|
|
|
|
|
|
for(var i=0; i<this.records.length; i++){
|
2019-04-17 21:43:53 +02:00
|
|
|
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);
|
2017-07-22 11:58:52 +02:00
|
|
|
}else{
|
2019-04-17 21:43:53 +02:00
|
|
|
tempFile.writeBytes(this.records[i].data);
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return tempFile
|
|
|
|
}
|
|
|
|
|
2024-08-09 19:30:49 +02:00
|
|
|
IPS.MAGIC=IPS_MAGIC;
|
2017-07-22 11:58:52 +02:00
|
|
|
|
|
|
|
|
2024-08-09 19:30:49 +02:00
|
|
|
IPS.fromFile=function(file){
|
2017-07-22 11:58:52 +02:00
|
|
|
var patchFile=new IPS();
|
2019-04-17 21:43:53 +02:00
|
|
|
file.seek(5);
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
while(!file.isEOF()){
|
|
|
|
var offset=file.readU24();
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
if(offset===0x454f46){ /* EOF */
|
|
|
|
if(file.isEOF()){
|
|
|
|
break;
|
|
|
|
}else if((file.offset+3)===file.fileSize){
|
|
|
|
patchFile.truncate=file.readU24();
|
|
|
|
break;
|
2025-04-27 17:43:07 +09:30
|
|
|
}else if (file.readU8()===EBP_MAGIC_META_OPENER) {
|
|
|
|
break;
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
|
|
|
|
var length=file.readU16();
|
|
|
|
|
|
|
|
if(length===IPS_RECORD_RLE){
|
|
|
|
patchFile.addRLERecord(offset, file.readU16(), file.readU8());
|
|
|
|
}else{
|
|
|
|
patchFile.addSimpleRecord(offset, file.readBytes(length));
|
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
return patchFile;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-04-27 17:43:07 +09:30
|
|
|
IPS.buildFromRoms=function(original, modified, asEBP=false){
|
2019-04-17 21:43:53 +02:00
|
|
|
var patch=new IPS();
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2025-04-27 17:43:07 +09:30
|
|
|
patch.isEBP=asEBP
|
|
|
|
|
|
|
|
if(modified.fileSize<original.fileSize && !patch.isEBP){
|
2019-04-17 21:43:53 +02:00
|
|
|
patch.truncate=modified.fileSize;
|
2018-04-27 21:06:44 +02:00
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
//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();
|
2017-07-22 11:58:52 +02:00
|
|
|
|
|
|
|
if(b1!==b2){
|
2019-04-17 21:43:53 +02:00
|
|
|
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)
|
2018-04-27 21:06:44 +02:00
|
|
|
break;
|
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
b1=original.isEOF()?0x00:original.readU8();
|
|
|
|
b2=modified.readU8();
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
//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};
|
2018-09-19 09:44:18 +02:00
|
|
|
}else{
|
2019-04-17 21:43:53 +02:00
|
|
|
// 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;
|
2018-09-19 09:44:18 +02:00
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
}else{
|
2024-08-09 19:30:49 +02:00
|
|
|
if(startOffset>=IPS_MAX_ROM_SIZE){
|
2025-04-27 17:43:07 +09:30
|
|
|
throw new Error(`Files are too big for ${'EBP' ? patch.isEBP : 'IPS'} format`);
|
2020-05-02 12:10:46 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
if(RLEmode && differentData.length>2){
|
|
|
|
patch.addRLERecord(startOffset, differentData.length, differentData[0]);
|
|
|
|
}else{
|
|
|
|
patch.addSimpleRecord(startOffset, differentData);
|
|
|
|
}
|
|
|
|
previousRecord=patch.records[patch.records.length-1];
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
}
|
|
|
|
}
|
2017-07-22 11:58:52 +02:00
|
|
|
|
2019-04-17 21:43:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]);
|
2017-07-22 11:58:52 +02:00
|
|
|
}
|
|
|
|
}
|
2019-04-17 21:43:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
return patch
|
2024-08-09 19:30:49 +02:00
|
|
|
}
|