homeplay/public/emulator.js

239 lines
6.3 KiB
JavaScript

// 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();