<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.2" /> <meta name="title" content="gba.ninja - Gameboy Advance emulator in the browser" /> <meta name="description" content="Run Gameboy Advance games in a web browser. Based on the VisualBoyAdvance-M emulator." /> <title>gba.ninja</title> <!-- Global site tag (gtag.js) - Google Analytics --> <script>function gtag() { }</script> <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag("js", new Date()); gtag("config", "UA-45495852-6"); </script> <script> if (navigator.serviceWorker) { navigator.serviceWorker.register("./sw.js", { scope: "./", }); navigator.serviceWorker.addEventListener("message", event => { var msg = event.data.msg; switch (msg.name) { default: console.log("unknown sw message", event); gtag("event", "unknown_sw_message_1", { name: msg.name, }); return; } }); } </script> <style> html, body { width: 100%; height: 100%; margin: 0px; font-family: sans-serif; background-color: #f3ddff; } canvas { right: 0px; left: 0px; bottom: 0px; top: 0px; position: absolute; width: 100%; height: 100%; } </style> <script id="2d-vertex-shader" type="x-shader/x-vertex"> attribute vec2 a_position; varying highp vec2 v_textureCoord; void main() { /* * This scales the quad so that the screen texture fits the viewport. * The texture is 256 * 256, but only 240 * 160 is used. The quad is 2*2, centered on (0,0) */ gl_Position = vec4((a_position.x * 2.0 * 1.0666) - 1.0, (a_position.y * 2.0 * 1.6) * -1.0 + 1.0, 0, 1); v_textureCoord = vec2(a_position.x, a_position.y); } </script> <script id="2d-fragment-shader" type="x-shader/x-fragment"> varying highp vec2 v_textureCoord; uniform sampler2D u_sampler; void main(void) { gl_FragColor = texture2D(u_sampler, vec2(v_textureCoord.s, v_textureCoord.t)); } </script> <script> window.onerror = function (messageOrEvent, source, lineno, colno, error) { try { var str = ""; if (typeof messageOrEvent === "object") { str += "Event: " + messageOrEvent.type + " " + messageOrEvent.message + " ;"; } else { str += messageOrEvent + "; "; } if (source) { str += " Source: " + source + "; "; } if (lineno !== void 0) { str += " Line: " + lineno + "; "; } if (colno !== void 0) { str += " Col: " + colno + "; "; } if (error) { str += " Message: " + error.message + "; "; try { str += " StackTop: " + error.stack.split(/\n/g)[1].trim() + "; "; } catch (e) { } } console.log("Remote logged: ", str); gtag("event", "exception", { description: str, }); } catch (e) { console.error(e); } }; </script> <script> "use strict"; try { void new Image("/logo.png"); } catch (e) { // Not sure why but on some browsers this crashes. } var qs = { autorun: "./thepurplenight.gba", exclusive: true, }; function escapeHtml(string) { var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', }; return string.replace(/[&<>"']/g, function (s) { return entityMap[s] || s; }); }; function unescapeHtml(string) { var reverseEntityMap = { "&": "&", "<": "<", ">": ">", '"': '"', ''': "'", }; return string.replace(/&.+?;/g, function (s) { return reverseEntityMap[s] || s; }); }; // Disable backspace navigation var backspaceHandler = function (e) { if (e.which === 8) { if (!/INPUT|SELECT|TEXTAREA/i.test(e.target.tagName) || e.target.disabled || e.target.readOnly) { e.preventDefault(); } } }.bind(this); document.addEventListener("keydown", backspaceHandler); document.addEventListener("keypress", backspaceHandler); // Shim performance.now window.performance = window.performance || {}; performance.now = (function () { return performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow || function () { return new Date().getTime(); }; })(); // Shim localstorage if (!window.localStorage) { window.localStorage = {}; } try { localStorage._ = 1; } catch (e) { window.isShittyLocalstorage = true; } var readyHandlers = {}; var onReady = function (what, fn) { if (readyHandlers[what] === null) { setTimeout(function () { fn(); }); return; } if (readyHandlers[what] === void 0) { readyHandlers[what] = []; } readyHandlers[what].push(fn); }; var triggerReady = function (what) { if (readyHandlers[what]) { readyHandlers[what].forEach(function (v) { v(); }); } readyHandlers[what] = null; }; function isPowerOf2(x) { return (x != 0) && ((x & (x - 1)) == 0); } var modalEls; var modalRefcount = 0; function modal(text, options) { modalRefcount++; modalEls = modalEls || { modal: document.querySelector(".modal"), modalTitle: document.querySelector(".modal-title"), modalTitleText: document.querySelector(".modal-title").childNodes[0], modalText: document.querySelector(".modal-text"), modalTextText: document.querySelector(".modal-text").childNodes[0], modalLeftButton: document.querySelector(".modal-button-left"), modalLeftButtonText: document.querySelector(".modal-button-left").childNodes[0], modalRightButton: document.querySelector(".modal-button-right"), modalRightButtonText: document.querySelector(".modal-button-right").childNodes[0], modalProgress: document.querySelector(".modal-progress"), modalInput: document.querySelector(".modal-input"), }; var removeEvents; function hideModal() { modalRefcount--; if (modalRefcount <= 0) { modalRefcount = 0; document.body.style.overflow = ""; } modalEls.modal.style.display = "none"; removeEvents(); } function setProgress(n) { modalEls.modalProgress.style.display = "block"; modalEls.modalProgress.style.width = n + "%"; } function getInputValue() { return modalEls.modalInput.value; } options = options || {}; options = { title: options.title || null, text: text, leftButtonText: options.leftButtonText || "OK", leftButtonFn: options.hasOwnProperty("leftButtonFn") ? options.leftButtonFn : function () { }, rightButtonText: options.rightButtonText || "OK", rightButtonFn: options.rightButtonFn || null, input: typeof options.input === "string" ? options.input : null, }; modalEls.modal.style.display = "block"; document.body.style.overflow = "hidden"; window.scrollTo(0, 0); if (options.title) { modalEls.modalTitle.style.display = "block"; modalEls.modalTitleText.textContent = options.title; } else { modalEls.modalTitle.style.display = "none"; } modalEls.modalTextText.textContent = options.text; modalEls.modalLeftButtonText.textContent = options.leftButtonText; modalEls.modalRightButtonText.textContent = options.rightButtonText; if (options.leftButtonFn) { modalEls.modalLeftButton.style.display = ""; } else { modalEls.modalLeftButton.style.display = "none"; } if (options.rightButtonFn) { modalEls.modalRightButton.style.display = ""; } else { modalEls.modalRightButton.style.display = "none"; } modalEls.modalProgress.style.display = "none"; modalEls.modalInput.value = ""; if (typeof options.input === "string") { modalEls.modalInput.style.display = ""; } else { modalEls.modalInput.style.display = "none"; } var leftHandler = function () { if (!options.leftButtonFn || options.leftButtonFn() !== false) { hideModal(); } }; modalEls.modalLeftButton.addEventListener("click", leftHandler); var rightHandler = function () { if (!options.rightButtonFn || options.rightButtonFn() !== false) { hideModal(); } }; modalEls.modalRightButton.addEventListener("click", rightHandler); removeEvents = function () { modalEls.modalLeftButton.removeEventListener("click", leftHandler); modalEls.modalRightButton.removeEventListener("click", rightHandler); }; return { hideModal: hideModal, setProgress: setProgress, getInputValue: getInputValue, }; } function stringToCharCodes(str) { return str.split("").map(function (c) { return c.charCodeAt(0); }); } var emuReady = false; function hasEmuModule() { return !!emuReady; } var romBuffer8 = null; window.loadRomFromBuffer = function (_romBuffer8, filename) { var errorOpts = { title: "Error" }; if (_romBuffer8.length < 512) { gtag("event", "load_tiny_rom_1", { event_label: filename, }); return modal("That file isn't a GBA ROM. (It's too small to be a ROM.)", errorOpts); } // Check if it's a real rom var romCode = String.fromCharCode( _romBuffer8[0xAC], _romBuffer8[0xAD], _romBuffer8[0xAE], _romBuffer8[0xAF] ); var gbMagic = [ _romBuffer8[0x0104], _romBuffer8[0x0105], _romBuffer8[0x0106], _romBuffer8[0x0107], _romBuffer8[0x0108], _romBuffer8[0x0109], _romBuffer8[0x010A], _romBuffer8[0x010B], ].map(function (v) { return v.toString(16); }).join(); if (filename.search(/\.zip$/i) !== -1) { gtag("event", "load_zip_rom_1", { event_label: filename, }); return modal("You need to extract the rom file from the zip.", errorOpts); } if (String.fromCharCode(_romBuffer8[0], _romBuffer8[1]) === "PK") { gtag("event", "load_zip_rom_1", { event_label: filename + " (non-dot-zip-file)", }); return modal("You need to extract the rom file.", errorOpts); } if (filename.search(/\.sav$/i) !== -1) { gtag("event", "load_sav_rom_1", { event_label: filename, }); return modal("That's not a ROM, it's a savegame file. GBA ROM files usually end in '.gba'.", errorOpts); } if (filename.search(/\.smc$/i) !== -1 || filename.search(/\.sfc$/i) !== -1) { gtag("event", "load_smc_rom_1", { event_label: filename, }); return modal("That's a SNES ROM, this emulator runs Gameboy Advance ROMs.", errorOpts); } if (gbMagic === "ce,ed,66,66,cc,d,0,b") { gtag("event", "load_gb_rom_1", { event_label: filename, }); var colorMaybe = ""; if (filename.search(/\.gbc$/i) !== -1) { colorMaybe = "Color "; } return modal("That's a Gameboy " + colorMaybe + "ROM, this emulator only runs Gameboy Advance ROMs.", errorOpts); } if (!isPowerOf2(_romBuffer8.length)) { // Some roms are actually non-pot, so don't enforce this. gtag("event", "non_pot_rom_1", { event_label: stringToCharCodes(romCode) + " " + filename + " " + _romBuffer8.length, }); // Don't return } function ok() { romBuffer8 = _romBuffer8; triggerReady("cartridge"); } function waitForEmuLoad() { if (hasEmuModule()) { ok(); } else { gtag("event", "emu_file_missing_at_load_rom_1", {}); var interval; var modalOpts = modal("The emulator module isn't loaded yet. Give it a moment.", { title: "Waiting For Emulator Module", leftButtonText: "Back", leftButtonFn: function () { clearInterval(interval); }, }); function progress() { if (window.emuScriptProgress === -1) { modalOpts.hideModal(); return modal("There was an error while loading the emulator module. You'll need to refresh the page.", { title: "Error", leftButtonText: "Ok", leftButtonFn: function () { clearInterval(interval); }, }); } else { modalOpts.setProgress(window.emuScriptProgress); } } progress(); interval = setInterval(function () { progress(); if (window.emuScriptProgress >= 100) { clearInterval(interval); modalOpts.hideModal(); ok(); } }, 100); } } if (romCode.search(/^[A-Z0-9]{4}$/) && !qs.ignoreInvalidRomCode) { gtag("event", "invalid_rom_code_1", { event_label: stringToCharCodes(romCode) + " " + filename, }); return modal("That file doesn't look like a GBA ROM. (Couldn't find a rom code in the file.)", { title: "Error", rightButtonText: "Run it anyway", rightButtonFn: waitForEmuLoad, }); } else { waitForEmuLoad(); } }; window.loadRomFromFile = function (e) { var binaryFile = e.currentTarget.files[0]; var filename = binaryFile.name; e.currentTarget.form.reset(); if (!binaryFile) { return; } var modalOpts = modal(filename, { title: "Loading File", leftButtonFn: null, }); var fr = new FileReader(); fr.readAsArrayBuffer(binaryFile); fr.onload = function () { modalOpts.hideModal(); var _romBuffer8 = new Uint8Array(fr.result); loadRomFromBuffer(_romBuffer8, binaryFile.name); }; }; window.loadRomFromNetwork = function (url) { var xhr = new XMLHttpRequest(); let loadingModalSettings = { title: "Loading", leftButtonText: "Cancel", leftButtonFn: function () { xhr.abort(); } }; if (qs.exclusive) { loadingModalSettings.leftButtonFn = null; } var modalOpts = modal("Loading " + url, loadingModalSettings); modalOpts.setProgress(0); xhr.onload = function (e) { modalOpts.hideModal(); window.loadRomFromBuffer(new Uint8Array(xhr.response), url); }; xhr.onprogress = function (e) { modalOpts.setProgress((e.loaded / e.total) * 100); }; xhr.onerror = function (e) { modalOpts.hideModal(); let errorModalSettings = { title: "Error", leftButtonText: "Ok", }; if (qs.exclusive) { errorModalSettings.leftButtonFn = function () { location.reload(); } errorModalSettings.leftButtonText = "Reload Page"; } modal("There was an error loading the ROM.", errorModalSettings); }; xhr.open("GET", url); xhr.responseType = "arraybuffer"; xhr.send(); }; window.gbaninja = { onRuntimeInitialized: function () { triggerReady("emu"); }, }; document.addEventListener("DOMContentLoaded", function () { triggerReady("document"); }); document.addEventListener("mousedown", function () { if (window.vbaSound.audioCtx.state === "suspended") { window.vbaSound.audioCtx.resume(); } }); onReady("document", function () { if (window.init) { window.init(); } else { document.querySelector(".pixels").innerHTML = "<p style='margin: 20px;'>A required file failed to load.</p>"; } if (qs.autorun) { loadRomFromNetwork(qs.autorun); } }); onReady("emu", function () { emuReady = true; onReady("document", function () { onReady("cartridge", function () { window.start(); }); }); }); // ------ VBA ENTRY POINTS ------- var VBAInterface = {}; VBAInterface.VBA_get_emulating = function () { return gbaninja.ccall("VBA_get_emulating", "int", [], []); }; VBAInterface.VBA_start = function () { return gbaninja.ccall("VBA_start", "int", [], []); }; VBAInterface.VBA_do_cycles = function (cycles) { return gbaninja.ccall("VBA_do_cycles", "int", ["int"], [cycles]); }; VBAInterface.VBA_stop = function () { return gbaninja.ccall("VBA_stop", "int", [], []); }; VBAInterface.VBA_get_bios = function () { return gbaninja.ccall("VBA_get_bios", "int", [], []); }; VBAInterface.VBA_get_rom = function () { return gbaninja.ccall("VBA_get_rom", "int", [], []); }; VBAInterface.VBA_get_internalRAM = function () { return gbaninja.ccall("VBA_get_internalRAM", "int", [], []); }; VBAInterface.VBA_get_workRAM = function () { return gbaninja.ccall("VBA_get_workRAM", "int", [], []); }; VBAInterface.VBA_get_paletteRAM = function () { return gbaninja.ccall("VBA_get_paletteRAM", "int", [], []); }; VBAInterface.VBA_get_vram = function () { return gbaninja.ccall("VBA_get_vram", "int", [], []); }; VBAInterface.VBA_get_pix = function () { return gbaninja.ccall("VBA_get_pix", "int", [], []); }; VBAInterface.VBA_get_oam = function () { return gbaninja.ccall("VBA_get_oam", "int", [], []); }; VBAInterface.VBA_get_ioMem = function () { return gbaninja.ccall("VBA_get_ioMem", "int", [], []); }; VBAInterface.VBA_get_systemColorMap16 = function () { return gbaninja.ccall("VBA_get_systemColorMap16", "int", [], []); }; VBAInterface.VBA_get_systemColorMap32 = function () { return gbaninja.ccall("VBA_get_systemColorMap32", "int", [], []); }; VBAInterface.VBA_get_systemFrameSkip = function () { return gbaninja.ccall("VBA_get_systemFrameSkip", "int", [], []); }; VBAInterface.VBA_set_systemFrameSkip = function (n) { return gbaninja.ccall("VBA_set_systemFrameSkip", "int", ["int"], [n]); }; VBAInterface.VBA_get_systemSaveUpdateCounter = function () { return gbaninja.ccall("VBA_get_systemSaveUpdateCounter", "int", [], []); }; VBAInterface.VBA_reset_systemSaveUpdateCounter = function () { return gbaninja.ccall("VBA_reset_systemSaveUpdateCounter", "int", [], []); }; VBAInterface.VBA_emuWriteBattery = function () { return gbaninja.ccall("VBA_emuWriteBattery", "int", [], []); }; VBAInterface.VBA_agbPrintFlush = function () { return gbaninja.ccall("VBA_agbPrintFlush", "int", [], []); }; // ------- VBA EXIT POINTS -------- VBAInterface.NYI = function (feature) { console.log("Feature is NYI: ", feature); }; VBAInterface.getAudioSampleRate = function () { return window.vbaSound.getSampleRate(); }; VBAInterface.getRomSize = function (startPointer8) { return romBuffer8.byteLength; }; VBAInterface.copyRomToMemory = function (startPointer8) { var gbaHeap8 = gbaninja.HEAP8; var byteLength = romBuffer8.byteLength; for (var i = 0; i < byteLength; i++) { gbaHeap8[startPointer8 + i] = romBuffer8[i]; } }; VBAInterface.renderFrame = function (pixPointer8) { window.vbaGraphics.drawGBAFrame(pixPointer8); }; VBAInterface.initSound = function () { }; VBAInterface.pauseSound = function () { }; VBAInterface.resetSound = function () { window.vbaSound.resetSound(); }; VBAInterface.resumeSound = function () { }; VBAInterface.writeSound = function (pointer8, length16) { return window.vbaSound.writeSound(pointer8, length16); }; VBAInterface.setThrottleSound = function (pointer8, length16) { }; VBAInterface.getSaveSize = function () { return vbaSaves.getSaveSize(); }; VBAInterface.commitFlash = VBAInterface.commitEeprom = function (pointer8, size) { return vbaSaves.softCommit(pointer8, size); }; VBAInterface.restoreSaveMemory = function (pointer8, targetBufferSize) { return vbaSaves.restoreSaveMemory(pointer8, targetBufferSize); }; VBAInterface.getJoypad = function (joypadNum) { return vbaInput.getJoypad(joypadNum); }; VBAInterface.dbgOutput = function (textPointer8, unknownPointer8) { return console.log("dbgOutput", textPointer8, unknownPointer8); }; function ajaxScript(url, progressCallback) { var xhr = new XMLHttpRequest(); xhr.onload = function (e) { var script = document.createElement('script'); script.text = xhr.responseText; document.head.appendChild(script); }; xhr.onprogress = function (e) { progressCallback((e.loaded / e.total) * 100); }; xhr.onerror = function () { progressCallback(-1); gtag("event", "emu_load_error", {}); }; xhr.open("GET", url); xhr.responseType = "text"; xhr.send(); } window.emuScriptProgress = 0; ajaxScript("./emu.js", function (progress, text) { window.emuScriptProgress = progress; }); </script> <script src="./app.js"></script> </head> <body> <style> .ui { color: #4d2990; } .ui-border-1 { float: left; border: transparent 20px solid; } .ui-border-2 { padding: 20px; border: 13px solid #aa17fe; background-color: white; float: left; border-left-width: 5px; border-right-width: 5px; position: relative; } section { margin: 23px 0px; } h2 { margin: 0; font-size: 14px; font-style: italic; text-decoration: underline; } table { margin: 0; } p, label { margin: 0; font-size: 14px; } .gap { margin-top: 10px; } input[type=file] { position: absolute; left: -1000000px; } #load-rom-from-url { height: 23px; border-radius: 3px; border: solid 2px black; margin-left: 16px; padding: 3px 13px; } .btn { display: inline-block; border: 1px #9b69b7 solid; border-width: 4px 2px; padding: 5px 13px; background-color: #3a3a3a; color: #ece6ff; font-weight: 600; font-size: 13px; min-width: 60px; text-align: center; } .btn:hover { cursor: pointer; border-color: #b09cbb; background-color: #7a7994; } .empty-table { font-size: 11px; } table { border-collapse: collapse; font-size: 11px; } td { padding: 3px 11px; border: 2px solid #aa17fe; border-top: none; border-bottom: none; } a { color: #a89be8; } .report-bug-button { font-size: 11px; } .modal { display: none; } .modal-background { position: absolute; top: 0; bottom: 0; left: 0; right: 0; opacity: 0.8; background-color: black; } .modal-body { width: 400px; margin: 0 auto; position: absolute; border: 10px solid #aa17fe; background-color: white; border-left-width: 3px; border-right-width: 3px; left: 50%; margin-left: -218px; /* =(400 + 2*15 + 2*3) / 2 */ margin-top: 60px; padding: 15px; } .modal-title { font-size: 16px; } .modal-buttons { text-align: center; } .modal-button-left, .modal-button-right { margin: 6px 4px 3px 4px; } .modal-progress { height: 4px; margin-bottom: 10px; background-color: #9b69b7; } .modal-input { width: 100%; padding: 6px; box-sizing: border-box; margin: 10px 0; border: 2px solid #9b69b7; border-radius: 5px; font-size: 15px; color: #4d2990; } .perf { position: fixed; bottom: 0; color: white; padding: 4px; margin: 4px; background-color: rgba(0, 0, 0, 0.5); } .perf-left { display: inline-block; width: 200px; } </style> <div class="pixels" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;"></div> <div style="display: none;" class="ui"> <div class="ui-border-1"> <div class="ui-border-2"> <img src="./logo.png" style="height: 50px; padding-bottom: 6px; padding-right: 90px;" /> <section class="load-rom-section"> <h2>Load a Gameboy Advance ROM</h2> <div class="gap"></div> <label class="btn" for="load-rom-from-file">From File</label> <div class="gap"></div> <form> <input id="load-rom-from-file" type="file" onchange="window.loadRomFromFile(event);" /> </form> </section> <section class="paused-section" style="display: none;"> <h2>Paused</h2> <p style="padding-top: 8px;"> Press <span class="unpause-key-prompt"></span> to resume. </p> </section> <!-- <div> <label class="btn" for="load-rom-from-url" onclick="window.loadCartridgeFromURL(event);window.onReady('cartridge', function () {start()});">From URL</label> <input id="load-rom-from-url" type="text"/> </div> --> <section class="savegames-section"> <h2>Saves</h2> <div class="gap"></div> <div class="saves-list"></div> <div class="gap"></div> <label class="btn" for="import-save-file">Import Save File</label> <form> <input id="import-save-file" type="file" onchange="vbaSaves.onFileImportInputChanged(event, window.vbaUI.reset.bind(vbaUI));" /> </form> </section> <section> <h2>Keyboard Bindings</h2> <div class="gap"></div> <div class="keyboard-bindings"></div> <div class="gap"></div> <button class="btn reset-bindings-button" onclick="window.vbaUI.resetBindings();">Reset Bindings</button> </section> <div style="position: absolute; right: 10px; bottom: 10px;"> <a class="report-bug-button" target="_blank" href="https://github.com/simon-paris/gba.ninja/blob/master/embed.md">embed gba.ninja</a> <a class="report-bug-button" target="_blank" href="https://github.com/simon-paris/gba.ninja/issues">report a bug</a> </div> </div> </div> </div> <div class="modal"> <div class="modal-background"></div> <div class="modal-body"> <h2 class="modal-title">Title</h2> <div class="gap"></div> <p class="modal-text">Text</p> <div class="gap"></div> <div class="modal-progress"></div> <input class="modal-input"></input> <div class="modal-buttons"> <div class="btn modal-button-left">Left</div> <div class="btn modal-button-right">Right</div> </div> </div> </div> <div class="toast"> </div> <div class="perf" style="display: none;"> <span class="perf-left">Game</span><span class="perf-right perf-game">-</span><br /> <span class="perf-left">Speed</span><span class="perf-right perf-percentage">-</span><br /> <span class="perf-left">On-Time Renders</span><span class="perf-right perf-render-deadlines">-</span><br /> <span class="perf-left">On-Time Audio Events</span><span class="perf-right perf-audio-deadlines">-</span><br /> <span class="perf-left">Timesteps/Second</span><span class="perf-right perf-timesteps">-</span><br /> <span class="perf-left">Audio Lag</span><span class="perf-right perf-audio-lag">-</span><br /> </div> </body> <script> window.addEventListener("keydown", (e) => { if ([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) { e.preventDefault(); } }, false); </script> </html>