// Init EJS stuff class HomeplayEmulator { scriptElement = null; initInterval = null; oldGetRetroArchCfg = null; gameStarted = false; saveUpdates = false; saveInterval = null; saveLock = false; constructor() { // Set up the EJS stuff window.EJS_player = "#game"; window.EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/"; window.EJS_language = "en-US"; window.EJS_volume = 0; window.EJS_startOnLoaded = true; window.EJS_backgroundColor = '#000'; window.EJS_Buttons = { playPause: false, restart: false, mute: false, settings: false, fullscreen: false, saveState: false, loadState: false, screenRecord: false, gamepad: false, cheat: false, volume: false, saveSavFiles: false, loadSavFiles: false, quickSave: false, quickLoad: false, screenshot: false, cacheManager: false, exitEmulation: false } window.EJS_defaultOptions = { }; // Bind events window.EJS_ready = this.onEJSReady; window.EJS_onGameStart = this.onEJSGameStart; window.EJS_onGameEnd = this.onEJSGameEnd; window.EJS_onSaveState = this.onEJSSaveState; window.EJS_onLoadState = this.onEJSLoadState; window.onmessage = this.onMessage; // Now check if we have the libs we NEED. if(!window.JSZip || !window.CryptoJS) { this.send({ message: 'error', error: 'Missing required libraries' }); return; } // Begin the init interval this.initInterval = setInterval(() => { this.send({ message: 'iframe_loaded' }); }, 1000); // Check for save updates this.saveInterval = setInterval(async () => { if(this.saveLock) return; this.saveLock = true; try { this.saveUpdates = await this.hasSaveUpdates(); if(this.saveUpdates) { await this.save(); } } catch(e) { this.send({ message: 'error', error: e.message }); } finally { this.saveLock = false; } }, 2000); } send = data => { window.parent.postMessage(data, '*'); } init = data => { window.EJS_gameUrl = `/api/v1/rom?id=${data.gameId}`; window.EJS_core = data.core; window.EJS_gameName = data.gameName; window.EJS_externalFiles = { '/homeplay/saves/': `/api/v1/save?id=${data.gameId}` } this.scriptElement = document.createElement('script'); this.scriptElement.src = 'https://cdn.emulatorjs.org/stable/data/loader.js'; document.body.appendChild(this.scriptElement); clearInterval(this.initInterval); } onEJSReady = () => { // Modify the EJS stuff this.oldGetRetroArchCfg = window.EJS_GameManager.prototype.getRetroArchCfg; const raConfig = (...a) => this.getRetroArchCfg(...a); window.EJS_GameManager.prototype.getRetroArchCfg = function() { return raConfig(this); } this.send({ message: 'ready' }); console.log('EJS Ready'); } onEJSGameStart = () => { console.log('EJS Game Start'); setTimeout(() => { window.EJS_emulator.gameManager.saveSaveFiles(); setTimeout(() => { this.gameStarted = true; }, 1000); }, 1000) } onEJSGameEnd = () => { console.log('EJS Game End'); } onEJSSaveState = () => { console.log('EJS Save State'); } onEJSLoadState = () => { console.log('EJS Load State');; } onMessage = e => { const { data } = e; if(!data) { console.error('No data'); return; } const { message } = data; switch(message) { case 'init': this.init(data); break; case 'volume': window.EJS_volume = data.volume; break; default: this.send({ message: 'error', error: 'Unknown message' }); break; } } getFileMD5 = async path => { const buffer = window.EJS_emulator.gameManager.FS.readFile(path); // Convert buffer to string, likely going to need to change later. const bufferArray = Array.from(new Uint8Array(buffer)); const bufferString = bufferArray.map(b => String.fromCharCode(b)).join(''); // Hash string const hash = window.CryptoJS.MD5(bufferString); const hashString = hash.toString(CryptoJS.enc.Base64); return hashString; } getRetroArchCfg = (gameManager) => { const ejsConfig = this.oldGetRetroArchCfg.call(gameManager); // Parse back into key value pair const raConfig = ejsConfig.split('\n').reduce((acc, line) => { if(!line.length) return acc; const [key, value] = line.split('='); acc[key.trim()] = value.trim(); return acc; }, {}); // Add new options raConfig['savefiles_in_content_dir'] = false; raConfig['sort_savefiles_by_content_enable'] = false; raConfig['sort_savefiles_enable'] = false; raConfig['savefile_directory'] = '/homeplay/saves'; // Return back as RA config string return Object.entries(raConfig).map(([k, v]) => `${k} = ${v}`).join('\n'); } getSaveFiles = () => { return [ 'rom.srm', 'rom.rtc' ]; } hasSaveUpdates = async () => { if(!window.EJS_emulator) return false; if(!window.EJS_emulator.gameManager) return false; if(!this.gameStarted) return false; // At least 20 seconds must've passed. if(window.EJS_emulator.gameManager.getFrameNum() < 20 * 60) return false; const saveFiles = this.getSaveFiles(); const hashes = await Promise.all(saveFiles.map(f => this.getFileMD5(`/homeplay/saves/${f}`))); window.EJS_emulator.gameManager.saveSaveFiles(); const newHashes = await Promise.all(saveFiles.map(f => this.getFileMD5(`/homeplay/saves/${f}`))); const changedFiles = saveFiles.filter((f, i) => hashes[i] !== newHashes[i]); return !!changedFiles.length; } save = async () => { // Check if updates if(!this.hasSaveUpdates) return; // Get save file contents const saveFiles = this.getSaveFiles(); // Now we need to create a zip archive. EJS provides a nice ZIP library. const zip = new JSZip(); await Promise.all(saveFiles.map(async f => { const data = window.EJS_emulator.gameManager.FS.readFile(`/homeplay/saves/${f}`); zip.file(f, data); })); this.send({ message: 'save', data: await zip.generateAsync({ type:"base64" }) }) } } window.homeplay = new HomeplayEmulator();