mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-27 18:21:34 +11:00
1039 lines
34 KiB
HTML
1039 lines
34 KiB
HTML
|
<!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>
|