/* Rom Patcher JS v20240721 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */ const TOO_BIG_ROM_SIZE=67108863; const HEADERS_INFO=[ [/\.nes$/, 16, 1024], //interNES [/\.fds$/, 16, 65500], //fwNES [/\.lnx$/, 64, 1024], //[/\.rom$/, 8192, 1024], //jaguar [/\.(pce|nes|gbc?|smc|sfc|fig|swc)$/, 512, 1024] ]; /* service worker */ /* const FORCE_HTTPS=true; if(FORCE_HTTPS && location.protocol==='http:') location.href=window.location.href.replace('http:','https:'); else if(location.protocol==='https:' && 'serviceWorker' in navigator && window.location.hostname==='www.marcrobledo.com') navigator.serviceWorker.register('/RomPatcher.js/_cache_service_worker.js', {scope: '/RomPatcher.js/'}); */ var romFile, patchFile, patch, romFile1, romFile2, tempFile, headerSize, oldHeader; var CAN_USE_WEB_WORKERS=true; var WEBWORKERS_PATH='./js/'; var webWorkerApply,webWorkerCreate,webWorkerCrc; try{ webWorkerApply=new Worker(WEBWORKERS_PATH + 'worker_apply.js'); webWorkerApply.onmessage = event => { // listen for events from the worker //retrieve arraybuffers back from webworker if(!el('checkbox-removeheader').checked && !el('checkbox-addheader').checked){ //when adding/removing header we don't need the arraybuffer back since we made a copy previously romFile._u8array=event.data.romFileU8Array; romFile._dataView=new DataView(romFile._u8array.buffer); } patchFile._u8array=event.data.patchFileU8Array; patchFile._dataView=new DataView(patchFile._u8array.buffer); if(event.data.patchedRomU8Array) preparePatchedRom(romFile, new MarcFile(event.data.patchedRomU8Array.buffer), headerSize); setTabApplyEnabled(true); if(event.data.errorMessage) setMessage('apply', _(event.data.errorMessage.replace('Error: ','')), 'error'); else setMessage('apply'); }; webWorkerApply.onerror = event => { // listen for events from the worker setTabApplyEnabled(true); setMessage('apply', _(event.message.replace('Error: ','')), 'error'); }; webWorkerCreate=new Worker(WEBWORKERS_PATH + 'worker_create.js'); webWorkerCreate.onmessage = event => { // listen for events from the worker var newPatchFile=new MarcFile(event.data.patchFileU8Array); newPatchFile.fileName=romFile2.fileName.replace(/\.[^\.]+$/,'')+'.'+el('select-patch-type').value; newPatchFile.save(); setMessage('create'); setTabCreateEnabled(true); }; webWorkerCreate.onerror = event => { // listen for events from the worker setTabCreateEnabled(true); setMessage('create', _(event.message.replace('Error: ','')), 'error'); }; webWorkerCrc=new Worker(WEBWORKERS_PATH + 'worker_crc.js'); webWorkerCrc.onmessage = event => { // listen for events from the worker //console.log('received_crc'); el('crc32').innerHTML=padZeroes(event.data.crc32, 4); el('md5').innerHTML=padZeroes(event.data.md5, 16); romFile._u8array=event.data.u8array; romFile._dataView=new DataView(event.data.u8array.buffer); if(window.crypto&&window.crypto.subtle&&window.crypto.subtle.digest){ sha1(romFile); } validateSource(); setTabApplyEnabled(true); }; webWorkerCrc.onerror = event => { // listen for events from the worker setMessage('apply', event.message.replace('Error: ',''), 'error'); }; }catch(e){ CAN_USE_WEB_WORKERS=false; } /* Shortcuts */ function addEvent(e,ev,f){e.addEventListener(ev,f,false)} function el(e){return document.getElementById(e)} function _(str){return (LOCALIZATION[AppSettings.langCode] && LOCALIZATION[AppSettings.langCode][str]) || LOCALIZATION['en'][str] || str} /* custom patcher */ function isCustomPatcherEnabled(){ return typeof CUSTOM_PATCHER!=='undefined' && typeof CUSTOM_PATCHER==='object' && CUSTOM_PATCHER.length } function parseCustomPatch(customPatch){ patchFile=customPatch.fetchedFile; patchFile.seek(0); _readPatchFile(); if(typeof patch.validateSource === 'undefined'){ if(typeof customPatch.crc==='number'){ patch.validateSource=function(romFile,headerSize){ return customPatch.crc===crc32(romFile, headerSize) }; patch.getValidationInfo=function(){ return [{ 'type':'CRC32', 'value':padZeroes(customPatch.crc,4) }] }; }else if(typeof customPatch.crc==='object'){ patch.validateSource=function(romFile,headerSize){ for(var i=0; i result.arrayBuffer()) // Gets the response and returns it as a blob .then(arrayBuffer => { patchFile=CUSTOM_PATCHER[customPatchIndex].fetchedFile=new MarcFile(arrayBuffer); patchFile.fileName=customPatch.file.replace(/^.*[\/\\]/g,''); if(patchFile.getExtension()!=='jar' && patchFile.readString(4).startsWith(ZIP_MAGIC)) ZIPManager.parseFile(CUSTOM_PATCHER[customPatchIndex].fetchedFile, compressedFileIndex); else parseCustomPatch(CUSTOM_PATCHER[customPatchIndex]); setMessage('apply'); }) .catch(function(evt){ setMessage('apply', (_('error_downloading')/* + evt.message */).replace('%s', CUSTOM_PATCHER[customPatchIndex].file.replace(/^.*[\/\\]/g,'')), 'error'); }); }else{ var xhr=new XMLHttpRequest(); xhr.open('GET', uri, true); xhr.responseType='arraybuffer'; xhr.onload=function(evt){ if(this.status===200){ patchFile=CUSTOM_PATCHER[customPatchIndex].fetchedFile=new MarcFile(xhr.response); patchFile.fileName=customPatch.file.replace(/^.*[\/\\]/g,''); if(patchFile.getExtension()!=='jar' && patchFile.readString(4).startsWith(ZIP_MAGIC)) ZIPManager.parseFile(CUSTOM_PATCHER[customPatchIndex].fetchedFile, compressedFileIndex); else parseCustomPatch(CUSTOM_PATCHER[customPatchIndex]); setMessage('apply'); }else{ setMessage('apply', _('error_downloading').replace('%s', CUSTOM_PATCHER[customPatchIndex].file.replace(/^.*[\/\\]/g,''))+' ('+this.status+')', 'error'); } }; xhr.onerror=function(evt){ setMessage('apply', 'error_downloading', 'error'); }; xhr.send(null); } } function _parseROM(){ el('checkbox-addheader').checked=false; el('checkbox-removeheader').checked=false; if(romFile.getExtension()!=='jar' && romFile.readString(4).startsWith(ZIP_MAGIC)){ ZIPManager.parseFile(romFile); setTabApplyEnabled(false); }else{ if(headerSize=canHaveFakeHeader(romFile)){ el('row-addheader').className='row m-b'; if(headerSize<1024){ el('headersize').innerHTML=headerSize+'b'; }else{ el('headersize').innerHTML=parseInt(headerSize/1024)+'kb'; } el('row-removeheader').className='row m-b hide'; }else if(headerSize=hasHeader(romFile)){ el('row-addheader').className='row m-b hide'; el('row-removeheader').className='row m-b'; }else{ el('row-addheader').className='row m-b hide'; el('row-removeheader').className='row m-b hide'; } updateChecksums(romFile, 0); } } var UI={ localize:function(){ if(typeof LOCALIZATION[AppSettings.langCode]==='undefined') return false; var translatableElements=document.querySelectorAll('*[data-localize]'); for(var i=0; i33554432 && !force){ el('crc32').innerHTML='File is too big. Force calculate checksum'; el('md5').innerHTML=''; el('sha1').innerHTML=''; setTabApplyEnabled(true); return false; } el('crc32').innerHTML='Calculating...'; el('md5').innerHTML='Calculating...'; if(CAN_USE_WEB_WORKERS){ setTabApplyEnabled(false); webWorkerCrc.postMessage({u8array:file._u8array, startOffset:startOffset}, [file._u8array.buffer]); if(window.crypto&&window.crypto.subtle&&window.crypto.subtle.digest){ el('sha1').innerHTML='Calculating...'; } }else{ window.setTimeout(function(){ el('crc32').innerHTML=padZeroes(crc32(file, startOffset), 4); el('md5').innerHTML=padZeroes(md5(file, startOffset), 16); validateSource(); setTabApplyEnabled(true); }, 30); if(window.crypto&&window.crypto.subtle&&window.crypto.subtle.digest){ el('sha1').innerHTML='Calculating...'; sha1(file); } } } function validateSource(){ if(patch && romFile && typeof patch.validateSource !== 'undefined'){ if(patch.validateSource(romFile, el('checkbox-removeheader').checked && hasHeader(romFile))){ el('crc32').className='valid'; setMessage('apply'); }else{ el('crc32').className='invalid'; setMessage('apply', 'error_crc_input', 'warning'); } }else{ el('crc32').className=''; setMessage('apply'); } } function _getRomSystem(file){ if(file.fileSize>0x0200 && file.fileSize%4===0){ if(/\.gbc?/i.test(file.fileName)){ var 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 ]; file.offset=0x104; var valid=true; for(var i=0; i>> 0) & 0xff; } /* fix checksum */ info.fix=function(file){ file.offset=0x014d; file.writeU8(this.calculated); } }else if(info.system==='smd'){ /* get current checksum */ file.offset=0x018e; info.current=file.readU16(); /* calculate checksum */ info.calculated=0x0000; file.offset=0x0200; while(!file.isEOF()){ info.calculated=((info.calculated + file.readU16()) >>> 0) & 0xffff; } /* fix checksum */ info.fix=function(file){ file.offset=0x018e; file.writeU16(this.calculated); } }else{ info=null; } return info; } function _readPatchFile(){ setTabApplyEnabled(false); patchFile.littleEndian=false; var header=patchFile.readString(6); if(patchFile.getExtension()!=='jar' && header.startsWith(ZIP_MAGIC)){ patch=false; validateSource(); setTabApplyEnabled(false); ZIPManager.parseFile(patchFile); }else{ if(header.startsWith(IPS_MAGIC)){ patch=parseIPSFile(patchFile); }else if(header.startsWith(UPS_MAGIC)){ patch=parseUPSFile(patchFile); }else if(header.startsWith(APS_N64_MAGIC)){ patch=parseAPSFile(patchFile); }else if(header.startsWith(APS_GBA_MAGIC)){ patch=APSGBA.fromFile(patchFile); }else if(header.startsWith(BPS_MAGIC)){ patch=parseBPSFile(patchFile); }else if(header.startsWith(RUP_MAGIC)){ patch=parseRUPFile(patchFile); }else if(header.startsWith(PPF_MAGIC)){ patch=parsePPFFile(patchFile); }else if(header.startsWith(PMSR_MAGIC)){ patch=parseMODFile(patchFile); }else if(header.startsWith(VCDIFF_MAGIC)){ patch=parseVCDIFF(patchFile); }else{ patch=null; setMessage('apply', 'error_invalid_patch', 'error'); } if(patch && patch.getValidationInfo){ const validationInfos=patch.getValidationInfo(); el('row-expected-source-info').className='row m-b'; el('row-expected-source-info').innerHTML=''; validationInfos.forEach(function(validationInfo){ var leftCol=document.createElement('div'); leftCol.className='leftcol text-right'; leftCol.innerHTML=_('expected_source').replace('%s', validationInfo.type); var rightCol=document.createElement('div'); rightCol.className='rightcol'; /* var a=document.createElement('a'); a.href='https://www.google.com/search?q=%22'+validationInfo.value+'%22'; a.target='_blank'; a.className='clickable'; a.innerHTML=validationInfo.value; rightCol.appendChild(a); */ rightCol.innerHTML=validationInfo.value; el('row-expected-source-info').appendChild(leftCol); el('row-expected-source-info').appendChild(rightCol); }); }else{ el('row-expected-source-info').className='row m-b hide'; el('row-expected-source-info').innerHTML=''; } validateSource(); setTabApplyEnabled(true); } } function preparePatchedRom(originalRom, patchedRom, headerSize){ if(AppSettings.outputFileNameMatch){ patchedRom.fileName=patchFile.fileName.replace(/\.\w+$/i, (/\.\w+$/i.test(originalRom.fileName)? originalRom.fileName.match(/\.\w+$/i)[0] : '')); }else{ patchedRom.fileName=originalRom.fileName.replace(/\.([^\.]*?)$/, ' (patched).$1'); } patchedRom.fileType=originalRom.fileType; if(headerSize){ if(el('checkbox-removeheader').checked){ var patchedRomWithOldHeader=new MarcFile(headerSize+patchedRom.fileSize); oldHeader.copyToFile(patchedRomWithOldHeader, 0); patchedRom.copyToFile(patchedRomWithOldHeader, 0, patchedRom.fileSize, headerSize); patchedRomWithOldHeader.fileName=patchedRom.fileName; patchedRomWithOldHeader.fileType=patchedRom.fileType; patchedRom=patchedRomWithOldHeader; }else if(el('checkbox-addheader').checked){ patchedRom=patchedRom.slice(headerSize); } } /* fix checksum if needed */ if(AppSettings.fixChecksum){ var checksumInfo=_getHeaderChecksumInfo(patchedRom); if(checksumInfo && checksumInfo.current!==checksumInfo.calculated && confirm(_('fix_checksum_prompt')+' ('+padZeroes(checksumInfo.current)+' -> '+padZeroes(checksumInfo.calculated)+')')){ checksumInfo.fix(patchedRom); } } setMessage('apply'); patchedRom.save(); //debug: create unheadered patch /*if(headerSize && el('checkbox-addheader').checked){ createPatch(romFile, patchedRom); }*/ } /*function removeHeader(romFile){ //r._dataView=new DataView(r._dataView.buffer, headerSize); oldHeader=romFile.slice(0,headerSize); r=r.slice(headerSize); }*/ function applyPatch(p,r,validateChecksums){ if(p && r){ if(headerSize){ if(el('checkbox-removeheader').checked){ //r._dataView=new DataView(r._dataView.buffer, headerSize); oldHeader=r.slice(0,headerSize); r=r.slice(headerSize); }else if(el('checkbox-addheader').checked){ var romWithFakeHeader=new MarcFile(headerSize+r.fileSize); romWithFakeHeader.fileName=r.fileName; romWithFakeHeader.fileType=r.fileType; r.copyToFile(romWithFakeHeader, 0, r.fileSize, headerSize); //add FDS header if(/\.fds$/.test(r.FileName) && r.fileSize%65500===0){ //romWithFakeHeader.seek(0); romWithFakeHeader.writeBytes([0x46, 0x44, 0x53, 0x1a, r.fileSize/65500]); } r=romWithFakeHeader; } } if(CAN_USE_WEB_WORKERS){ setMessage('apply', 'applying_patch', 'loading'); setTabApplyEnabled(false); webWorkerApply.postMessage( { romFileU8Array:r._u8array, patchFileU8Array:patchFile._u8array, validateChecksums:validateChecksums },[ r._u8array.buffer, patchFile._u8array.buffer ] ); }else{ setMessage('apply', 'applying_patch', 'loading'); try{ p.apply(r, validateChecksums); preparePatchedRom(r, p.apply(r, validateChecksums), headerSize); }catch(e){ setMessage('apply', 'Error: '+_(e.message), 'error'); } } }else{ setMessage('apply', 'No ROM/patch selected', 'error'); } } function createPatch(sourceFile, modifiedFile, mode){ if(!sourceFile){ setMessage('create', 'No source ROM file specified.', 'error'); return false; }else if(!modifiedFile){ setMessage('create', 'No modified ROM file specified.', 'error'); return false; } if(CAN_USE_WEB_WORKERS){ setTabCreateEnabled(false); setMessage('create', 'creating_patch', 'loading'); webWorkerCreate.postMessage( { sourceFileU8Array:sourceFile._u8array, modifiedFileU8Array:modifiedFile._u8array, modifiedFileName:modifiedFile.fileName, patchMode:mode },[ sourceFile._u8array.buffer, modifiedFile._u8array.buffer ] ); romFile1=new MarcFile(el('input-file-rom1')); romFile2=new MarcFile(el('input-file-rom2')); }else{ try{ sourceFile.seek(0); modifiedFile.seek(0); var newPatch; if(mode==='ips'){ newPatch=createIPSFromFiles(sourceFile, modifiedFile); }else if(mode==='bps'){ newPatch=createBPSFromFiles(sourceFile, modifiedFile, (sourceFile.fileSize<=4194304)); }else if(mode==='ups'){ newPatch=createUPSFromFiles(sourceFile, modifiedFile); }else if(mode==='aps'){ newPatch=createAPSFromFiles(sourceFile, modifiedFile); }else if(mode==='rup'){ newPatch=createRUPFromFiles(sourceFile, modifiedFile); }else{ setMessage('create', 'error_invalid_patch', 'error'); } if(crc32(modifiedFile)===crc32(newPatch.apply(sourceFile))){ newPatch.export(modifiedFile.fileName.replace(/\.[^\.]+$/,'')).save(); }else{ setMessage('create', 'Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.', 'error'); } }catch(e){ setMessage('create', 'Error: '+_(e.message), 'error'); } } } /* GUI functions */ function setMessage(tab, key, className){ var messageBox=el('message-'+tab); if(key){ messageBox.setAttribute('data-localize',key); if(className==='loading'){ messageBox.className='message'; messageBox.innerHTML=' '+_(key); }else{ messageBox.className='message '+className; if(className==='warning') messageBox.innerHTML='⚠ '+_(key); else if(className==='error') messageBox.innerHTML='✗ '+_(key); else messageBox.innerHTML=_(key); } messageBox.style.display='inline'; }else{ messageBox.style.display='none'; } } function setElementEnabled(element,status){ if(status){ el(element).className='enabled'; }else{ el(element).className='disabled'; } el(element).disabled=!status; } function setTabCreateEnabled(status){ if( (romFile1 && romFile1.fileSize>TOO_BIG_ROM_SIZE) || (romFile2 && romFile2.fileSize>TOO_BIG_ROM_SIZE) ){ setMessage('create',_('warning_too_big'),'warning'); } setElementEnabled('input-file-rom1', status); setElementEnabled('input-file-rom2', status); setElementEnabled('select-patch-type', status); if(romFile1 && romFile2 && status){ setElementEnabled('button-create', status); }else{ setElementEnabled('button-create', false); } } function setTabApplyEnabled(status){ setElementEnabled('input-file-rom', status); setElementEnabled('input-file-patch', status); if(romFile && status && (patch || isCustomPatcherEnabled())){ setElementEnabled('button-apply', status); }else{ setElementEnabled('button-apply', false); } } function setCreatorMode(creatorMode){ if(creatorMode){ el('tab0').style.display='none'; el('tab1').style.display='block'; el('switch-create').className='switch enabled' }else{ el('tab0').style.display='block'; el('tab1').style.display='none'; el('switch-create').className='switch disabled' } } /* FileSaver.js (source: http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js) * A saveAs() FileSaver implementation. * 1.3.8 * 2018-03-22 14:03:47 * * By Eli Grey, https://eligrey.com * License: MIT * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md */ var saveAs=saveAs||function(c){"use strict";if(!(void 0===c||"undefined"!=typeof navigator&&/MSIE [1-9]\./.test(navigator.userAgent))){var t=c.document,f=function(){return c.URL||c.webkitURL||c},s=t.createElementNS("http://www.w3.org/1999/xhtml","a"),d="download"in s,u=/constructor/i.test(c.HTMLElement)||c.safari,l=/CriOS\/[\d]+/.test(navigator.userAgent),p=c.setImmediate||c.setTimeout,v=function(t){p(function(){throw t},0)},w=function(t){setTimeout(function(){"string"==typeof t?f().revokeObjectURL(t):t.remove()},4e4)},m=function(t){return/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(t.type)?new Blob([String.fromCharCode(65279),t],{type:t.type}):t},r=function(t,n,e){e||(t=m(t));var r,o=this,a="application/octet-stream"===t.type,i=function(){!function(t,e,n){for(var r=(e=[].concat(e)).length;r--;){var o=t["on"+e[r]];if("function"==typeof o)try{o.call(t,n||t)}catch(t){v(t)}}}(o,"writestart progress write writeend".split(" "))};if(o.readyState=o.INIT,d)return r=f().createObjectURL(t),void p(function(){var t,e;s.href=r,s.download=n,t=s,e=new MouseEvent("click"),t.dispatchEvent(e),i(),w(r),o.readyState=o.DONE},0);!function(){if((l||a&&u)&&c.FileReader){var e=new FileReader;return e.onloadend=function(){var t=l?e.result:e.result.replace(/^data:[^;]*;/,"data:attachment/file;");c.open(t,"_blank")||(c.location.href=t),t=void 0,o.readyState=o.DONE,i()},e.readAsDataURL(t),o.readyState=o.INIT}r||(r=f().createObjectURL(t)),a?c.location.href=r:c.open(r,"_blank")||(c.location.href=r);o.readyState=o.DONE,i(),w(r)}()},e=r.prototype;return"undefined"!=typeof navigator&&navigator.msSaveOrOpenBlob?function(t,e,n){return e=e||t.name||"download",n||(t=m(t)),navigator.msSaveOrOpenBlob(t,e)}:(e.abort=function(){},e.readyState=e.INIT=0,e.WRITING=1,e.DONE=2,e.error=e.onwritestart=e.onprogress=e.onwrite=e.onabort=e.onerror=e.onwriteend=null,function(t,e,n){return new r(t,e||t.name||"download",n)})}}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||this);