class EmulatorJS { createElement(type) { return document.createElement(type); } addEventListener(element, listener, callback) { const listeners = listener.split(" "); let rv = []; 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) { window.EJS_TESTING = this; this.touch = false; this.debug = (window.EJS_DEBUG_XX === true); this.cheats = []; 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.fullscreen = false; 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"); this.elements.parent.setAttribute("tabindex", -1); } // 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, "touchstart", () => { this.touch = true; }) 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); }) }) } const decompressRar = (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); } } this.downloadFile("compression/libunrar.js", (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } const path = origin + this.config.dataPath + 'compression/libunrar.js.mem'; let data = '\nlet dataToPass = [];\nModule = {\n monitorRunDependencies: function(left) {\n if (left == 0) {\n setTimeout(function() {\n unrar(dataToPass, null);\n }, 100);\n }\n },\n onRuntimeInitialized: function() {\n },\n locateFile: function(file) {\n return \''+path+'\';\n }\n};\n'+res.data+'\nlet unrar = function(data, password) {\n let cb = function(fileName, fileSize, progress) {\n postMessage({"t":4,"current":progress,"total":fileSize, "name": fileName});\n };\n\n let rarContent = readRARContent(data.map(function(d) {\n return {\n name: d.name,\n content: new Uint8Array(d.content)\n }\n }), password, cb)\n let rec = function(entry) {\n if (entry.type === \'file\') {\n postMessage({"t":2,"file":entry.fullFileName,"size":entry.fileSize,"data":entry.fileContent});\n } else if (entry.type === \'dir\') {\n Object.keys(entry.ls).forEach(function(k) {\n rec(entry.ls[k]);\n })\n } else {\n throw "Unknown type";\n }\n }\n rec(rarContent);\n postMessage({"t":1});\n return rarContent;\n};\nonmessage = function(data) {\n dataToPass.push({name: \'test.rar\', content: data.data});\n};\n '; const blob = new Blob([data], { 'type': 'application/javascript' }) const url = window.URL.createObjectURL(blob); const worker = new Worker(url); worker.onmessage = onMessage; worker.postMessage(file); }, null, false, {responseType: "text", method: "GET"}); }) } const decompressZip = (file) => { return new Promise((resolve, reject) => { const files = {}; const onMessage = (data) => { //console.log(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/extractzip.js').then((worker) => { worker.onmessage = onMessage; worker.postMessage(file); }) }) } const compression = isCompressed(data.slice(0, 10)); if (compression) { //Need to do zip and rar still if (compression === "7z") { return decompress7z(data); } else if (compression === "zip") { return decompressZip(data); } else if (compression === "rar") { return decompressRar(data); } } else { return new Promise(resolve => resolve({"!!notCompressedData": 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(generic) { const core = this.config.system; if (generic) { const options = { 'fceumm': 'nes', 'snes9x': 'snes', 'a5200': 'atari5200', 'gambatte': 'gb', 'mgba': 'gba', 'beetle_vb': 'vb', 'mupen64plus_next': 'n64', 'desmume2015': 'nds', 'mame2003': 'mame2003', 'fbalpha2012_cps1': 'arcade', 'fbalpha2012_cps2': 'arcade', 'mednafen_psx': 'psx', 'mednafen_psx_hw': 'psx', 'melonds': 'nds', 'nestopia': 'nes', 'opera': '3do' } return options[core] || core; } const options = { 'nes': 'fceumm', 'snes': 'snes9x', 'atari5200': 'a5200', 'gb': 'gambatte', 'gba': 'mgba', 'vb': 'beetle_vb', 'n64': 'mupen64plus_next', 'nds': 'desmume2015', 'mame2003': 'mame2003', 'arcade': 'fbalpha2012_cps1', // I need to find a more compatible arcade core 'psx': 'mednafen_psx_hw', '3do': 'opera' } return options[core] || core; } getBaseFileName() { //Only once game and core is loaded if (!this.started) return null; let parts = this.fileName.split("."); parts.splice(parts.length-1, 1); return parts.join("."); } downloadBios() { if (!this.config.biosUrl) { this.startGame(); return; } this.textElem.innerText = this.localization("Download Game BIOS"); this.downloadFile(this.config.biosUrl, (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } this.checkCompression(new Uint8Array(res.data), this.localization("Decompress Game BIOS")).then((data) => { for (const k in data) { if (k === "!!notCompressedData") { FS.writeFile(this.config.biosUrl.split('/').pop().split("#")[0].split("?")[0], data[k]); break; } if (k.endsWith('/')) continue; console.log(k.split('/').pop()); FS.writeFile(k.split('/').pop(), data[k]); } this.startGame(); }); }, (progress) => { this.textElem.innerText = this.localization("Download Game Data") + progress; }, true, {responseType: "arraybuffer", method: "GET"}); } 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) => { for (const k in data) { if (k === "!!notCompressedData") { this.fileName = this.config.gameUrl.split('/').pop().split("#")[0].split("?")[0]; FS.writeFile(this.fileName, data[k]); break; } if (k.endsWith('/')) { FS.mkdir(k); continue; } this.fileName = k; console.log(k); FS.writeFile(k, data[k]); } this.downloadBios(); }); }, (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('/'+this.fileName); console.log(args); this.Module.callMain(args); this.Module.resumeMainLoop(); this.started = true; this.paused = false; if (this.touch) { this.virtualGamepad.style.display = ""; } this.setupSettingsMenu(); this.handleResize(); this.updateCheatUI(); this.updateGamepadLabels(); this.elements.parent.focus(); } bindListeners() { this.createContextMenu(); this.createBottomMenuBar(); this.createControlSettingMenu(); this.createCheatsMenu() this.setVirtualGamepad(); this.addEventListener(this.elements.parent, "keydown keyup", this.keyChange.bind(this)); this.addEventListener(this.elements.parent, "mousedown mouseup click touchstart touchend touchcancel", (e) => { this.elements.parent.focus(); }) this.addEventListener(window, "resize", this.handleResize.bind(this)); //this.addEventListener(window, "blur", e => console.log(e), true); //TODO - add "click to make keyboard keys work" message this.gamepad = new GamepadHandler(); //https://github.com/ethanaobrien/Gamepad this.gamepad.on('connected', (e) => { if (!this.gamepadLabels) return; this.updateGamepadLabels(); }) this.gamepad.on('disconnected', (e) => { setTimeout(this.updateGamepadLabels.bind(this), 10); }) this.gamepad.on('axischanged', function(e) { console.log('axischanged', e); }) this.gamepad.on('buttondown', function(e) { console.log('buttondown', e); }) this.gamepad.on('buttonup', function(e) { console.log('buttonup', e); }) } updateGamepadLabels() { for (let i=0; i { 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; const date = new Date(); a.download = this.getBaseFileName()+"-"+date.getMonth()+"-"+date.getDate()+"-"+date.getFullYear()+".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, hidden) { if (!hidden) this.closePopup(); const popup = this.createElement('div'); popup.classList.add("ejs_popup_container"); this.elements.parent.appendChild(popup); const title = this.createElement("h4"); title.innerText = popupTitle; const main = this.createElement("div"); main.classList.add("ejs_popup_body"); popup.appendChild(title); popup.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; popup.appendChild(button); } if (!hidden) { this.currentPopup = popup; } else { popup.style.display = "none"; } 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(); }) } isPopupOpen() { return this.cheatMenu.style.display !== "none" || this.controlMenu.style.display !== "none"; } 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 || this.settingsMenuOpen) return; this.elements.menu.classList.add("ejs_menu_bar_hidden"); } this.addEventListener(this.elements.parent, 'mousemove click', (e) => { if (!this.started) return; if (this.isPopupOpen()) 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, element, both) => { 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); if (element) { element.appendChild(button); } else { this.elements.menu.appendChild(button); } if (callback instanceof Function) { this.addEventListener(button, 'click', callback); } return both ? [button, svg, text] : button; } //todo. Center text on not restart button addButton("Restart", '', () => { this.gameManager.restart(); }); const pauseButton = addButton("Pause", '', () => { this.togglePlaying(); }); const playButton = addButton("Play", '', () => { this.togglePlaying(); }); playButton.style.display = "none"; this.togglePlaying = () => { this.paused = !this.paused; if (this.paused) { pauseButton.style.display = "none"; playButton.style.display = ""; } else { pauseButton.style.display = ""; playButton.style.display = "none"; } this.gameManager.toggleMainLoop(this.paused ? 0 : 1); } 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 = this.getBaseFileName()+".state"; a.click(); }); addButton("Load State", '', async () => { const file = await this.selectFile(); const state = new Uint8Array(await file.arrayBuffer()); this.gameManager.loadState(state); }); addButton("Control Settings", '', () => { this.controlMenu.style.display = ""; }); addButton("Cheats", '', () => { this.cheatMenu.style.display = ""; }); const spacer = this.createElement("span"); spacer.style = "flex:1;"; this.elements.menu.appendChild(spacer); this.settingParent = this.createElement("div"); this.settingsMenuOpen = false; const settingButton = addButton("Settings", '', () => { this.settingsMenuOpen = !this.settingsMenuOpen; settingButton[1].classList.toggle("ejs_svg_rotate", this.settingsMenuOpen); settingButton[2].style.display = this.settingsMenuOpen ? "none" : ""; this.settingsMenu.style.display = this.settingsMenuOpen ? "" : "none"; }, this.settingParent, true); this.elements.menu.appendChild(this.settingParent); this.closeSettingsMenu = () => { if (!this.settingsMenu) return; this.settingsMenuOpen = false; settingButton[1].classList.toggle("ejs_svg_rotate", this.settingsMenuOpen); settingButton[2].style.display = ""; this.settingsMenu.style.display = "none"; } this.addEventListener(this.elements.parent, "click", (e) => { if (e.target === settingButton[0]) return; setTimeout(() => { if (this.settingsJustClicked) { this.settingsJustClicked = false; return; } this.closeSettingsMenu(); }, 10) }) const enter = addButton("Enter Fullscreen", '', () => { if (this.elements.parent.requestFullscreen) { this.elements.parent.requestFullscreen(); } else if (this.elements.parent.mozRequestFullScreen) { this.elements.parent.mozRequestFullScreen(); } else if (this.elements.parent.webkitRequestFullscreen) { this.elements.parent.webkitRequestFullscreen(); } else if (this.elements.parent.msRequestFullscreen) { this.elements.parent.msRequestFullscreen(); } exit.style.display = ""; enter.style.display = "none"; }); //todo, when user exits by pressing esc, labels dont currently change const exit = addButton("Exit Fullscreen", '', () => { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } exit.style.display = "none"; enter.style.display = ""; }); exit.style.display = "none"; this.addEventListener(document, "webkitfullscreenchange mozfullscreenchange fullscreenchange", (e) => { if (e.target !== this.elements.parent) return; if (document.fullscreenElement === null) { exit.style.display = "none"; enter.style.display = ""; } else { //not sure if this is possible, lets put it here anyways exit.style.display = ""; enter.style.display = "none"; } }) } createControlSettingMenu() { let buttonListeners = []; this.gamepadLabels = []; this.controls = this.defaultControllers; const body = this.createPopup("Control Settings", { "Reset": () => { this.controls = JSON.parse(JSON.stringify(this.defaultControllers)); buttonListeners.forEach(elem => elem()); }, "Clear": () => { this.controls = {0:{},1:{},2:{},3:{}}; buttonListeners.forEach(elem => elem()); }, "Close": () => { this.controlMenu.style.display = "none"; } }, true); this.controlMenu = body.parentElement; body.classList.add("ejs_control_body"); const buttons = { 0: 'B', 1: 'Y', 2: 'SELECT', 3: 'START', 4: 'UP', 5: 'DOWN', 6: 'LEFT', 7: 'RIGHT', 8: 'A', 9: 'X', 10: 'L', 11: 'R', 12: 'L2', 13: 'R2', 14: 'L3', 15: 'R3', 19: 'L STICK UP', 18: 'L STICK DOWN', 17: 'L STICK LEFT', 16: 'L STICK RIGHT', 23: 'R STICK UP', 22: 'R STICK DOWN', 21: 'R STICK LEFT', 20: 'R STICK RIGHT', 24: this.localization('QUICK SAVE STATE'), 25: this.localization('QUICK LOAD STATE'), 26: this.localization('CHANGE STATE SLOT') } let selectedPlayer; let players = []; let playerDivs = []; const playerSelect = this.createElement("ul"); playerSelect.classList.add("ejs_control_player_bar"); for (let i=1; i<5; i++) { const playerContainer = this.createElement("li"); playerContainer.classList.add("tabs-title"); playerContainer.setAttribute("role", "presentation"); const player = this.createElement("a"); player.innerText = "Player "+i; player.setAttribute("role", "tab"); player.setAttribute("aria-controls", "controls-"+(i-1)); player.setAttribute("aria-selected", "false"); player.id = "controls-"+(i-1)+"-label"; this.addEventListener(player, "click", (e) => { e.preventDefault(); players[selectedPlayer].classList.remove("ejs_control_selected"); playerDivs[selectedPlayer].setAttribute("hidden", ""); selectedPlayer = i-1; players[i-1].classList.add("ejs_control_selected"); playerDivs[i-1].removeAttribute("hidden"); }) playerContainer.appendChild(player); playerSelect.appendChild(playerContainer); players.push(playerContainer); } body.appendChild(playerSelect); const controls = this.createElement("div"); for (let i=0; i<4; i++) { const player = this.createElement("div"); const playerTitle = this.createElement("div"); const gamepadTitle = this.createElement("div"); gamepadTitle.style = "font-size:12px;"; gamepadTitle.innerText = "Connected Gamepad: "; const gamepadName = this.createElement("span"); this.gamepadLabels.push(gamepadName); gamepadName.innerText = "n/a"; gamepadTitle.appendChild(gamepadName); const leftPadding = this.createElement("div"); leftPadding.style = "width:25%;float:left;"; leftPadding.innerHTML = " "; const aboutParent = this.createElement("div"); aboutParent.style = "font-size:12px;width:50%;float:left;"; const gamepad = this.createElement("div"); gamepad.style = "text-align:center;width:50%;float:left;"; gamepad.innerText = "Gamepad"; aboutParent.appendChild(gamepad); const keyboard = this.createElement("div"); keyboard.style = "text-align:center;width:50%;float:left;"; keyboard.innerText = "Keyboard"; aboutParent.appendChild(keyboard); const headingPadding = this.createElement("div"); headingPadding.style = "clear:both;"; playerTitle.appendChild(gamepadTitle); playerTitle.appendChild(leftPadding); playerTitle.appendChild(aboutParent); playerTitle.appendChild(headingPadding); player.appendChild(playerTitle); for (const k in buttons) { const buttonText = this.createElement("div"); buttonText.setAttribute("data-id", k); buttonText.setAttribute("data-index", i); buttonText.setAttribute("data-label", buttons[k]); buttonText.style = "margin-bottom:10px;"; buttonText.classList.add("ejs_control_bar"); const title = this.createElement("div"); title.style = "width:25%;float:left;font-size:12px;"; const label = this.createElement("label"); label.innerText = buttons[k]+":"; title.appendChild(label); const textBoxes = this.createElement("div"); textBoxes.style = "width:50%;float:left;"; const textBox1Parent = this.createElement("div"); textBox1Parent.style = "width:50%;float:left;padding: 0 5px;"; const textBox1 = this.createElement("input"); textBox1.style = "text-align:center;height:25px;width: 100%;"; textBox1.type = "text"; textBox1.setAttribute("readonly", ""); textBox1.setAttribute("placeholder", ""); textBox1Parent.appendChild(textBox1); const textBox2Parent = this.createElement("div"); textBox2Parent.style = "width:50%;float:left;padding: 0 5px;"; const textBox2 = this.createElement("input"); textBox2.style = "text-align:center;height:25px;width: 100%;"; textBox2.type = "text"; textBox2.setAttribute("readonly", ""); textBox2.setAttribute("placeholder", ""); textBox2Parent.appendChild(textBox2); buttonListeners.push(() => { textBox2.value = ""; textBox1.value = ""; if (this.controls[i][k] && this.controls[i][k].value) { textBox2.value = this.controls[i][k].value; } if (this.controls[i][k] && this.controls[i][k].value2) { textBox1.value = this.controls[i][k].value2; } }) if (this.controls[i][k] && this.controls[i][k].value) { textBox2.value = this.controls[i][k].value; } if (this.controls[i][k] && this.controls[i][k].value2) { textBox1.value = this.controls[i][k].value2; } textBoxes.appendChild(textBox1Parent); textBoxes.appendChild(textBox2Parent); const padding = this.createElement("div"); padding.style = "clear:both;"; textBoxes.appendChild(padding); const setButton = this.createElement("div"); setButton.style = "width:25%;float:left;"; const button = this.createElement("a"); button.classList.add("ejs_control_set_button"); button.innerText = "Set"; setButton.appendChild(button); const padding2 = this.createElement("div"); padding2.style = "clear:both;"; buttonText.appendChild(title); buttonText.appendChild(textBoxes); buttonText.appendChild(setButton); buttonText.appendChild(padding2); player.appendChild(buttonText); this.addEventListener(buttonText, "mousedown", (e) => { e.preventDefault(); this.controlPopup.parentElement.removeAttribute("hidden"); this.controlPopup.innerText = "[ " + buttons[k] + " ]\n"; this.controlPopup.setAttribute("button-num", k); this.controlPopup.setAttribute("player-num", i); this.updateTextBoxes = () => { if (this.controls[i][k] && this.controls[i][k].value) { textBox2.value = this.controls[i][k].value; } if (this.controls[i][k] && this.controls[i][k].value2) { textBox1.value = this.controls[i][k].value2; } delete this.updateTextBoxes; } }) } controls.appendChild(player); player.setAttribute("hidden", ""); playerDivs.push(player); } body.appendChild(controls); selectedPlayer = 0; players[0].classList.add("ejs_control_selected"); playerDivs[0].removeAttribute("hidden"); const popup = this.createElement('div'); popup.classList.add("ejs_popup_container"); const popupMsg = this.createElement("div"); popupMsg.classList.add("ejs_popup_box"); popupMsg.innerText = ""; popup.setAttribute("hidden", ""); this.controlPopup = popupMsg; popup.appendChild(popupMsg); this.controlMenu.appendChild(popup); } defaultControllers = { 0: { 0: { 'value': 'x' }, 1: { 'value': 's' }, 2: { 'value': 'v' }, 3: { 'value': 'enter' }, 4: { 'value': 'arrowup' }, 5: { 'value': 'arrowdown' }, 6: { 'value': 'arrowleft' }, 7: { 'value': 'arrowright' }, 8: { 'value': 'z' }, 9: { 'value': 'a' }, 10: { 'value': 'q' }, 11: { 'value': 'e' }, 12: { 'value': 'e' }, 13: { 'value': 'w' }, 14: {}, 15: {}, 16: { 'value': 'h' }, 17: { 'value': 'f' }, 18: { 'value': 'g' }, 19: { 'value': 't' }, 20: {'value': 'l'}, 21: {'value': 'j'}, 22: {'value': 'k'}, 23: {'value': 'i'}, 24: {}, 25: {}, 26: {} }, 1: {}, 2: {}, 3: {} } controls; keyChange(e) { if (!this.started) return; if (this.settingsMenu.style.display !== "none" || this.cheatMenu.style.display !== "none") return; e.preventDefault(); if (this.controlPopup.parentElement.getAttribute("hidden") === null) { const num = this.controlPopup.getAttribute("button-num"); const player = this.controlPopup.getAttribute("player-num"); if (!this.controls[player][num]) { this.controls[player][num] = {}; } this.controls[player][num].value = e.key.toLowerCase(); this.controlPopup.parentElement.setAttribute("hidden", ""); this.updateTextBoxes(); return; } const special = [16, 17, 18, 19, 20, 21, 22, 23]; for (let i=0; i<4; i++) { for (let j=0; j<26; j++) { if (this.controls[i][j] && this.controls[i][j].value === e.key.toLowerCase()) { this.gameManager.simulateInput(i, j, (e.type === 'keyup' ? 0 : (special.includes(j) ? 0x7fff : 1))); } } } } setVirtualGamepad() { this.virtualGamepad = this.createElement("div"); this.virtualGamepad.style.display = "none"; this.virtualGamepad.classList.add("ejs_virtualGamepad_parent"); this.elements.parent.appendChild(this.virtualGamepad); let info; if (this.config.VirtualGamepadSettings && function(set) { if (!Array.isArray(set)) { console.warn("Vritual gamepad settings is not array! Using default gamepad settings"); return false; } if (!set.length) { console.warn("Virtual gamepad settings is empty! Using default gamepad settings"); return false; } for (let i=0; i { e.preventDefault(); if (e.type === 'touchend' || e.type === 'touchcancel') { e.target.classList.remove("ejs_virtualGamepad_button_down"); window.setTimeout(() => { this.gameManager.simulateInput(0, value, 0); }) } else { e.target.classList.add("ejs_virtualGamepad_button_down"); this.gameManager.simulateInput(0, value, 1); } }) } } const createDPad = (opts) => { const container = opts.container; const callback = opts.event; const dpadMain = this.createElement("div"); dpadMain.classList.add("ejs_dpad_main"); const vertical = this.createElement("div"); vertical.classList.add("ejs_dpad_vertical"); const horizontal = this.createElement("div"); horizontal.classList.add("ejs_dpad_horizontal"); const bar1 = this.createElement("div"); bar1.classList.add("ejs_dpad_bar"); const bar2 = this.createElement("div"); bar2.classList.add("ejs_dpad_bar"); horizontal.appendChild(bar1); vertical.appendChild(bar2); dpadMain.appendChild(vertical); dpadMain.appendChild(horizontal); const updateCb = (e) => { e.preventDefault(); const touch = e.targetTouches[0]; if (!touch) return; const rect = dpadMain.getBoundingClientRect(); const x = touch.clientX - rect.left - dpadMain.clientWidth / 2; const y = touch.clientY - rect.top - dpadMain.clientHeight / 2; let up = 0, down = 0, left = 0, right = 0, angle = Math.atan(x / y) / (Math.PI / 180); if (y <= -10) { up = 1; } if (y >= 10) { down = 1; } if (x >= 10) { right = 1; left = 0; if (angle < 0 && angle >= -35 || angle > 0 && angle <= 35) { right = 0; } up = (angle < 0 && angle >= -55 ? 1 : 0); down = (angle > 0 && angle <= 55 ? 1 : 0); } if (x <= -10) { right = 0; left = 1; if (angle < 0 && angle >= -35 || angle > 0 && angle <= 35) { left = 0; } up = (angle > 0 && angle <= 55 ? 1 : 0); down = (angle < 0 && angle >= -55 ? 1 : 0); } dpadMain.classList.toggle("ejs_dpad_up_pressed", up); dpadMain.classList.toggle("ejs_dpad_down_pressed", down); dpadMain.classList.toggle("ejs_dpad_right_pressed", right); dpadMain.classList.toggle("ejs_dpad_left_pressed", left); callback(up, down, left, right); } const cancelCb = (e) => { e.preventDefault(); dpadMain.classList.remove("ejs_dpad_up_pressed"); dpadMain.classList.remove("ejs_dpad_down_pressed"); dpadMain.classList.remove("ejs_dpad_right_pressed"); dpadMain.classList.remove("ejs_dpad_left_pressed"); callback(0, 0, 0, 0); } this.addEventListener(dpadMain, 'touchstart touchmove', updateCb); this.addEventListener(dpadMain, 'touchend touchcancel', cancelCb); container.appendChild(dpadMain); } info.forEach((dpad, index) => { if (dpad.type !== 'dpad') return; if (leftHandedMode && ['left', 'right'].includes(dpad.location)) { dpad.location = (dpad.location==='left') ? 'right' : 'left'; const amnt = JSON.parse(JSON.stringify(dpad)); if (amnt.left) { dpad.right = amnt.left; } if (amnt.right) { dpad.left = amnt.right; } } const elem = this.createElement("div"); let style = ''; if (dpad.left) { style += 'left:'+dpad.left+';'; } if (dpad.right) { style += 'right:'+dpad.right+';'; } if (dpad.top) { style += 'top:'+dpad.top+';'; } elem.style = style; elems[dpad.location].appendChild(elem); createDPad({container: elem, event: (up, down, left, right) => { if (dpad.joystickInput) { if (up === 1) up=0x7fff; if (down === 1) up=0x7fff; if (left === 1) up=0x7fff; if (right === 1) up=0x7fff; } this.gameManager.simulateInput(0, dpad.inputValues[0], up); this.gameManager.simulateInput(0, dpad.inputValues[1], down); this.gameManager.simulateInput(0, dpad.inputValues[2], left); this.gameManager.simulateInput(0, dpad.inputValues[3], right); }}); }) info.forEach((zone, index) => { if (zone.type !== 'zone') return; if (leftHandedMode && ['left', 'right'].includes(zone.location)) { zone.location = (zone.location==='left') ? 'right' : 'left'; const amnt = JSON.parse(JSON.stringify(zone)); if (amnt.left) { zone.right = amnt.left; } if (amnt.right) { zone.left = amnt.right; } } const elem = this.createElement("div"); this.addEventListener(elem, "touchstart touchmove touchend touchcancel", (e) => { e.preventDefault(); }); elems[zone.location].appendChild(elem); const zoneObj = nipplejs.create({ 'zone': elem, 'mode': 'static', 'position': { 'left': zone.left, 'top': zone.top }, 'color': zone.color || 'red' }); zoneObj.on('end', () => { this.gameManager.simulateInput(0, zone.inputValues[0], 0); this.gameManager.simulateInput(0, zone.inputValues[1], 0); this.gameManager.simulateInput(0, zone.inputValues[2], 0); this.gameManager.simulateInput(0, zone.inputValues[3], 0); }); zoneObj.on('move', (e, info) => { const degree = info.angle.degree; const distance = info.distance; if (zone.joystickInput === true) { let x = 0, y = 0; if (degree > 0 && degree <= 45) { x = distance / 50; y = -0.022222222222222223 * degree * distance / 50; } if (degree > 45 && degree <= 90) { x = 0.022222222222222223 * (90 - degree) * distance / 50; y = -distance / 50; } if (degree > 90 && degree <= 135) { x = 0.022222222222222223 * (90 - degree) * distance / 50; y = -distance / 50; } if (degree > 135 && degree <= 180) { x = -distance / 50; y = -0.022222222222222223 * (180 - degree) * distance / 50; } if (degree > 135 && degree <= 225) { x = -distance / 50; y = -0.022222222222222223 * (180 - degree) * distance / 50; } if (degree > 225 && degree <= 270) { x = -0.022222222222222223 * (270 - degree) * distance / 50; y = distance / 50; } if (degree > 270 && degree <= 315) { x = -0.022222222222222223 * (270 - degree) * distance / 50; y = distance / 50; } if (degree > 315 && degree <= 359.9) { x = distance / 50; y = 0.022222222222222223 * (360 - degree) * distance / 50; } if (x > 0) { this.gameManager.simulateInput(0, zone.inputValues[0], 0x7fff * x); this.gameManager.simulateInput(0, zone.inputValues[1], 0); } else { this.gameManager.simulateInput(0, zone.inputValues[1], 0x7fff * -x); this.gameManager.simulateInput(0, zone.inputValues[0], 0); } if (y > 0) { this.gameManager.simulateInput(0, zone.inputValues[2], 0x7fff * y); this.gameManager.simulateInput(0, zone.inputValues[3], 0); } else { this.gameManager.simulateInput(0, zone.inputValues[3], 0x7fff * -y); this.gameManager.simulateInput(0, zone.inputValues[2], 0); } } else { if (degree >= 30 && degree < 150) { this.gameManager.simulateInput(0, zone.inputValues[0], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[0], 0); }, 30); } if (degree >= 210 && degree < 330) { this.gameManager.simulateInput(0, zone.inputValues[1], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[1], 0); }, 30); } if (degree >= 120 && degree < 240) { this.gameManager.simulateInput(0, zone.inputValues[2], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[2], 0); }, 30); } if (degree >= 300 || degree >= 0 && degree < 60) { this.gameManager.simulateInput(0, zone.inputValues[3], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[3], 0); }, 30); } } }); }) } handleResize() { if (!this.Module) return; const dpr = window.devicePixelRatio || 1; const positionInfo = this.game.getBoundingClientRect(); const width = positionInfo.width * dpr; const height = (positionInfo.height * dpr); this.Module.setCanvasSize(width, height); } getElementSize(element) { let elem = element.cloneNode(true); elem.style.position = 'absolute'; elem.style.opacity = 0; elem.removeAttribute('hidden'); element.parentNode.appendChild(elem); let width = elem.scrollWidth, height = elem.scrollHeight; elem.remove(); return { 'width': width, 'height': height }; } menuOptionChanged(option, value) { if (option === "shader") { try { this.Module.FS.unlink("/shader/shader.glslp"); } catch(e) {} if (value === "disabled") { this.gameManager.toggleShader(0); return; } this.Module.FS.writeFile("/shader/shader.glslp", window.EJS_SHADERS[value]); this.gameManager.toggleShader(1); return; } //console.log(option, value); this.gameManager.setVariable(option, value); } setupSettingsMenu() { this.settingsMenu = this.createElement("div"); this.addEventListener(this.settingsMenu, "click", (e) => { this.settingsJustClicked = true; }) this.settingsMenu.classList.add("ejs_settings_parent"); const nested = this.createElement("div"); nested.classList.add("ejs_settings_transition"); this.settings = {}; const home = this.createElement("div"); home.classList.add("ejs_setting_home"); home.classList.add("ejs_setting_menu"); nested.appendChild(home); const addToMenu = (title, id, options, defaultOption) => { const menuOption = this.createElement("div"); menuOption.classList.add("ejs_settings_main_bar"); const span = this.createElement("span"); span.innerText = title; const current = this.createElement("div"); current.innerText = ""; current.classList.add("ejs_settings_main_bar_selected"); span.appendChild(current); menuOption.appendChild(span); home.appendChild(menuOption); const menu = this.createElement("div"); menu.setAttribute("hidden", ""); const button = this.createElement("button"); const goToHome = () => { const homeSize = this.getElementSize(home); nested.style.width = homeSize.width + "px"; nested.style.height = homeSize.height + "px"; menu.setAttribute("hidden", ""); home.removeAttribute("hidden"); } this.addEventListener(menuOption, "click", (e) => { const targetSize = this.getElementSize(menu); nested.style.width = targetSize.width + "px"; nested.style.height = targetSize.height + "px"; menu.removeAttribute("hidden"); home.setAttribute("hidden", ""); }) this.addEventListener(button, "click", goToHome); button.type = "button"; button.classList.add("ejs_back_button"); menu.appendChild(button); const pageTitle = this.createElement("span"); pageTitle.innerText = title; pageTitle.classList.add("ejs_menu_text_a"); button.appendChild(pageTitle); const optionsMenu = this.createElement("div"); optionsMenu.classList.add("ejs_setting_menu"); optionsMenu.style["max-height"] = "385px"; optionsMenu.style.overflow = "auto"; let buttons = []; let opts = options; if (Array.isArray(options)) { opts = {}; for (let i=0; i { this.settings[id] = opt; for (let j=0; j { let option = line.split('; '); let name = option[0]; let options = option[1].split('|'), optionName = name.split("|")[0].replace(/_/g, ' ').replace(/.+\-(.+)/, '$1'); options.slice(1, -1); if (options.length === 1) return; let availableOptions = {}; for (let i=0; i 1) ? name.split("|")[1] : options[0].replace('(Default) ', '')); }) //addToMenu("Test", 'test', {a:1, b:2, c:3}, 2); //addToMenu("Test2", 'test_2', [4, 5, 6]); //addToMenu("Testertthgfd", 'booger', [7, 8, 9]); addToMenu(this.localization('FPS'), 'fps', { 'show': this.localization("show"), 'hide': this.localization("hide") }, 'hide'); if (window.EJS_SHADERS) { addToMenu(this.localization('Shaders'), 'shader', { 'disabled': "Disabled", '2xScaleHQ.glslp': "2xScaleHQ", '4xScaleHQ.glslp': "4xScaleHQ", 'crt-easymode.glslp': 'CRT easymode', 'crt-aperture.glslp': 'CRT aperture', 'crt-geom.glslp': 'CRT geom', 'crt-mattias.glslp': 'CRT mattias' }, 'disabled'); } this.settingsMenu.appendChild(nested); this.settingParent.appendChild(this.settingsMenu); this.settingParent.style.position = "relative"; const homeSize = this.getElementSize(home); nested.style.width = homeSize.width + "px"; nested.style.height = homeSize.height + "px"; this.settingsMenu.style.display = "none"; } createSubPopup(hidden) { const popup = this.createElement('div'); popup.classList.add("ejs_popup_container"); popup.classList.add("ejs_popup_container_box"); const popupMsg = this.createElement("div"); popupMsg.innerText = ""; if (hidden) popup.setAttribute("hidden", ""); popup.appendChild(popupMsg); return [popup, popupMsg]; } createCheatsMenu() { const body = this.createPopup("Cheats", { "Add Cheat": () => { const popups = this.createSubPopup(); this.cheatMenu.appendChild(popups[0]); popups[1].classList.add("ejs_cheat_parent"); popups[1].style.width = "100%"; const popup = popups[1]; const header = this.createElement("div"); header.classList.add("ejs_cheat_header"); const title = this.createElement("h2"); title.innerText = "Add Cheat Code"; title.classList.add("ejs_cheat_heading"); const close = this.createElement("button"); close.classList.add("ejs_cheat_close"); header.appendChild(title); header.appendChild(close); popup.appendChild(header); this.addEventListener(close, "click", (e) => { popups[0].remove(); }) const main = this.createElement("div"); main.classList.add("ejs_cheat_main"); const header3 = this.createElement("strong"); header3.innerText = "Code"; main.appendChild(header3); main.appendChild(this.createElement("br")); const mainText = this.createElement("textarea"); mainText.classList.add("ejs_cheat_code"); mainText.style.width = "100%"; mainText.style.height = "80px"; main.appendChild(mainText); main.appendChild(this.createElement("br")); const header2 = this.createElement("strong"); header2.innerText = "Description"; main.appendChild(header2); main.appendChild(this.createElement("br")); const mainText2 = this.createElement("input"); mainText2.type = "text"; mainText2.classList.add("ejs_cheat_code"); main.appendChild(mainText2); main.appendChild(this.createElement("br")); popup.appendChild(main); const footer = this.createElement("footer"); const submit = this.createElement("button"); const closeButton = this.createElement("button"); submit.innerText = "Submit"; closeButton.innerText = "Close"; submit.classList.add("ejs_button_button"); closeButton.classList.add("ejs_button_button"); submit.classList.add("ejs_popup_submit"); closeButton.classList.add("ejs_popup_submit"); submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)"; footer.appendChild(submit); const span = this.createElement("span"); span.innerText = " "; footer.appendChild(span); footer.appendChild(closeButton); popup.appendChild(footer); this.addEventListener(submit, "click", (e) => { if (!mainText.value.trim() || !mainText2.value.trim()) return; popups[0].remove(); this.cheats.push({ code: mainText.value, desc: mainText2.value, checked: false }); this.updateCheatUI(); }) this.addEventListener(closeButton, "click", (e) => { popups[0].remove(); }) }, "Close": () => { this.cheatMenu.style.display = "none"; } }, true); this.cheatMenu = body.parentElement; const rows = this.createElement("div"); body.appendChild(rows); rows.classList.add("ejs_cheat_rows"); this.elements.cheatRows = rows; } updateCheatUI() { if (!this.cheats) this.cheats = []; this.elements.cheatRows.innerHTML = ""; const addToMenu = (desc, checked, code, i) => { const row = this.createElement("div"); row.classList.add("ejs_cheat_row"); const input = this.createElement("input"); input.type = "checkbox"; input.checked = checked; input.value = i; input.id = "ejs_cheat_switch_"+i; row.appendChild(input); const label = this.createElement("label"); label.for = "ejs_cheat_switch_"+i; label.innerText = desc; row.appendChild(label); label.addEventListener("click", (e) => { input.checked = !input.checked; this.cheats[i].checked = input.checked; this.cheatChanged(input.checked, code, i); }) const close = this.createElement("a"); close.classList.add("ejs_cheat_row_button"); close.innerText = "×"; row.appendChild(close); this.elements.cheatRows.appendChild(row); this.cheatChanged(checked, code, i); } this.gameManager.resetCheat(); for (let i=0; i