var AUDIOBUFFSIZE = 1024; const SaveTypes = { Savestate: "savestate", Disk: "disk", ISO: "iso", BaseImage: "baseimage", } class MyClass { constructor() { this.rom_name = ''; this.rom_size = 0; this.mobileMode = false; this.iosMode = false; this.base_name = ''; this.initCount = 0; this.baseImageSaved = false; this.isoSaved = false; this.moduleInitializing = true; this.exportFilesRequested = false; this.lblError = ''; this.isoMounted = false; this.floppyMounted = false; this.canvasHeight = 480; this.ram = 32; this.initialHardDrive = 'hd_520'; this.dosVersion = '7.1'; this.iso_loaded = false; this.noIso = false; this.importedFileNames = []; this.isSpecialHandler = false; this.img_loaded = false; this.cueFile = ''; this.hasBinCue = false; this.beforeEmulatorStarted = true; this.audioInited = false; this.dblistSavestates = []; this.dblistDisks = []; this.dblistBaseImages = []; this.dblistIsos = []; this.multiFiles = []; this.multiFileMode = false; this.singleFileUpload = false; this.noLocalSave = true; this.message = ''; this.loading = true; this.isoName = ''; this.loginModalOpened = false; this.noCloudSave = true; this.password = ''; this.loggedIn = false; this.dosSaveStates = []; this.allSaveStates = []; this.baseHardDrive = new Uint8Array(); this.compareCount = 0; this.doIntegrityCheck = false; this.cpu = 'auto'; this.showLoadAndSavestate = false; this.loadSavestateAfterBoot = false; this.noCopyImport = false; this.changeCD = false; this.changeFloppy = false; this.loadFloppy = false; this.isDosMode = true; this.autoKeyboard = false; this.autoKeyboardTimer = 0; this.autoKeyboardInterval = 48*180; //three minutes (audioprocessrecurring gets called 48 times a second) this.lastCalledTime = new Date(); this.fpscounter = 0; this.currentfps = 0; this.fpsInterval = 1000 / 60; this.then = Date.now(); this.hasCloud = false; this.initialInstallation = false; this.hardDiskFallbackFromFloppy = false; this.ranWindowsSetup = false; this.win95InstallationFix = false; this.winNotFoundCommands = ''; this.doswasmxBatFound = false; this.romList = []; this.settings = { CLOUDSAVEURL: "", ISOURL: "", DEFAULTIMG: "" }; this.specialFileHandlers = [ '.7z', '.zip', '.bin', '.cue', '.img', '.iso' ]; var Module = {}; Module['canvas'] = document.getElementById('canvas'); window['Module'] = Module; document.getElementById('file-upload').addEventListener('change', this.uploadRom.bind(this)); document.getElementById('file-import').addEventListener('change', this.importFiles.bind(this)); //comes from settings.js this.settings = window["DOSWASMSETTINGS"]; if (this.settings.CLOUDSAVEURL) { this.hasCloud = true; } if (window["ROMLIST"].length > 0) { window["ROMLIST"].forEach(rom => { this.romList.push(rom); }); } rivets.formatters.ev = function (value, arg) { return eval(value + arg); } rivets.formatters.ev_string = function (value, arg) { let eval_string = "'" + value + "'" + arg; return eval(eval_string); } rivets.bind(document.getElementById('maindiv'), { data: this }); rivets.bind(document.getElementById('importModal'), { data: this }); rivets.bind(document.getElementById('loginModal'), { data: this }); rivets.bind(document.getElementById('settingsModal'), { data: this }); rivets.bind(document.getElementById('divInstructions'), { data: this }); this.detectBrowser(); this.setupDragDropRom(); this.createDB(); this.retrieveSettings(); if (this.hasCloud) { this.setupLogin(); } $('#topPanel').show(); $('#errorOuter').show(); } detectBrowser(){ if (navigator.userAgent.toLocaleLowerCase().includes('iphone')) { this.iosMode = true; try { let iosVersion = navigator.userAgent.substring(navigator.userAgent.indexOf("iPhone OS ") + 10); iosVersion = iosVersion.substring(0, iosVersion.indexOf(' ')); iosVersion = iosVersion.substring(0, iosVersion.indexOf('_')); this.iosVersion = parseInt(iosVersion); } catch (err) { } } if (window.innerWidth < 600 || this.iosMode) this.mobileMode = true; else this.mobileMode = false; // firefox only supports 250 megs?? if (navigator.userAgent.toLocaleLowerCase().includes('firefox')) { this.initialHardDrive = 'hd_250'; } if (this.iosMode) { this.initialHardDrive = 'hd -size 25'; } if (this.mobileMode) { this.canvasHeight = window.innerWidth / 2; console.log('detected mobile mode - canvasheight: ' + this.canvasHeight) } } //DRAG AND DROP ROM setupDragDropRom(){ let dropArea = document.getElementById('dropArea'); dropArea.addEventListener('dragenter', this.preventDefaults, false); dropArea.addEventListener('dragover', this.preventDefaults, false); dropArea.addEventListener('dragleave', this.preventDefaults, false); dropArea.addEventListener('drop', this.preventDefaults, false); dropArea.addEventListener('dragenter', this.dragDropHighlight, false); dropArea.addEventListener('dragover', this.dragDropHighlight, false); dropArea.addEventListener('dragleave', this.dragDropUnHighlight, false); dropArea.addEventListener('drop', this.dragDropUnHighlight, false); dropArea.addEventListener('drop', this.handleDrop, false); } preventDefaults(e){ e.preventDefault(); e.stopPropagation(); } dragDropHighlight(e){ $('#dropArea').css({"background-color": "lightblue"}); } dragDropUnHighlight(e){ $('#dropArea').css({"background-color": "inherit"}); } handleDrop(e){ myClass.initAudio(); myClass.showProgress = true; let dt = e.dataTransfer; let files = dt.files; if (files.length == 1) { myClass.detectSingleFileUpload(files[0].name); } else if (files.length > 1) { myClass.handleMultipleFiles(files, 0); return; } var file = files[0]; myClass.rom_name = file.name; myClass.extractBaseName(); console.log(file); var reader = new FileReader(); reader.onprogress = function (event) { myClass.handleProgress(event, file); }; reader.onload = function (e) { console.log('finished loading'); var byteArray = new Uint8Array(this.result); myClass.LoadEmulator(byteArray); } reader.readAsArrayBuffer(file); } handleProgress(event, file){ console.log('loaded: ' + event.loaded); let loaded = event.loaded; let total = event.total; let percent = (loaded / total)*100; loaded = Math.ceil(loaded / 1000000); total = Math.ceil(total / 1000000); let formatted = file.name + ' ' + loaded + 'MB / ' + total + 'MB'; document.getElementById('myProgress').style.width= percent + '%'; document.getElementById('myProgress').innerHTML = formatted; } configureEmulator(){ if (this.password) this.loginSilent(); let size = localStorage.getItem('doswasmx-height'); if (size) { console.log('size found'); let sizeNum = parseInt(size); this.canvasHeight = sizeNum; } this.resizeCanvas(); $('#canvasDiv').show(); $('#divInstructions').show(); } processPrintStatement(text) { console.log(text); //they tried to load an .img file that turned out to be a floppy disk if (text.includes('detected floppy disk')) { if (this.dblistDisks.length == 0 && !this.settings.DEFAULTIMG) { //this means they don't have a hard disk myClass.base_name = 'mydisk'; myClass.initialInstallation = true; } else { //fall back to using their hard drive myClass.base_name = 'mydisk'; myClass.hardDiskFallbackFromFloppy = true; } } //we detected a floppy disk if (text.includes('floppy disk mounted')) { setTimeout(() => { if (myClass.initialInstallation) { myClass.sendDosCommands( 'imgmake \"' + this.base_name + ".img\" -t " + this.initialHardDrive + "\n" + 'imgmount c \"' + this.base_name + ".img\na:\n"); } else if (myClass.hardDiskFallbackFromFloppy) { //if they already have a hard disk we load it //currently does not support this.settings.DEFAULTIMG + dragging .img floppy if (this.dblistDisks.length > 0) { this.loadFromDatabase(SaveTypes.Disk); } } else { myClass.sendDosCommands("a:\n"); } myClass.floppyMounted = true; }, //TODO this is a hack //dos commands should queue up rather //than overwrite eachother 500); } //this means we detected the windows cd if (text.includes("iso mounted root file: WIN98") || text.includes("iso mounted root file: WIN95")) { //auto start the setup process - only do this once if (!myClass.ranWindowsSetup) { myClass.ranWindowsSetup = true; setTimeout(() => { myClass.initialInstallation = true; myClass.sendDosCommands("d:setup.exe\n"); }, 50); //set cpu to max during windows installation setTimeout(() => { myClass.updateCpuNeil('cycles=max'); }, 100); } } if (text.includes('windows not found') || text.includes('found noboot.txt')) { //if we don't detect a windows installation just send //them to the C drive setTimeout(() => { let dosCommands = "c:\n"; //if we found a DOSWASMX.BAT we run it if (myClass.doswasmxBatFound) { dosCommands += 'doswasmx.bat\n' } //add any additional commands appended based on the rom file dosCommands += myClass.winNotFoundCommands; //send it to the dos shell myClass.sendDosCommands(dosCommands); //clear it for next time myClass.winNotFoundCommands = ''; }, 50); } if (text.includes('Parsing command line: d:setup.exe')) { //a bunch of hacks to get it to dismiss the install //warnings for win95rtm, win95osr2, and win98se if (myClass.initialInstallation) { setTimeout(() => { myClass.sendKey(52); //enter }, 1000); setTimeout(() => { myClass.sendKey(49); //escape }, 3000); setTimeout(() => { myClass.sendKey(52); //enter }, 3100); } } if (text.includes('Plug & Play OS reports itself inactive')) { //this is hack during windows 95 installation //where it doesnt detect one of the restarts if (myClass.initialInstallation && !myClass.win95InstallationFix) { console.log('windows95 fix'); myClass.win95InstallationFix = true; setTimeout(() => { myClass.updateAutoexecAdditional("boot c:\r\n"); // myClass.saveDrive(); }, 100); } } if (text.includes('drive mounted C file: DOSWASMX.BAT')) { myClass.doswasmxBatFound = true; } if (text.includes('x ==')) { if (text.includes('x == 2')) { //this means we are booting into windows myClass.isDosMode = false; } else { if (text.includes('x == 0')) { //this means we explicitly selected shutdown so go to DOS } else { //otherwise they probably picked restart //so send them back to windows setTimeout(() => { myClass.updateAutoexecAdditional("boot c:\r\n"); }, 100); } //save the hard disk every time we restart/shutdown if (!myClass.loggedIn) { setTimeout(() => { myClass.saveDrive(); }, 100); } //we are back to the dos shell myClass.isoMounted = false; myClass.floppyMounted = false; myClass.isDosMode = true; } } if (text.includes('iso drive mounted')) { //we mounted a cd myClass.isoMounted = true; } //emulator has started event if (text.includes('DEBUG_ShowMsg: pixratio 1.000') && myClass.loadSavestateAfterBoot) { console.log('detected windows started'); myClass.loadSavestateAfterBoot = false; if (myClass.loggedIn && !myClass.noCloudSave) { //we give it a 5 second delay because we //want to wait for the windows startup sound setTimeout(() => { myClass.loadCloud(); }, 5000); } } //this means its done exporting if (text.includes('echo DONE')) { if (this.exportFilesRequested) { this.exportFilesRequested = false; setTimeout(() => { let filearray = FS.readFile("/export.zip"); var file = new File([filearray], "export.zip", {type: "text/plain; charset=x-user-defined"}); saveAs(file); Module._neil_clear_autoexec(); }, 500); } } //this means its done importing if (text.includes('echo Import Finished')) { setTimeout(() => { Module._neil_clear_autoexec(); }, 500); } } async initModule(){ myClass.initCount++; myClass.finishInitialization(); console.log('module initialized'); } //need to wait for both indexedDB and wasm runtime finishInitialization() { if (myClass.initCount == 2) { myClass.moduleInitializing = false; myClass.message = ''; //create some directories we will need FS.mkdir('/uploaded'); FS.mkdir('/res'); FS.mkdir('/save'); $('#githubDiv').show(); this.loading = false; } } uploadBrowse() { this.initAudio(); document.getElementById('file-upload').click(); } importBrowse() { document.getElementById('file-import').click(); } detectSingleFileUpload(fileName) { let fileExtension = fileName.substr(fileName.lastIndexOf('.')).toLocaleLowerCase(); if (!this.specialFileHandlers.includes(fileExtension)) { myClass.singleFileUpload = true; } } uploadRom(event) { myClass.initAudio(); myClass.showProgress = true; if (event.currentTarget.files.length == 1) { myClass.detectSingleFileUpload(event.currentTarget.files[0].name); } else if (event.currentTarget.files.length > 1) { myClass.handleMultipleFiles(event.currentTarget.files, 0); return; } var file = event.currentTarget.files[0]; myClass.rom_name = file.name; myClass.extractBaseName(); console.log(file); var reader = new FileReader(); reader.onprogress = function (event) { myClass.handleProgress(event, file); }; reader.onload = function (e) { console.log('finished loading'); var byteArray = new Uint8Array(this.result); myClass.LoadEmulator(byteArray); } reader.readAsArrayBuffer(file); } async parseMultipleFiles() { console.log('parseMultipleFiles', this.multiFiles); this.multiFileMode = true; //set some baseline default this.rom_name = 'blank.txt'; let firstBytes = new Uint8Array(5); this.extractBaseName(); for(let i = 0; i < this.multiFiles.length; i++) { let file = this.multiFiles[i]; if (file.name.toLocaleLowerCase().endsWith('img')) { //we prioritize the img name as the rom_name //because we want to be sure it uses this as the //hard drive when it gets to the LoadEmulator stage this.rom_name = file.name; this.extractBaseName(); this.baseHardDrive = file.data; let finalByteArray = await this.loadHardDriveDiffs(file.data); FS.writeFile('/' + this.base_name + '.img',finalByteArray); this.img_loaded = true; } else if ( file.name.toLocaleLowerCase().endsWith('iso') || file.name.toLocaleLowerCase().endsWith('.cue')) { FS.writeFile('/' + file.name,file.data); this.isoName = file.name; if (file.name.toLocaleLowerCase().endsWith('.cue')) { this.hasBinCue = true; this.cueFile = file.name; } //if we didn't find an img then use this as the rom_name if (!this.rom_name) { this.rom_name = file.name; this.extractBaseName(); } } else { //except bin/cue files if (file.name.toLocaleLowerCase().endsWith('.bin') ) { //will handle these manually FS.writeFile('/' + file.name,file.data); } else { //put them in the uploaded folder FS.writeFile('/uploaded/' + file.name,file.data); } } } //FREE THE MEMORY this.multiFiles = null; //we want to avoid setting the iso bytes because they were set above this.noIso = true; this.LoadEmulator(firstBytes); } handleMultipleFiles(files, index) { var file = files[index]; console.log('processing file ' + (index+1) + ' of ' + files.length, file); var reader = new FileReader(); reader.onprogress = function (event) { console.log('loaded: ' + event.loaded); let loaded = event.loaded; let total = event.total; let percent = (loaded / total)*100; loaded = Math.ceil(loaded / 1000000); total = Math.ceil(total / 1000000); let formatted = '(' + (index+1) + ' of ' + files.length + ') ' + file.name + ' ' + loaded + 'MB / ' + total + 'MB'; document.getElementById('myProgress').style.width= percent + '%'; document.getElementById('myProgress').innerHTML = formatted; }; reader.onload = function (e) { var byteArray = new Uint8Array(this.result); myClass.multiFiles.push( { name: file.name, data: byteArray } ) if ( (index+1) 6) { name = name.substr(0,6); } else if (name.length<3) // as long as its atleast 3 long we leave it { //fill in the gaps with random numbers var rando = Math.floor(Math.random() * Math.floor(100000)); name += rando; if (name.length > 6) name = name.substr(0,6); } return name; } readRomProp(key){ let myselect = document.getElementById('romselect'); try { return myselect.options[myselect.selectedIndex].attributes[key].value; } catch(err) { return ''; } } loadRomAndSavestate(){ this.loadSavestateAfterBoot = true; this.loadRom(); } extractRomName(name){ if (name.includes('/')) { name = name.substr(name.lastIndexOf('/')+1); } return name; } async loadRom(noIso) { this.initAudio(); if (noIso) { this.noIso = true; this.LoadEmulator(); } else { let romurl = this.readRomProp("value"); let ram = this.readRomProp("ram"); this.rom_name = this.extractRomName(romurl); if (ram) { this.ram = ram; } if (this.settings.ISOURL) { romurl = this.settings.ISOURL + romurl; } this.extractBaseName(); this.load_file(romurl); } } async initAudio() { if (!this.audioInited) { this.audioInited = true; this.audioContext = new AudioContext({ latencyHint: 'interactive', sampleRate: 48000, }); this.gainNode = this.audioContext.createGain(); this.gainNode.gain.value = 0.5; this.gainNode.connect(this.audioContext.destination); //point at where the emulator is storing the audio buffer this.audioBufferResampled = new Int16Array(Module.HEAP16.buffer,Module._neilGetSoundBufferResampledAddress(),64000); this.audioWritePosition = 0; this.audioReadPosition = 0; this.audioBackOffCounter = 0; this.pcmPlayer = this.audioContext.createScriptProcessor(AUDIOBUFFSIZE, 2, 2); this.pcmPlayer.onaudioprocess = this.AudioProcessRecurring.bind(this); this.pcmPlayer.connect(this.gainNode); } } countFPS(){ this.fpscounter++; let delta = (new Date().getTime() - this.lastCalledTime.getTime())/1000; if (delta>1) { this.currentfps = this.fpscounter; this.fpscounter = 0; this.lastCalledTime = new Date(); console.log(this.currentfps); } } //this method keeps getting called when it needs more audio //data to play so we just keep streaming it from the emulator AudioProcessRecurring(audioProcessingEvent){ if (this.beforeEmulatorStarted) { return; } if (this.autoKeyboard) { this.tickAutoKeyboard(); } var sampleRate = audioProcessingEvent.outputBuffer.sampleRate; let outputBuffer = audioProcessingEvent.outputBuffer; let outputData1 = outputBuffer.getChannelData(0); let outputData2 = outputBuffer.getChannelData(1); this.audioWritePosition = Module._neilGetAudioWritePosition(); //the bytes are arranged L,R,L,R,etc.... for each speaker for (let sample = 0; sample < AUDIOBUFFSIZE; sample++) { if (this.audioWritePosition != this.audioReadPosition) { outputData1[sample] = (this.audioBufferResampled[this.audioReadPosition] / 32768); outputData2[sample] = (this.audioBufferResampled[this.audioReadPosition + 1] / 32768); this.audioReadPosition += 2; //wrap back around within the ring buffer if (this.audioReadPosition == 64000) { this.audioReadPosition = 0; } } else { //if there's nothing to play then just play silence outputData1[sample] = 0; outputData2[sample] = 0; } } //calculate remaining audio in buffer let audioBufferRemaining = 0; let readPositionTemp = this.audioReadPosition; let writePositionTemp = this.audioWritePosition; for(let i = 0; i < 64000; i++) { if (readPositionTemp != writePositionTemp) { readPositionTemp += 2; audioBufferRemaining += 2; if (readPositionTemp == 64000) { readPositionTemp = 0; } } } } extractBaseName(){ try { this.base_name = this.rom_name.substr(0,this.rom_name.lastIndexOf('.')); } catch{ this.base_name = 'blank'; } } async load_file(path) { console.log('loading ' + path); myClass.load_url_request(path); } load_url_request(path){ //check cache let cleanPath = path.substr(path.lastIndexOf('/')+1); if (cleanPath.endsWith('.img')) { let baseImageName = cleanPath.replace(".img",".baseimage"); if (myClass.dblistBaseImages.includes(baseImageName)) { myClass.loadFromDatabase(SaveTypes.BaseImage); return; } } if (cleanPath.endsWith('.iso')) { if (myClass.dblistIsos.includes(cleanPath)) { myClass.loadFromDatabase(SaveTypes.ISO); return; } } this.showProgress = true; var req = new XMLHttpRequest(); req.open("GET", path); req.overrideMimeType("text/plain; charset=x-user-defined"); req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`); req.responseType = "arraybuffer"; req.onprogress = function (event) { let loaded = event.loaded; let total = event.total; let percent = (loaded / total)*100; loaded = Math.ceil(loaded / 1000000); total = Math.ceil(total / 1000000); let formatted = loaded + 'MB / ' + total + 'MB'; document.getElementById('myProgress').style.width= percent + '%'; document.getElementById('myProgress').innerHTML = formatted; }; req.onload = function (e) { console.log('request loaded',e,req); var arrayBuffer = req.response; // Note: not oReq.responseText try{ if (req.status==404) { console.log('request returned 404'); if (myClass.loggedIn) { myClass.load_file(myClass.settings.DEFAULTIMG); } } else if (arrayBuffer) { var byteArray = new Uint8Array(arrayBuffer); myClass.LoadEmulator(byteArray); } else{ this.lblError = 'Error downloading data. Try reloading browser.'; console.log('error downloading') console.log(req); } } catch(error){ console.log(error); toastr.error('Error Loading Save'); } }; req.send(); } newRom(){ location.reload(); } onError(message){ console.log('error triggered',event); if ( !message.includes('user has exited the lock') ) { this.lblError = message; } } //prevent dropdown from popping up from keyboard events dropdownKeyDown(e){ e.preventDefault(); e.stopPropagation(); } fullscreen() { let el = document.getElementById('canvasDiv'); if (el.webkitRequestFullScreen) { el.webkitRequestFullScreen(); } else { el.mozRequestFullScreen(); } } zoomIn(){ this.canvasHeight += 30; document.getElementById('canvasDiv').style.height = + this.canvasHeight + 'px' localStorage.setItem('doswasmx-height', this.canvasHeight.toString()); this.resizeCanvas(); console.log('zoom in'); } zoomOut(){ this.canvasHeight -= 30; localStorage.setItem('doswasmx-height', this.canvasHeight.toString()); this.resizeCanvas(); console.log('zoom out'); } resizeCanvas(){ document.getElementById('canvasDiv').style.height = this.canvasHeight + 'px'; } saveDrive() { let bytes = FS.readFile('/' + this.base_name + '.img'); //this is a Uint8Array this.saveToDatabase(bytes, SaveTypes.Disk); } readFromLocalStorage(localStorageName, name){ if (localStorage.getItem(localStorageName)) { if (localStorage.getItem(localStorageName)=="true") this[name] = true; else if (localStorage.getItem(localStorageName)=="false") this[name] = false; else this[name] = localStorage.getItem(localStorageName); } } writeToLocalStorage(localStorageName, name){ if (typeof(this[name]) == 'boolean') { if (this[name]) localStorage.setItem(localStorageName, 'true'); else localStorage.setItem(localStorageName, 'false'); } else { localStorage.setItem(localStorageName, this[name]); } } retrieveSettings(){ this.readFromLocalStorage('doswasmx-ram','ram'); this.readFromLocalStorage('doswasmx-initialhd','initialHardDrive'); this.readFromLocalStorage('doswasmx-dosversion','dosVersion'); } saveOptions(){ this.ram = this.ramTemp; this.initialHardDrive = this.initialHardDriveTemp; this.dosVersion = this.dosVersionTemp; this.writeToLocalStorage('doswasmx-ram','ram'); this.writeToLocalStorage('doswasmx-initialhd','initialHardDrive'); this.writeToLocalStorage('doswasmx-dosversion','dosVersion'); } createDB() { if (window["indexedDB"]==undefined){ console.log('indexedDB not available'); return; } var request = indexedDB.open('DOSWASMXDB'); request.onupgradeneeded = function (ev) { console.log('upgrade needed'); let db = ev.target.result; let objectStore = db.createObjectStore('DOSWASMXSTATES', { autoIncrement: true }); objectStore.transaction.oncomplete = function (event) { console.log('db created'); }; } request.onsuccess = function (ev) { var db = ev.target.result; var romStore = db.transaction("DOSWASMXSTATES", "readwrite").objectStore("DOSWASMXSTATES"); try { //rewrote using cursor instead of getAllKeys //for compatibility with MS EDGE romStore.openCursor().onsuccess = function (ev) { var cursor = ev.target.result; if (cursor) { let rom = cursor.key.toString(); if (rom.endsWith('.savestate')) { myClass.dblistSavestates.push(rom); } if (rom.endsWith('.disk')) { myClass.dblistDisks.push(rom); } if (rom.endsWith('.iso')) { myClass.dblistIsos.push(rom); } if (rom.endsWith('.baseimage')) { myClass.dblistBaseImages.push(rom); } cursor.continue(); } else { myClass.initCount++; myClass.finishInitialization(); } } } catch (error) { console.log('error reading keys'); console.log(error); } } } findSavestateInDatabase() { let imgKey = myClass.base_name; if (!myClass.loggedIn) imgKey = 'win95'; imgKey += + '.savestate'; myClass.dblistSavestates.forEach(save => { if (save == imgKey) { console.log('found savestate in indexedDB'); myClass.noLocalSave = false; } }); } /** * Description * @param {any} data * @param {SaveTypes} saveType * @returns {any} */ saveToDatabase(data, saveType) { if (!window["indexedDB"]==undefined){ console.log('indexedDB not available'); return; } console.log('save to database called: ', data.length); var request = indexedDB.open('DOSWASMXDB'); request.onsuccess = function (ev) { var db = ev.target.result; var transaction = db.transaction("DOSWASMXSTATES", "readwrite"); var romStore = transaction.objectStore("DOSWASMXSTATES"); let imgKey = myClass.base_name; if (!myClass.loggedIn) imgKey = 'win95'; if (saveType == SaveTypes.Savestate) { imgKey = imgKey + '.savestate'; } if (saveType == SaveTypes.Disk) { imgKey = imgKey + '.disk'; } if (saveType == SaveTypes.ISO) { imgKey = imgKey + '.iso'; } if (saveType == SaveTypes.BaseImage) { imgKey = imgKey + '.baseimage' } var addRequest = romStore.put(data, imgKey); addRequest.onsuccess = function (event) { console.log('data onsuccess'); //these take a long time so we want to let the user know if (saveType != SaveTypes.Savestate) { toastr.info('Please Wait...'); } }; addRequest.onerror = function (event) { toastr.error('Error Saving Data'); console.log('error adding data'); console.log(event); }; transaction.oncomplete = function(event) { console.log('transaction completed'); if (saveType == SaveTypes.Savestate) { myClass.showToast("State Saved") toastr.info('State Saved'); } if (saveType == SaveTypes.Disk) { myClass.showToast("Hard Drive Saved") toastr.info('Hard Drive Saved'); } if (saveType == SaveTypes.BaseImage) { myClass.showToast("Base Image Saved") toastr.info('Base Image Saved'); myClass.baseImageSaved = true; myClass.cacheIsoAndBaseImage(); } if (saveType == SaveTypes.ISO) { myClass.showToast("ISO Saved") toastr.info('ISO Saved'); myClass.isoSaved = true; myClass.cacheIsoAndBaseImage(); } } } } /** * Description * @param {SaveTypes} saveType * @returns {any} */ loadFromDatabase(saveType) { var request = indexedDB.open('DOSWASMXDB'); request.onsuccess = function (ev) { var db = ev.target.result; var romStore = db.transaction("DOSWASMXSTATES", "readwrite").objectStore("DOSWASMXSTATES"); let imgKey = myClass.base_name; if (!myClass.loggedIn) imgKey = 'win95'; if (saveType == SaveTypes.Savestate) { imgKey = imgKey + '.savestate'; } if (saveType == SaveTypes.Disk) { imgKey = imgKey + '.disk'; } if (saveType == SaveTypes.ISO) { imgKey = imgKey + '.iso'; } if (saveType == SaveTypes.BaseImage) { imgKey = imgKey + '.baseimage'; } var rom = romStore.get(imgKey); rom.onsuccess = function (event) { if (saveType == SaveTypes.Savestate) { let byteArray = rom.result; //Uint8Array FS.writeFile('/save/1.sav',byteArray); Module._neil_unserialize(); } if (saveType == SaveTypes.Disk) { if (myClass.hardDiskFallbackFromFloppy) { let byteArray = rom.result; //Uint8Array let imgName = '/' + myClass.base_name + '.img'; FS.writeFile(imgName,byteArray); myClass.sendDosCommands('imgmount c \"' + myClass.base_name + ".img\na:\n"); } else if (!myClass.loggedIn) { let byteArray = rom.result; //Uint8Array let imgName = '/' + myClass.base_name + '.img'; FS.writeFile(imgName,byteArray); console.log('loaded drive from db: ' + imgName); myClass.img_loaded = true; myClass.LoadEmulator(); } else { //TODO - if we are logged in then this is the //base image so we need to apply the diff drive } } if (saveType == SaveTypes.ISO || saveType == SaveTypes.BaseImage) { let byteArray = rom.result; //Uint8Array myClass.LoadEmulator(byteArray); } }; rom.onerror = function (event) { toastr.error('error getting rom from store'); } } request.onerror = function (ev) { toastr.error('error loading from db') } } clearHardDrive(){ let romToDelete = 'win95.disk'; if (!window["indexedDB"]==undefined){ console.log('indexedDB not available'); return; } var request = indexedDB.open('DOSWASMXDB'); request.onsuccess = function (ev) { var db = ev.target.result; var transaction = db.transaction("DOSWASMXSTATES", "readwrite"); let request = transaction.objectStore("DOSWASMXSTATES").delete(romToDelete); try { // report that the data item has been deleted transaction.oncomplete = function() { toastr.success('Hard Drive Deleted'); $('#settingsModal').modal('hide'); myClass.dblistDisks = []; }; } catch (error) { toastr.error('Error Deleting Disk'); console.log(error); } } } clearDatabase() { var request = indexedDB.deleteDatabase('DOSWASMXDB'); request.onerror = function (event) { console.log("Error deleting database."); toastr.error("Error deleting database"); }; request.onsuccess = function (event) { console.log("Database deleted successfully"); toastr.error("Database deleted successfully"); }; } async unzipFile(arrayBuffer){ const data = new Blob([ arrayBuffer ]) let file = new File([data], 'win95.zip'); document.getElementById('myProgress').innerHTML = 'Decompressing...'; let zipReader = new zip.ZipReader(new zip.BlobReader(file)); let entries = await zipReader.getEntries() let blob = await entries[0].getData(new zip.BlobWriter()); let byteArray = new Uint8Array(await blob.arrayBuffer()); document.getElementById('myProgress').innerHTML = 'Finished Decompressing'; myClass.LoadEmulator(byteArray); } toggleFPS(){ Module._neil_toggle_fps(); } exportModal(){ $("#exportModal").modal(); } settingsModal(){ this.ramTemp = this.ram; this.initialHardDriveTemp = this.initialHardDrive; this.dosVersionTemp = this.dosVersion; $("#settingsModal").modal(); } settingsSubmit(){ this.saveOptions(); $('#settingsModal').modal('hide'); toastr.info("Settings Saved"); } importModal(importType){ myClass.noCopyImport = false; myClass.changeCD = false; myClass.loadCD = false; myClass.changeFloppy = false; myClass.loadFloppy = false; if (importType == 'noCopy') { myClass.noCopyImport = true; } if (importType == 'changeCD') { myClass.changeCD = true; } if (importType == 'changeFloppy') { myClass.changeFloppy = true; } if (importType == 'loadFloppy') { myClass.loadFloppy = true; } if (importType == 'loadCD') { myClass.loadCD = true; } myClass.importStatus = ''; $("#importModal").modal(); } exportFiles(){ console.log('exportFiles'); $('#exportModal').modal('hide'); this.exportFilesRequested = true; Module._neil_export_files(); } saveStateLocal(){ console.log('saveStateLocal'); this.noLocalSave = false; Module._neil_serialize(); } loadStateLocal(){ console.log('loadStateLocal'); myClass.loadFromDatabase(SaveTypes.Savestate); } //when it returns from emscripten SaveStateEvent() { console.log('js savestate event'); let compressed = FS.readFile('/save/1.sav'); //this is a Uint8Array if (!myClass.loggedIn) { myClass.saveToDatabase(compressed, SaveTypes.Savestate); return; } var saveMessage = "Cloud State Saved"; var xhr = new XMLHttpRequest; xhr.open("POST", this.settings.CLOUDSAVEURL + "/SendStaveState?name=" + this.base_name + '.savestate.doswasmx' + "&password=" + this.password + "&emulator=doswasmx", true); xhr.send(compressed); xhr.onreadystatechange = function() { try{ if (xhr.readyState === 4) { let result = xhr.response; if (result=="\"Success\""){ myClass.noCloudSave = false; toastr.info(saveMessage); myClass.showToast(saveMessage); }else{ toastr.error('Error Saving Cloud Save'); } } } catch(error){ console.log(error); toastr.error('Error Loading Cloud Save'); } } } async loadHardDriveDiffs(byteArray){ await myClass.getSaveStates(); let promise = new Promise(function (resolve, reject) { let foundCloudDrive = false; for(let i = 0; i < myClass.allSaveStates.length; i++) { let element = myClass.allSaveStates[i]; if (element.Name==myClass.base_name + ".doswasmx") { foundCloudDrive = true; console.log('foundCloudDrive'); } } // we didnt find a cloud drive if (!foundCloudDrive) { resolve(byteArray); return; } toastr.info('Found Diff Drive'); var oReq = new XMLHttpRequest(); oReq.open("GET", myClass.settings.CLOUDSAVEURL + "/LoadStaveState?name=" + myClass.base_name + '.doswasmx' + "&password=" + myClass.password, true); oReq.responseType = "arraybuffer"; oReq.onload = function (oEvent) { var arrayBuffer = oReq.response; // Note: not oReq.responseText try{ if (arrayBuffer) { var byteArray = new Uint8Array(arrayBuffer); myClass.applyHardDriveDiffs(byteArray, resolve); } else{ reject(); } } catch(error){ console.log(error); reject(); } }; oReq.send(null); }); return promise; } async applyHardDriveDiffs(byteArrayDiffs, resolve){ console.log('applyHardDriveDiffs'); let pointer = 0; byteArrayDiffs = await this.decompressArrayBuffer(byteArrayDiffs.buffer); //start with a copy of the hold hard drive let newHardDrive = new Uint8Array(this.baseHardDrive); while(pointer < byteArrayDiffs.length) { let index = byteArrayDiffs[pointer] + (byteArrayDiffs[pointer+1]*256) + (byteArrayDiffs[pointer+2]*256*256) + (byteArrayDiffs[pointer+3]*256*256*256); pointer += 4; let length = byteArrayDiffs[pointer] + (byteArrayDiffs[pointer+1]*256) + (byteArrayDiffs[pointer+2]*256*256) + (byteArrayDiffs[pointer+3]*256*256*256); pointer += 4; //apply the diffs for (let i = 0; i < length; i++) { newHardDrive[index] = byteArrayDiffs[pointer]; pointer++; index++; } } resolve(newHardDrive); } async saveHardDriveDiffs(){ //pause dosbox Module._neil_toggle_pause(); this.message += 'Calculating Diffs...'; await new Promise(resolve => {setTimeout(resolve, 20); }); let compareHardDrive = new Uint8Array(); compareHardDrive = FS.readFile('/' + this.base_name + '.img'); //this is a Uint8Array let chunkSize = 10000; let arrayChunks = []; //array of Uint8SubArrays each of size chunk this.diffCount = 0; let progressCounter = 5000000; //we update progress every 5 million for (let i = 0; i < this.baseHardDrive.length; i++) { if (this.baseHardDrive[i] != compareHardDrive[i]) { let end = i + chunkSize; if (end >= this.baseHardDrive.length) { end = this.baseHardDrive.length-1; } let subArray = compareHardDrive.subarray(i,end); arrayChunks.push( { index: i, data: subArray }); i += chunkSize-1; this.diffCount++; } if (i > progressCounter) { let percent = Math.floor( (i / this.baseHardDrive.length)*100 ); this.message = "Diffs: " + this.diffCount + ", " + percent + "%"; await new Promise(resolve => {setTimeout(resolve, 20); }); progressCounter += 5000000; } } this.arrayChunks = arrayChunks; console.log(arrayChunks); let finalsize = 0; for(let i = 0; i < arrayChunks.length; i++) { //8 bytes for the two ints representing index and length finalsize += 8; let chunk = arrayChunks[i]; finalsize += chunk.data.length; } this.message = "Generating Final Array..."; await new Promise(resolve => {setTimeout(resolve, 20); }); let finalArray = new Uint8Array(finalsize); let pointer = 0; for(let i = 0; i < arrayChunks.length; i++) { let chunk = arrayChunks[i]; let index = chunk.index; // index (little endian) finalArray[pointer] = index & 0xFF; finalArray[pointer+1] = (index >> 8) & 0xFF; finalArray[pointer+2] = (index >> 16) & 0xFF; finalArray[pointer+3] = (index >> 24) & 0xFF; pointer += 4; let length = chunk.data.length; // length (little endian) finalArray[pointer] = length & 0xFF; finalArray[pointer+1] = (length >> 8) & 0xFF; finalArray[pointer+2] = (length >> 16) & 0xFF; finalArray[pointer+3] = (length >> 24) & 0xFF; pointer += 4; for(let j = 0; j < chunk.data.length; j++) { finalArray[pointer] = chunk.data[j] pointer++; } } //compress drive finalArray = await this.compressArrayBuffer(finalArray.buffer); console.log('diffSize: ' + finalsize + ' compressedSize: ' + finalArray.length); if (this.doIntegrityCheck) { this.message = 'Doing Integrity Check...'; } else { Module._neil_toggle_pause(); this.message = 'Sending to server...'; } var saveMessage = "Saved: " + finalArray.length.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); var xhr = new XMLHttpRequest; xhr.open("POST", this.settings.CLOUDSAVEURL + "/SendStaveState?name=" + this.base_name + '.doswasmx' + "&password=" + this.password + "&emulator=doswasmx", true); xhr.send(finalArray); xhr.onreadystatechange = function() { try{ if (xhr.readyState === 4) { let result = xhr.response; if (result=="\"Success\""){ toastr.info(saveMessage); if (myClass.doIntegrityCheck) { myClass.integrityCheck(compareHardDrive); } else { myClass.message = ''; } }else{ toastr.error('Error Saving Cloud Save'); } } } catch(error){ console.log(error); toastr.error('Error Loading Cloud Save'); } } } async compressArrayBuffer(input) { //create the stream const cs = new CompressionStream("gzip"); //create the writer const writer = cs.writable.getWriter(); //write the buffer to the writer writer.write(input); writer.close(); //create the output const output = []; const reader = cs.readable.getReader(); let totalSize = 0; //go through each chunk and add it to the output while (true) { const { value, done } = await reader.read(); if (done) break; output.push(value); totalSize += value.byteLength; } const concatenated = new Uint8Array(totalSize); let offset = 0; //finally build the compressed array and return it for (const array of output) { concatenated.set(array, offset); offset += array.byteLength; } console.log('compressed', concatenated); return concatenated; } async decompressArrayBuffer(input) { //create the stream const ds = new DecompressionStream("gzip"); //create the writer const writer = ds.writable.getWriter(); //write the buffer to the writer thus decompressing it writer.write(input); writer.close(); //create the output const output = []; //create the reader const reader = ds.readable.getReader(); let totalSize = 0; //go through each chunk and add it to the output while (true) { const { value, done } = await reader.read(); if (done) break; output.push(value); totalSize += value.byteLength; } const concatenated = new Uint8Array(totalSize); let offset = 0; //finally build the compressed array and return it for (const array of output) { concatenated.set(array, offset); offset += array.byteLength; } return concatenated; } exportHardDrive(){ let imgName = this.base_name + '.img'; let exportName = imgName; if (!this.loggedIn) { exportName = 'hdd.img'; } let filearray = FS.readFile(imgName); var file = new File([filearray], exportName, {type: "text/plain; charset=x-user-defined"}); saveAs(file); } importFiles(event){ console.log('import files'); if (!myClass.noCopyImport) { var rando = Math.floor(Math.random() * Math.floor(1000)); myClass.importFolderName = 'Imp' + rando; FS.mkdir('/' + myClass.importFolderName); } this.isSpecialHandler = false; this.importedFileNames = []; let files = event.currentTarget.files; for(let i = 0; i < files.length; i++) { this.importedFileNames.push(files[i].name); let fileExtension = files[i].name.substr(files[i].name.lastIndexOf('.')).toLocaleLowerCase(); if (this.specialFileHandlers.includes(fileExtension)) { this.isSpecialHandler = true; } } myClass.processImportFiles(files, 0) } processImportFiles(files, index){ var file = files[index]; console.log('processing file ' + (index+1) + ' of ' + files.length, file); var reader = new FileReader(); reader.onprogress = function (event) { let loaded = event.loaded; let total = event.total; loaded = Math.ceil(loaded / 1000000); total = Math.ceil(total / 1000000); console.log('loaded: ' + event.loaded); myClass.importStatus = '(' + (index+1) + ' of ' + files.length + ') ' + file.name + ' ' + loaded + 'MB / ' + total + 'MB'; }; reader.onload = function (e) { var byteArray = new Uint8Array(this.result); if (myClass.noCopyImport || myClass.isSpecialHandler || myClass.changeFloppy || myClass.loadFloppy) { FS.writeFile('/' + file.name, byteArray); } else { FS.writeFile('/' + myClass.importFolderName + '/' + file.name, byteArray); } if ( (index+1) { //focus on textbox $("#txtPassword").focus(); }, 500); } logout(){ this.loggedIn = false; this.password = ''; localStorage.setItem('doswasmx-password', this.password); } async loginSubmit(){ $('#loginModal').modal('hide'); this.loginModalOpened = false; let result = await this.loginToServer(); if (result=='Success'){ toastr.success('Logged In'); localStorage.setItem('doswasmx-password', this.password); await this.getSaveStates(); this.postLoginProcess(); } else{ toastr.error('Login Failed'); this.password = ''; localStorage.setItem('doswasmx-password', ''); } } async loginSilent(){ if (!this.hasCloud) return; let result = await this.loginToServer(); if (result=='Success'){ await this.getSaveStates(); this.postLoginProcess(); } } postLoginProcess(){ //filter by .doswasmx extension and sort by date this.dosSaveStates = this.allSaveStates.filter((state)=>{ return state.Name.endsWith('.savestate.doswasmx') }); this.dosSaveStates.forEach(state => { state.Date = this.convertCSharpDateTime(state.Date); }); this.dosSaveStates.sort((a,b)=>{ return b.Date.getTime() - a.Date.getTime() }); this.loggedIn = true; } convertCSharpDateTime(initialDate) { let dateString = initialDate; dateString = dateString.substring(0, dateString.indexOf('T')); let timeString = initialDate.substr(initialDate.indexOf("T") + 1); let dateComponents = dateString.split('-'); let timeComponents = timeString.split(':'); let myDate = null; myDate = new Date(parseInt(dateComponents[0]), parseInt(dateComponents[1]) - 1, parseInt(dateComponents[2]), parseInt(timeComponents[0]), parseInt(timeComponents[1]), parseInt(timeComponents[2])); return myDate; } async loginToServer(){ let result = await $.get(this.settings.CLOUDSAVEURL + '/Login?password=' + this.password); console.log('login result: ' + result); return result; } async getSaveStates(){ if (!this.loggedIn) return; let result = await $.get(this.settings.CLOUDSAVEURL + '/GetSaveStates?password=' + this.password); console.log('getSaveStates result: ', result); this.allSaveStates = result; result.forEach(element => { if (element.Name==this.base_name + ".savestate.doswasmx") this.noCloudSave = false; }); } //USE THIS FOR DOING AN INTEGRITY CHECK ON DIFFED HARD DRIVE - async integrityCheck(newHardDriveBytes) { let finalByteArray = await this.loadHardDriveDiffs(this.baseHardDrive); //hard drive with applied diffs //compare bytes this.message += 'Calculating Diffs...'; await new Promise(resolve => {setTimeout(resolve, 20); }); let compareHardDrive = finalByteArray; let chunkSize = 10000; let arrayChunks = []; //array of Uint8SubArrays each of size chunk this.diffCount = 0; let progressCounter = 5000000; //we update progress every 5 million for (let i = 0; i < newHardDriveBytes.length; i++) { if (newHardDriveBytes[i] != compareHardDrive[i]) { let end = i + chunkSize; if (end >= newHardDriveBytes.length) { end = newHardDriveBytes.length-1; } let subArray = compareHardDrive.subarray(i,end); arrayChunks.push( { index: i, data: subArray }); i += chunkSize; this.diffCount++; } if (i > progressCounter) { let percent = Math.floor( (i / newHardDriveBytes.length)*100 ); this.message = "Diffs: " + this.diffCount + ", " + percent + "%"; await new Promise(resolve => {setTimeout(resolve, 20); }); progressCounter += 5000000; } } console.log(arrayChunks); let finalsize = 0; for(let i = 0; i < arrayChunks.length; i++) { //8 bytes for the two ints representing index and length finalsize += 8; let chunk = arrayChunks[i]; finalsize += chunk.data.length; } this.message = "Generating Final Array Size: " + finalsize.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); this.message = "Diffs: " + this.diffCount + " Final Array Size: " + finalsize.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " DONE"; console.log('integrity check results', arrayChunks, this.diffCount); if (arrayChunks.length>0) { toastr.error("Failed integrity check"); } else { toastr.success("Passed integrity check"); } setTimeout(() => { myClass.message = ''; }, 2000); Module._neil_toggle_pause(); } togglePause(){ Module._neil_toggle_pause(); } updateCPU(value){ this.cpu = value; if (value == 'auto') { this.updateCpuNeil('cycles=auto'); } else if (value == 'max') { this.updateCpuNeil('cycles=max'); } else { this.updateCpuNeil('cycles=fixed ' + value); } } sendCtrlAltDel(){ Module._neil_send_ctrlaltdel(); } toggle16BitColorFix(){ Module._neil_toggle_16_bit_color_fix(); } toggleAlwaysUseBackbuffer(){ Module._neil_toggle_always_use_backbuffer(); } toggleAutoKeybaord(){ this.autoKeyboard = !this.autoKeyboard; if (this.autoKeyboard) { this.autoKeyboardTimer = this.autoKeyboardInterval; toastr.info('Auto Keyboard Enabled'); } else { toastr.info('Auto Keyboard Disabled'); } } //used to automate keyboard buttons on a timer (useful for certain games) tickAutoKeyboard(){ this.autoKeyboardTimer--; if (this.autoKeyboardTimer==0) { this.showToast("Autokeyboard...") this.sendKey(48) //F12 setTimeout(() => { myClass.sendKey(52); //enter }, 600); setTimeout(() => { myClass.sendKey(52); //enter }, 3000); this.autoKeyboardTimer = this.autoKeyboardInterval; } } turboSpeed() { Module._neil_turbo(); } } let myClass = new MyClass(); window["myApp"] = myClass; //so that I can reference from EM_ASM window["Module"] = { onRuntimeInitialized: myClass.initModule, canvas: document.getElementById('canvas'), print: (text) => myClass.processPrintStatement(text), // printErr: (text) => myClass.print(text) } var script = document.createElement('script'); script.src = 'main.js' document.getElementsByTagName('head')[0].appendChild(script); window.onerror = function(message) { console.log('window.onerror',message); myClass.onError(message); } window.onunhandledrejection = function(error) { console.log('window.onunhandledrejection',error); myClass.onError(error.reason.message); }