/* savegame-editor.js v20230729 A library that lets you create easily a savegame editor. Made with vanilla JS. by Marc Robledo 2016-2023 http://www.marcrobledo.com/license */ /* LIBRARIES */ /* MODDED VERSION OF MarcFile.js v20181020 - Marc Robledo 2014-2018 - http://www.marcrobledo.com/license */ function MarcFile(a,b){"object"==typeof a&&a.files&&(a=a.files[0]);var c=!1;if("object"==typeof a&&a.name&&a.size){if("function"!=typeof window.FileReader)throw new Error("Incompatible Browser");c=!0,this.fileName=a.name,this.fileType=a.type,this.fileSize=a.size}else if("number"==typeof a)this.fileName="file.bin",this.fileType="application/octet-stream",this.fileSize=a;else throw new Error("Invalid source");if(this.littleEndian=!1,c)this._fileReader=new FileReader,this._fileReader.marcFile=this,this._fileReader.addEventListener("load",function(){this.marcFile._u8array=new Uint8Array(this.result),this.marcFile._dataView=new DataView(this.result),b&&b.call()},!1),this._fileReader.readAsArrayBuffer(a);else if(0>>0:(this._u8array[a]<<8)+this._u8array[a+1]>>>0},MarcFile.prototype.readU24=function(a){return this.littleEndian?this._u8array[a]+(this._u8array[a+1]<<8)+(this._u8array[a+2]<<16)>>>0:(this._u8array[a]<<16)+(this._u8array[a+1]<<8)+this._u8array[a+2]>>>0},MarcFile.prototype.readU32=function(a){return this.littleEndian?this._u8array[a]+(this._u8array[a+1]<<8)+(this._u8array[a+2]<<16)+(this._u8array[a+3]<<24)>>>0:(this._u8array[a]<<24)+(this._u8array[a+1]<<16)+(this._u8array[a+2]<<8)+this._u8array[a+3]>>>0},MarcFile.prototype.readS8=function(a){return this._dataView.getInt8(a,this.littleEndian)},MarcFile.prototype.readS16=function(a){return this._dataView.getInt16(a,this.littleEndian)},MarcFile.prototype.readS32=function(a){return this._dataView.getInt32(a,this.littleEndian)},MarcFile.prototype.readF32=function(a){return this._dataView.getFloat32(a,this.littleEndian)},MarcFile.prototype.readF64=function(a){return this._dataView.getFloat64(a,this.littleEndian)},MarcFile.prototype.readBytes=function(a,b){for(var c=Array(b),d=0;d>8):(this._u8array[a]=b>>8,this._u8array[a+1]=255&b)},MarcFile.prototype.writeU24=function(a,b){this.littleEndian?(this._u8array[a]=255&b,this._u8array[a+1]=(65280&b)>>8,this._u8array[a+2]=(16711680&b)>>16):(this._u8array[a]=(16711680&b)>>16,this._u8array[a+1]=(65280&b)>>8,this._u8array[a+2]=255&b)},MarcFile.prototype.writeU32=function(a,b){this.littleEndian?(this._u8array[a]=255&b,this._u8array[a+1]=(65280&b)>>8,this._u8array[a+2]=(16711680&b)>>16,this._u8array[a+3]=(4278190080&b)>>24):(this._u8array[a]=(4278190080&b)>>24,this._u8array[a+1]=(16711680&b)>>16,this._u8array[a+2]=(65280&b)>>8,this._u8array[a+3]=255&b)},MarcFile.prototype.writeS8=function(a,b){this._dataView.setInt8(a,b,this.littleEndian)},MarcFile.prototype.writeS16=function(a,b){this._dataView.setInt16(a,b,this.littleEndian)},MarcFile.prototype.writeS32=function(a,b){this._dataView.setInt32(a,b,this.littleEndian)},MarcFile.prototype.writeF32=function(a,b){this._dataView.setFloat32(a,b,this.littleEndian)},MarcFile.prototype.writeF64=function(a,b){this._dataView.setFloat64(a,b,this.littleEndian)},MarcFile.prototype.writeBytes=function(b,c){for(var a=0;a { try { var marcFile = new MarcFile(file, function() { resolve(marcFile); }); } catch (err) { reject(err); } }); return ret; } /* FileSaver.js by eligrey - https://github.com/eligrey/FileSaver.js */ 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); /* MarcDialogs.js v2016 */ MarcDialogs=function(){function e(e,t,n){a?e.attachEvent("on"+t,n):e.addEventListener(t,n,!1)}function t(){s&&(o?history.go(-1):(c.className="dialog-overlay",s.className=s.className.replace(/ active/g,""),s=null))}function n(e){for(var t=0;t 0) { let entry = queue.shift(); if (entry.isFile) { fileEntries.push(entry); } else if (entry.isDirectory) { let reader = entry.createReader(); queue.push(...await readAllDirectoryEntries(reader)); } } return await Promise.all(fileEntries.map(entry => toFilePromise(entry))); } // Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array async function readAllDirectoryEntries(directoryReader) { let entries = []; let readEntries = await readEntriesPromise(directoryReader); while (readEntries.length > 0) { entries.push(...readEntries); readEntries = await readEntriesPromise(directoryReader); } return entries; } // Wrap readEntries in a promise to make working with readEntries easier async function readEntriesPromise(directoryReader) { try { return await new Promise((resolve, reject) => { directoryReader.readEntries(resolve, reject); }); } catch (err) { console.log(err); } } async function toFilePromise(fileEntry) { try { return await new Promise((resolve, reject) => { fileEntry.file(function(file){ // Patch webkitRelativePath for Chrome which doesn't always set it? https://github.com/ant-design/ant-design/issues/16426 // TODO could we just pass fileEntry.fullpath out instead of patching this? Object.defineProperties(file, { webkitRelativePath: { writable: true, }, }); file.webkitRelativePath = file.webkitRelativePath || fileEntry.fullPath.replace(/^\//, ''); Object.defineProperties(file, { webkitRelativePath: { writable: false, }, }); resolve(file); }, reject); }); } catch (err) { console.log(err); } } function removeClass(){document.body.className=document.body.className.replace(' dragging-files','')} addEvent(document,'dragenter',function(e){ if(checkIfHasFiles(e)){ no(e); document.body.className+=' dragging-files' } }); addEvent(document,'dragexit',function(e){ //alert('exit'); no(e); removeClass(); /* why!? */ removeClass(); removeClass(); removeClass(); }); addEvent(document,'dragover',function(e){ if(checkIfHasFiles(e)) no(e) }); var dropOutside=false; function enableDropOutside(){ addEvent(document,'drop',function(e){ removeClass(); no(e); }); } return{ add:function(z,f){ if(!dropOutside){ enableDropOutside(); } addEvent(document.getElementById(z),'drop',async function (e) { var files = await getAllFiles(e.dataTransfer.items); if(files.length==0) { return false; } no(e); removeClass(); f(files); }); }, addGlobalZone:function(f,t){ if(!dropOutside){ enableDropOutside(); } var div=document.createElement('div'); div.id='drop-overlay'; div.className='marc-drop-files'; var span=document.createElement('span'); if(t) span.innerHTML=t; else span.innerHTML='Drop files here'; div.appendChild(span); document.body.appendChild(div); this.add('#drop-overlay',f); } } }()); /* savegame load/save */ var tempFile,hasBeenLoaded=false; function _tempFileLoadFunction(){ if(SavegameEditor.checkValidSavegame()){ hide('dragzone'); if(SavegameEditor.preload && !hasBeenLoaded){ SavegameEditor.preload(); hasBeenLoaded=true; } SavegameEditor.load(); show('the-editor'); show('toolbar'); }else{ MarcDialogs.alert('Invalid savegame file'); } } function saveChanges(){ if(decodeURIComponent(document.cookie).indexOf('hideWarningMessage=1')>=0 || location.protocol==='file:'){ /* chrome does not write cookies in local, so skip warning message in that case */ SavegameEditor.save(); tempFile.save(); }else{ MarcDialogs.open('warning'); } } function closeFileConfirm(){ MarcDialogs.confirm('All changes will be lost.', function(){ closeFile(); MarcDialogs.close() }); } function closeFile(){ show('dragzone'); hide('the-editor'); hide('toolbar'); if(typeof SavegameEditor.unload==='function') SavegameEditor.unload(); } function getSavegameDefaultName(){ if(typeof SavegameEditor.Filename==='string') return SavegameEditor.Filename; return SavegameEditor.Filename[0] } function getSavegameAllNames(){ if(typeof SavegameEditor.Filename==='string') return SavegameEditor.Filename; else{ var s=''; for(var i=0; i Browse '+getSavegameAllNames()+' or drop it here'; var inputFile=document.createElement('input'); inputFile.type='file'; inputFile.className='hidden'; inputFile.id='file-load'; // Requires a folder for "browse window" picking, but this works when running webpage from filesystem on Chrome, where dropping folders does not work. // `webkitGetAsEntry` may be a better workaround https://github.com/danialfarid/ng-file-upload/issues/236#issuecomment-45053629 // inputFile.webkitdirectory = true; inputFile.addEventListener('change', async function(evt){ if(this.files.length == 1 || typeof SavegameEditor.showSavegameIndex === 'undefined') { // Load savegame from file tempFile=new MarcFile(this.files[0], _tempFileLoadFunction); } else { // Some games have a complex structure of multiple savegames, so we provide a custom picker+overview await SavegameEditor.showSavegameIndex(this.files); } }, false); dragZone.appendChild(dragMessage); dragZone.appendChild(inputFile); document.body.appendChild(dragZone); if(!SavegameEditor.noDemo){ var demoMessage=document.createElement('button'); demoMessage.id='demo'; demoMessage.innerHTML='Do you want to try it out? Try an example savegame'; demoMessage.addEventListener('click', function(){ var filename=getSavegameDefaultName(); if(typeof window.fetch==='function'){ fetch(filename) .then(res => res.arrayBuffer()) // Gets the response and returns it as a blob .then(ab => { tempFile=new MarcFile(ab.byteLength); tempFile.fileName=filename; tempFile._u8array=new Uint8Array(ab); tempFile._dataView=new DataView(ab); _tempFileLoadFunction(); }) .catch(function(){ alert('Unexpected error: can\'t download example savegame'); }); }else{ var oReq=new XMLHttpRequest(); oReq.open('GET', filename, true); oReq.responseType='arraybuffer'; oReq.onload=function(oEvent){ if(this.status===200) { var ab=oReq.response; //Note: not oReq.responseText tempFile=new MarcFile(ab.byteLength); tempFile.fileName=filename; tempFile._u8array=new Uint8Array(ab); tempFile._dataView=new DataView(ab); _tempFileLoadFunction(); }else{ alert('Unexpected error: can\'t download example savegame'); } }; oReq.onerror=function(oEvent){ alert('Unexpected error: can\'t download example savegame'); }; oReq.send(null); } }, false); dragZone.appendChild(demoMessage); } MarcDragAndDrop.add('dragzone', async function(droppedFiles){ if(droppedFiles.length == 1 || typeof SavegameEditor.showSavegameIndex === 'undefined') { // Load savegame from file tempFile=new MarcFile(droppedFiles[0], _tempFileLoadFunction); } else { // Some games have a complex structure of multiple savegames, so we provide a custom picker+overview await SavegameEditor.showSavegameIndex(droppedFiles); } }); var warningDialog=document.createElement('div'); warningDialog.className='dialog'; warningDialog.id='dialog-warning'; warningDialog.innerHTML='Use this tool at your own risk. By using it, you are responsible of any data lost.'; var divButtons=document.createElement('div'); divButtons.className='buttons'; var understandButton=document.createElement('button'); understandButton.innerHTML='I understand'; understandButton.addEventListener('click',function(){ var EXPIRE_DAYS=3; var d=new Date(); d.setTime(d.getTime()+(EXPIRE_DAYS*24*60*60*1000)); document.cookie="hideWarningMessage=1;expires="+d.toUTCString();//+";path=./"; MarcDialogs.close(); saveChanges(); }, false); divButtons.appendChild(understandButton); warningDialog.appendChild(divButtons); document.body.appendChild(warningDialog); }, false); /* binary and other helpers */ function compareBytes(offset,a2){ var a1=tempFile.readBytes(offset, a2.length); for(var i=0;i field.maxValue){ val=field.maxValue; } field.value=val; } function fixNumericFieldValueFromEvent(){fixNumericFieldValue(this)} function inputNumber(id,min,max,def){ var input=document.createElement('input'); input.id='number-'+id; input.className='full-width text-right'; input.type='text'; /* type='number' validation breaks getting input value when it's not valid */ input.minValue=min; input.maxValue=max; input.value=def; input.addEventListener('change', fixNumericFieldValueFromEvent, false); return input; } function inputFloat(id,min,max,def){ var input=document.createElement('input'); input.id='float-'+id; input.className='full-width text-right'; input.type='text'; input.minValue=min; input.maxValue=max; input.value=def; input.addEventListener('change', fixNumericFieldValueFromEvent, false); return input } function input(id,def){ var input=document.createElement('input'); input.id='input-'+id; input.className='full-width'; input.type='text'; input.value=def; return input } function checkbox(id,val){ var input=document.createElement('input'); input.id='checkbox-'+id; input.type='checkbox'; if(val) input.value=val; return input } function select(id,options,func,def){ var select; if(document.getElementById('select-'+id)){ select=document.getElementById('select-'+id); }else{ select=document.createElement('select'); select.id='select-'+id; select.className='full-width'; } var unknownValue=typeof def!=='undefined'; if(options){ for(var i=0; i