class EmulatorJS { createElement(type) { return document.createElement(type); } addEventListener(element, listener, callback) { const listeners = listener.split(" "); for (let i=0; i { const progress = e.total ? ' '+Math.floor(e.loaded / e.total * 100).toString()+'%' : ' '+(e.loaded/1048576).toFixed(2)+'MB'; progressCB(progress); }); } xhr.onload = function() { if (xhr.readyState === xhr.DONE) { let data = xhr.response; try {data=JSON.parse(data)}catch(e){} cb({ data: data, headers: { "content-length": xhr.getResponseHeader('content-length'), "content-type": xhr.getResponseHeader('content-type'), "last-modified": xhr.getResponseHeader('last-modified') } }); } } xhr.responseType = opts.responseType; xhr.onerror = () => cb(-1); xhr.open(opts.method, path, true); xhr.send(); } else { (async () => { //Most commonly blob: urls. Not sure what else it could be if (opts.method === 'HEAD') { cb({headers:{}}); return; } let res; try { res = await fetch(path); if (opts.type && opts.type.toLowerCase() === 'arraybuffer') { res = await res.arrayBuffer(); } else { res = await res.text(); try {res = JSON.parse(res)} catch(e) {} } } catch(e) { cb(-1); } if (path.startsWith('blob:')) URL.revokeObjectURL(path); cb({ data: res, headers: {} }); }) } } constructor(element, config) { this.debug = (window.EJS_DEBUG_XX === true); this.setElements(element); this.started = false; this.paused = true; this.listeners = []; this.config = config; this.canvas = this.createElement('canvas'); this.canvas.classList.add('ejs_canvas'); this.bindListeners(); this.game.classList.add("ejs_game"); this.createStartButton(); console.log(this) } setElements(element) { this.game = document.querySelector(element); this.elements = { main: this.game, parent: this.game.parentElement } this.elements.parent.classList.add("ejs_parent"); } // Start button createStartButton() { const button = this.createElement("div"); button.classList.add("ejs_start_button"); button.innerText = this.localization("Start Game"); this.elements.parent.appendChild(button); this.addEventListener(button, "click", this.startButtonClicked.bind(this)); } startButtonClicked(e) { e.preventDefault(); e.target.remove(); this.createText(); this.downloadGameCore(); } // End start button createText() { this.textElem = this.createElement("div"); this.textElem.classList.add("ejs_loading_text"); this.textElem.innerText = this.localization("Loading..."); this.elements.parent.appendChild(this.textElem); } localization(text) { //todo return text; } checkCompression(data, msg) { if (msg) { this.textElem.innerText = msg; } //to be put in another file function isCompressed(data) { //https://www.garykessler.net/library/file_sigs.html //todo. Use hex instead of numbers if ((data[0] === 80 && data[1] === 75) && ((data[2] === 3 && data[3] === 4) || (data[2] === 5 && data[3] === 6) || (data[2] === 7 && data[3] === 8))) { return 'zip'; } else if (data[0] === 55 && data[1] === 122 && data[2] === 188 && data[3] === 175 && data[4] === 39 && data[5] === 28) { return '7z'; } else if ((data[0] === 82 && data[1] === 97 && data[2] === 114 && data[3] === 33 && data[4] === 26 && data[5] === 7) && ((data[6] === 0) || (data[6] === 1 && data[7] == 0))) { return 'rar'; } } const createWorker = (path) => { return new Promise((resolve, reject) => { this.downloadFile(path, (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } const blob = new Blob([res.data], { 'type': 'application/javascript' }) const url = window.URL.createObjectURL(blob); resolve(new Worker(url)); }, null, false, {responseType: "arraybuffer", method: "GET"}); }) } const decompress7z = (file) => { return new Promise((resolve, reject) => { const files = {}; const onMessage = (data) => { if (!data.data) return; //data.data.t/ 4=progress, 2 is file, 1 is zip done if (data.data.t === 4 && msg) { const pg = data.data; const num = Math.floor(pg.current / pg.total * 100); if (isNaN(num)) return; const progress = ' '+num.toString()+'%'; this.textElem.innerText = msg + progress; } if (data.data.t === 2) { files[data.data.file] = data.data.data; } if (data.data.t === 1) { resolve(files); } } createWorker('compression/extract7z.js').then((worker) => { worker.onmessage = onMessage; worker.postMessage(file); //console.log(file); }) }) } async function decompressRar() { } const compression = isCompressed(data.slice(0, 10)); if (compression) { //Need to do zip and rar still return decompress7z(data); } else { return new Promise(resolve => resolve(data)); } } downloadGameCore() { this.textElem.innerText = this.localization("Download Game Core"); this.downloadFile('cores/'+this.getCore()+'-wasm.data', (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } this.checkCompression(new Uint8Array(res.data), this.localization("Decompress Game Core")).then((data) => { //console.log(data); let js, wasm; for (let k in data) { if (k.endsWith(".wasm")) { wasm = data[k]; } else if (k.endsWith(".js")) { js = data[k]; } } this.initGameCore(js, wasm); }); }, (progress) => { this.textElem.innerText = this.localization("Download Game Core") + progress; }, false, {responseType: "arraybuffer", method: "GET"}); } initGameCore(js, wasm) { this.initModule(wasm); let script = this.createElement("script"); script.src = URL.createObjectURL(new Blob([js], {type: "application/javascript"})); document.body.appendChild(script); } getCore() { const core = this.config.system; //switch case or an object holding this data if (core === 'nes') { return 'fceumm'; } } downloadRom() { this.gameManager = new window.EJS_GameManager(this.Module); this.textElem.innerText = this.localization("Download Game Data"); this.downloadFile(this.config.gameUrl, (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } this.checkCompression(new Uint8Array(res.data), this.localization("Decompress Game Data")).then((data) => { FS.writeFile("/game", data); this.startGame(); }); }, (progress) => { this.textElem.innerText = this.localization("Download Game Data") + progress; }, true, {responseType: "arraybuffer", method: "GET"}); } initModule(wasmData) { window.Module = { 'TOTAL_MEMORY': 0x10000000, 'noInitialRun': true, 'onRuntimeInitialized': this.downloadRom.bind(this), 'arguments': [], 'preRun': [], 'postRun': [], 'canvas': this.canvas, 'print': (msg) => { if (this.debug) { console.log(msg); } }, 'printErr': (msg) => { if (this.debug) { console.log(msg); } }, 'totalDependencies': 0, 'monitorRunDependencies': () => {}, 'locateFile': function(fileName) { console.log(fileName); if (fileName.endsWith(".wasm")) { return URL.createObjectURL(new Blob([wasmData], {type: "application/wasm"})); } }, 'readAsync': function(a, b, c) { console.log(a, b, c) } }; this.Module = window.Module; } startGame() { this.textElem.remove(); this.textElem = null; this.game.classList.remove("ejs_game"); this.game.appendChild(this.canvas); const args = []; if (this.debug) args.push('-v'); args.push('/game'); Module.callMain(args); Module.resumeMainLoop(); this.started = true; this.paused = false; Module.setCanvasSize(800, 600); let i=0; // this needs to be fixed. Ugh. let j = setInterval(function() { // some cores have a messed up screen size on load (for example - gba) if (i>20) clearInterval(j); i++; Module.setCanvasSize(800, 600); }, 100) } bindListeners() { this.createContextMenu(); this.createBottomMenuBar(); //keyboard, etc... } createContextMenu() { this.elements.contextmenu = this.createElement('div'); this.elements.contextmenu.classList.add("ejs_context_menu"); this.addEventListener(this.game, 'contextmenu', (e) => { if (this.started) { this.elements.contextmenu.style.display = "block"; this.elements.contextmenu.style.left = e.offsetX; this.elements.contextmenu.style.top = e.offsetY; } e.preventDefault(); }) const hideMenu = () => { this.elements.contextmenu.style.display = "none"; } this.addEventListener(this.elements.contextmenu, 'contextmenu', (e) => e.preventDefault()); this.addEventListener(this.elements.parent, 'contextmenu', (e) => e.preventDefault()); this.addEventListener(this.game, 'mousedown', hideMenu); const parent = this.createElement("ul"); const addButton = (title, hidden, functi0n) => { //
  • '+title+'
  • const li = this.createElement("li"); if (hidden) li.hidden = true; const a = this.createElement("a"); if (functi0n instanceof Function) { this.addEventListener(li, 'click', (e) => { e.preventDefault(); functi0n(); }); } a.href = "#"; a.onclick = "return false"; a.innerText = title; li.appendChild(a); parent.appendChild(li); hideMenu(); } let screenshotUrl; addButton("Take Screenshot", false, () => { if (screenshotUrl) URL.revokeObjectURL(screenshotUrl); const screenshot = this.gameManager.screenshot(); const blob = new Blob([screenshot]); screenshotUrl = URL.createObjectURL(blob); const a = this.createElement("a"); a.href = screenshotUrl; a.download = "screenshot.png"; a.click(); hideMenu(); }); addButton("Quick Save", false, () => { this.gameManager.quickSave(); hideMenu(); }); addButton("Quick Load", false, () => { this.gameManager.quickLoad(); hideMenu(); }); addButton("EmulatorJS", false, () => { hideMenu(); const body = this.createPopup("EmulatorJS", { "Close": () => { this.closePopup(); } }); body.innerText = "Todo. Write about, include tabs on side with licenses, links to docs/repo/discord?"; }); this.elements.contextmenu.appendChild(parent); this.elements.parent.appendChild(this.elements.contextmenu); } closePopup() { if (this.currentPopup !== null) { try { this.currentPopup.remove(); } catch(e){} this.currentPopup = null; } } //creates a full box popup. createPopup(popupTitle, buttons) { this.closePopup(); this.currentPopup = this.createElement('div'); this.currentPopup.classList.add("ejs_popup_container"); this.elements.parent.appendChild(this.currentPopup); const title = this.createElement("h4"); title.innerText = popupTitle; const main = this.createElement("div"); main.classList.add("ejs_popup_body"); this.currentPopup.appendChild(title); this.currentPopup.appendChild(main); for (let k in buttons) { const button = this.createElement("a"); if (buttons[k] instanceof Function) { button.addEventListener("click", (e) => { buttons[k](); e.preventDefault(); }); } button.classList.add("ejs_button"); button.innerText = k; this.currentPopup.appendChild(button); } return main; } selectFile() { return new Promise((resolve, reject) => { const file = this.createElement("input"); file.type = "file"; this.addEventListener(file, "change", (e) => { resolve(e.target.files[0]); }) file.click(); }) } createBottomMenuBar() { this.elements.menu = this.createElement("div"); this.elements.menu.classList.add("ejs_menu_bar"); this.elements.menu.classList.add("ejs_menu_bar_hidden"); let timeout = null; const hide = () => { if (this.paused) return; this.elements.menu.classList.add("ejs_menu_bar_hidden"); } this.addEventListener(this.elements.parent, 'mousemove click', (e) => { if (!this.started) return; if (timeout !== null) clearTimeout(timeout); timeout = setTimeout(hide, 3000); this.elements.menu.classList.remove("ejs_menu_bar_hidden"); }) this.menu = { close: () => { if (!this.started) return; if (timeout !== null) clearTimeout(timeout); this.elements.menu.classList.remove("ejs_menu_bar_hidden"); }, open: () => { if (!this.started) return; if (timeout !== null) clearTimeout(timeout); timeout = setTimeout(hide, 3000); this.elements.menu.classList.remove("ejs_menu_bar_hidden"); } } this.elements.parent.appendChild(this.elements.menu); //Now add buttons const addButton = (title, image, callback) => { const button = this.createElement("button"); button.type = "button"; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute("role", "presentation"); svg.setAttribute("focusable", "false"); svg.innerHTML = image; const text = this.createElement("span"); text.innerText = title; text.classList.add("ejs_menu_text"); button.classList.add("ejs_menu_button"); button.appendChild(svg); button.appendChild(text); this.elements.menu.appendChild(button); if (callback instanceof Function) { this.addEventListener(button, 'click', callback); } } //todo. Center text on not restart button addButton("Restart", '', () => { this.gameManager.restart(); }); let stateUrl; addButton("Save State", '', async () => { if (stateUrl) URL.revokeObjectURL(stateUrl); const state = await this.gameManager.getState(); const blob = new Blob([state]); stateUrl = URL.createObjectURL(blob); const a = this.createElement("a"); a.href = stateUrl; a.download = "game.state"; a.click(); }); addButton("Load State", '', async () => { const file = await this.selectFile(); const state = new Uint8Array(await file.arrayBuffer()); this.gameManager.loadState(state); }); } }