/* 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; } 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}) } 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'){ 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; for(var i=0; iromFile.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; inewFileSize){ 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; i6){ // 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 ${patch.EBPmetadata? 'EBP' : '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