commit before switching this from static site to app
This commit is contained in:
BIN
bios/3do/goldstar.bin
Normal file
BIN
bios/3do/goldstar.bin
Normal file
Binary file not shown.
BIN
bios/3do/panafz1.bin
Normal file
BIN
bios/3do/panafz1.bin
Normal file
Binary file not shown.
BIN
bios/3do/panafz10.bin
Normal file
BIN
bios/3do/panafz10.bin
Normal file
Binary file not shown.
BIN
bios/amiga/kick34005.A500
Normal file
BIN
bios/amiga/kick34005.A500
Normal file
Binary file not shown.
BIN
bios/amiga/kick40063.A600
Normal file
BIN
bios/amiga/kick40063.A600
Normal file
Binary file not shown.
BIN
bios/amiga/kick40068.A1200
Normal file
BIN
bios/amiga/kick40068.A1200
Normal file
Binary file not shown.
BIN
bios/amiga/kick40068.A4000
Normal file
BIN
bios/amiga/kick40068.A4000
Normal file
Binary file not shown.
BIN
bios/arcade/neogeo.zip
Normal file
BIN
bios/arcade/neogeo.zip
Normal file
Binary file not shown.
BIN
bios/colecovision/coleco.rom
Normal file
BIN
bios/colecovision/coleco.rom
Normal file
Binary file not shown.
BIN
bios/dc/dc_boot.bin
Normal file
BIN
bios/dc/dc_boot.bin
Normal file
Binary file not shown.
BIN
bios/dc/dc_flash.bin
Normal file
BIN
bios/dc/dc_flash.bin
Normal file
Binary file not shown.
BIN
bios/fds/disksys.rom
Normal file
BIN
bios/fds/disksys.rom
Normal file
Binary file not shown.
BIN
bios/gba/gba_bios.bin
Normal file
BIN
bios/gba/gba_bios.bin
Normal file
Binary file not shown.
BIN
bios/msx/MSX2P.ROM
Normal file
BIN
bios/msx/MSX2P.ROM
Normal file
Binary file not shown.
BIN
bios/msx/MSX2PEXT.ROM
Normal file
BIN
bios/msx/MSX2PEXT.ROM
Normal file
Binary file not shown.
BIN
bios/msx/msx.rom
Normal file
BIN
bios/msx/msx.rom
Normal file
Binary file not shown.
BIN
bios/msx/msx2.rom
Normal file
BIN
bios/msx/msx2.rom
Normal file
Binary file not shown.
BIN
bios/msx/msx2ext.rom
Normal file
BIN
bios/msx/msx2ext.rom
Normal file
Binary file not shown.
BIN
bios/nds/firmware.bin
Normal file
BIN
bios/nds/firmware.bin
Normal file
Binary file not shown.
BIN
bios/neogeoaes/neogeo.zip
Normal file
BIN
bios/neogeoaes/neogeo.zip
Normal file
Binary file not shown.
BIN
bios/neogeomvs/neogeo.zip
Normal file
BIN
bios/neogeomvs/neogeo.zip
Normal file
Binary file not shown.
BIN
bios/nintendo-dsi/firmware.bin
Normal file
BIN
bios/nintendo-dsi/firmware.bin
Normal file
Binary file not shown.
BIN
bios/odyssey-2/c52.bin
Normal file
BIN
bios/odyssey-2/c52.bin
Normal file
Binary file not shown.
BIN
bios/odyssey-2/g7400.bin
Normal file
BIN
bios/odyssey-2/g7400.bin
Normal file
Binary file not shown.
BIN
bios/odyssey-2/jopac.bin
Normal file
BIN
bios/odyssey-2/jopac.bin
Normal file
Binary file not shown.
BIN
bios/odyssey-2/o2rom.bin
Normal file
BIN
bios/odyssey-2/o2rom.bin
Normal file
Binary file not shown.
BIN
bios/psp/ppge_atlas.zim
Normal file
BIN
bios/psp/ppge_atlas.zim
Normal file
Binary file not shown.
BIN
bios/psx/scph1001.bin
Normal file
BIN
bios/psx/scph1001.bin
Normal file
Binary file not shown.
BIN
bios/psx/scph5500.bin
Normal file
BIN
bios/psx/scph5500.bin
Normal file
Binary file not shown.
BIN
bios/psx/scph5501.bin
Normal file
BIN
bios/psx/scph5501.bin
Normal file
Binary file not shown.
BIN
bios/psx/scph5502.bin
Normal file
BIN
bios/psx/scph5502.bin
Normal file
Binary file not shown.
BIN
bios/psx/scph7001.bin
Normal file
BIN
bios/psx/scph7001.bin
Normal file
Binary file not shown.
BIN
bios/saturn/mpr-17933.bin
Normal file
BIN
bios/saturn/mpr-17933.bin
Normal file
Binary file not shown.
BIN
bios/saturn/sega_101.bin
Normal file
BIN
bios/saturn/sega_101.bin
Normal file
Binary file not shown.
BIN
bios/segacd/bios_CD_E.bin
Normal file
BIN
bios/segacd/bios_CD_E.bin
Normal file
Binary file not shown.
BIN
bios/segacd/bios_CD_J.bin
Normal file
BIN
bios/segacd/bios_CD_J.bin
Normal file
Binary file not shown.
BIN
bios/segacd/bios_CD_U.bin
Normal file
BIN
bios/segacd/bios_CD_U.bin
Normal file
Binary file not shown.
BIN
bios/turbografx-cd/syscard1.pce
Normal file
BIN
bios/turbografx-cd/syscard1.pce
Normal file
Binary file not shown.
BIN
bios/turbografx-cd/syscard2.pce
Normal file
BIN
bios/turbografx-cd/syscard2.pce
Normal file
Binary file not shown.
BIN
bios/turbografx-cd/syscard3.pce
Normal file
BIN
bios/turbografx-cd/syscard3.pce
Normal file
Binary file not shown.
BIN
client_history.txt
Normal file
BIN
client_history.txt
Normal file
Binary file not shown.
BIN
cores.json
Normal file
BIN
cores.json
Normal file
Binary file not shown.
BIN
cores2.json
Normal file
BIN
cores2.json
Normal file
Binary file not shown.
BIN
cores_list.json
Normal file
BIN
cores_list.json
Normal file
Binary file not shown.
1
emulator.js
Normal file
1
emulator.js
Normal file
File diff suppressed because one or more lines are too long
@@ -4,6 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>RomM Web UI</title>
|
<title>RomM Web UI</title>
|
||||||
|
<!-- Cross-Origin-Isolation Worker: Unlocks SharedArrayBuffer multi-threading without stripping external cover art proxy streams! -->
|
||||||
|
<script src="/coi-serviceworker.min.js"></script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"/>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
0
latest_roms.json
Normal file
0
latest_roms.json
Normal file
170
loader.js
Normal file
170
loader.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
(async function() {
|
||||||
|
const scripts = [
|
||||||
|
"emulator.js",
|
||||||
|
"nipplejs.js",
|
||||||
|
"shaders.js",
|
||||||
|
"storage.js",
|
||||||
|
"gamepad.js",
|
||||||
|
"GameManager.js",
|
||||||
|
"socket.io.min.js",
|
||||||
|
"compression.js"
|
||||||
|
];
|
||||||
|
|
||||||
|
const folderPath = (path) => path.substring(0, path.length - path.split("/").pop().length);
|
||||||
|
let scriptPath = (typeof window.EJS_pathtodata === "string") ? window.EJS_pathtodata : folderPath((new URL(document.currentScript.src)).pathname);
|
||||||
|
if (!scriptPath.endsWith("/")) scriptPath += "/";
|
||||||
|
//console.log(scriptPath);
|
||||||
|
function loadScript(file) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
let script = document.createElement("script");
|
||||||
|
script.src = function() {
|
||||||
|
if ("undefined" != typeof EJS_paths && typeof EJS_paths[file] === "string") {
|
||||||
|
return EJS_paths[file];
|
||||||
|
} else if (file.endsWith("emulator.min.js")) {
|
||||||
|
return scriptPath + file;
|
||||||
|
} else {
|
||||||
|
return scriptPath + "src/" + file;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = () => {
|
||||||
|
filesmissing(file).then(e => resolve());
|
||||||
|
}
|
||||||
|
document.head.appendChild(script);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStyle(file) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
let css = document.createElement("link");
|
||||||
|
css.rel = "stylesheet";
|
||||||
|
css.href = function() {
|
||||||
|
if ("undefined" != typeof EJS_paths && typeof EJS_paths[file] === "string") {
|
||||||
|
return EJS_paths[file];
|
||||||
|
} else {
|
||||||
|
return scriptPath + file;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
css.onload = resolve;
|
||||||
|
css.onerror = () => {
|
||||||
|
filesmissing(file).then(e => resolve());
|
||||||
|
}
|
||||||
|
document.head.appendChild(css);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filesmissing(file) {
|
||||||
|
console.error("Failed to load " + file);
|
||||||
|
let minifiedFailed = file.includes(".min.") && !file.includes("socket");
|
||||||
|
console[minifiedFailed ? "warn" : "error"]("Failed to load " + file + " beacuse it's likly that the minified files are missing.\nTo fix this you have 3 options:\n1. You can download the zip from the latest release here: https://github.com/EmulatorJS/EmulatorJS/releases/latest - Stable\n2. You can download the zip from here: https://cdn.emulatorjs.org/latest/data/emulator.min.zip and extract it to the data/ folder. (easiest option) - Beta\n3. You can build the files by running `npm i && npm run build` in the data/minify folder. (hardest option) - Beta\nNote: you will probably need to do the same for the cores, extract them to the data/cores/ folder.");
|
||||||
|
if (minifiedFailed) {
|
||||||
|
console.log("Attempting to load non-minified files");
|
||||||
|
if (file === "emulator.min.js") {
|
||||||
|
for (let i = 0; i < scripts.length; i++) {
|
||||||
|
await loadScript(scripts[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await loadStyle("emulator.css");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (("undefined" != typeof EJS_DEBUG_XX && true === EJS_DEBUG_XX)) {
|
||||||
|
for (let i = 0; i < scripts.length; i++) {
|
||||||
|
await loadScript(scripts[i]);
|
||||||
|
}
|
||||||
|
await loadStyle("emulator.css");
|
||||||
|
} else {
|
||||||
|
await loadScript("emulator.min.js");
|
||||||
|
await loadStyle("emulator.min.css");
|
||||||
|
}
|
||||||
|
const config = {};
|
||||||
|
config.gameUrl = window.EJS_gameUrl;
|
||||||
|
config.dataPath = scriptPath;
|
||||||
|
config.system = window.EJS_core;
|
||||||
|
config.biosUrl = window.EJS_biosUrl;
|
||||||
|
config.gameName = window.EJS_gameName;
|
||||||
|
config.color = window.EJS_color;
|
||||||
|
config.adUrl = window.EJS_AdUrl;
|
||||||
|
config.adMode = window.EJS_AdMode;
|
||||||
|
config.adTimer = window.EJS_AdTimer;
|
||||||
|
config.adSize = window.EJS_AdSize;
|
||||||
|
config.alignStartButton = window.EJS_alignStartButton;
|
||||||
|
config.VirtualGamepadSettings = window.EJS_VirtualGamepadSettings;
|
||||||
|
config.buttonOpts = window.EJS_Buttons;
|
||||||
|
config.volume = window.EJS_volume;
|
||||||
|
config.defaultControllers = window.EJS_defaultControls;
|
||||||
|
config.startOnLoad = window.EJS_startOnLoaded;
|
||||||
|
config.fullscreenOnLoad = window.EJS_fullscreenOnLoaded;
|
||||||
|
config.filePaths = window.EJS_paths;
|
||||||
|
config.loadState = window.EJS_loadStateURL;
|
||||||
|
config.cacheLimit = window.EJS_CacheLimit;
|
||||||
|
config.cheats = window.EJS_cheats;
|
||||||
|
config.defaultOptions = window.EJS_defaultOptions;
|
||||||
|
config.gamePatchUrl = window.EJS_gamePatchUrl;
|
||||||
|
config.gameParentUrl = window.EJS_gameParentUrl;
|
||||||
|
config.netplayUrl = window.EJS_netplayServer;
|
||||||
|
config.gameId = window.EJS_gameID;
|
||||||
|
config.backgroundImg = window.EJS_backgroundImage;
|
||||||
|
config.backgroundBlur = window.EJS_backgroundBlur;
|
||||||
|
config.backgroundColor = window.EJS_backgroundColor;
|
||||||
|
config.controlScheme = window.EJS_controlScheme;
|
||||||
|
config.threads = window.EJS_threads;
|
||||||
|
config.disableCue = window.EJS_disableCue;
|
||||||
|
config.startBtnName = window.EJS_startButtonName;
|
||||||
|
config.softLoad = window.EJS_softLoad;
|
||||||
|
config.capture = window.EJS_screenCapture;
|
||||||
|
config.externalFiles = window.EJS_externalFiles;
|
||||||
|
config.dontExtractBIOS = window.EJS_dontExtractBIOS;
|
||||||
|
config.disableDatabases = window.EJS_disableDatabases;
|
||||||
|
config.disableLocalStorage = window.EJS_disableLocalStorage;
|
||||||
|
config.forceLegacyCores = window.EJS_forceLegacyCores;
|
||||||
|
config.noAutoFocus = window.EJS_noAutoFocus;
|
||||||
|
config.videoRotation = window.EJS_videoRotation;
|
||||||
|
config.hideSettings = window.EJS_hideSettings;
|
||||||
|
config.shaders = Object.assign({}, window.EJS_SHADERS, window.EJS_shaders ? window.EJS_shaders : {});
|
||||||
|
|
||||||
|
let systemLang;
|
||||||
|
try {
|
||||||
|
systemLang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
|
} catch(e) {} //Ignore
|
||||||
|
if ((typeof window.EJS_language === "string" && window.EJS_language !== "en-US") || (systemLang && window.EJS_disableAutoLang !== false)) {
|
||||||
|
const language = window.EJS_language || systemLang;
|
||||||
|
try {
|
||||||
|
let path;
|
||||||
|
console.log("Loading language", language);
|
||||||
|
if ("undefined" != typeof EJS_paths && typeof EJS_paths[language] === "string") {
|
||||||
|
path = EJS_paths[language];
|
||||||
|
} else {
|
||||||
|
path = scriptPath + "localization/" + language + ".json";
|
||||||
|
}
|
||||||
|
config.language = language;
|
||||||
|
config.langJson = JSON.parse(await (await fetch(path)).text());
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Missing language", language, "!!");
|
||||||
|
delete config.language;
|
||||||
|
delete config.langJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.EJS_emulator = new EmulatorJS(EJS_player, config);
|
||||||
|
window.EJS_adBlocked = (url, del) => window.EJS_emulator.adBlocked(url, del);
|
||||||
|
if (typeof window.EJS_ready === "function") {
|
||||||
|
window.EJS_emulator.on("ready", window.EJS_ready);
|
||||||
|
}
|
||||||
|
if (typeof window.EJS_onGameStart === "function") {
|
||||||
|
window.EJS_emulator.on("start", window.EJS_onGameStart);
|
||||||
|
}
|
||||||
|
if (typeof window.EJS_onLoadState === "function") {
|
||||||
|
window.EJS_emulator.on("loadState", window.EJS_onLoadState);
|
||||||
|
}
|
||||||
|
if (typeof window.EJS_onSaveState === "function") {
|
||||||
|
window.EJS_emulator.on("saveState", window.EJS_onSaveState);
|
||||||
|
}
|
||||||
|
if (typeof window.EJS_onLoadSave === "function") {
|
||||||
|
window.EJS_emulator.on("loadSave", window.EJS_onLoadSave);
|
||||||
|
}
|
||||||
|
if (typeof window.EJS_onSaveSave === "function") {
|
||||||
|
window.EJS_emulator.on("saveSave", window.EJS_onSaveSave);
|
||||||
|
}
|
||||||
|
})();
|
||||||
BIN
openapi.json
Normal file
BIN
openapi.json
Normal file
Binary file not shown.
BIN
public/bios/3do.zip
Normal file
BIN
public/bios/3do.zip
Normal file
Binary file not shown.
BIN
public/bios/amiga.zip
Normal file
BIN
public/bios/amiga.zip
Normal file
Binary file not shown.
BIN
public/bios/arcade.zip
Normal file
BIN
public/bios/arcade.zip
Normal file
Binary file not shown.
BIN
public/bios/colecovision.zip
Normal file
BIN
public/bios/colecovision.zip
Normal file
Binary file not shown.
BIN
public/bios/dc.zip
Normal file
BIN
public/bios/dc.zip
Normal file
Binary file not shown.
BIN
public/bios/fds.zip
Normal file
BIN
public/bios/fds.zip
Normal file
Binary file not shown.
BIN
public/bios/gba.zip
Normal file
BIN
public/bios/gba.zip
Normal file
Binary file not shown.
BIN
public/bios/jaguar.zip
Normal file
BIN
public/bios/jaguar.zip
Normal file
Binary file not shown.
BIN
public/bios/msx.zip
Normal file
BIN
public/bios/msx.zip
Normal file
Binary file not shown.
BIN
public/bios/nds.zip
Normal file
BIN
public/bios/nds.zip
Normal file
Binary file not shown.
BIN
public/bios/neogeoaes.zip
Normal file
BIN
public/bios/neogeoaes.zip
Normal file
Binary file not shown.
BIN
public/bios/neogeomvs.zip
Normal file
BIN
public/bios/neogeomvs.zip
Normal file
Binary file not shown.
BIN
public/bios/nintendo-dsi.zip
Normal file
BIN
public/bios/nintendo-dsi.zip
Normal file
Binary file not shown.
BIN
public/bios/odyssey-2.zip
Normal file
BIN
public/bios/odyssey-2.zip
Normal file
Binary file not shown.
BIN
public/bios/ps2.zip
Normal file
BIN
public/bios/ps2.zip
Normal file
Binary file not shown.
BIN
public/bios/psp.zip
Normal file
BIN
public/bios/psp.zip
Normal file
Binary file not shown.
BIN
public/bios/psx.zip
Normal file
BIN
public/bios/psx.zip
Normal file
Binary file not shown.
BIN
public/bios/saturn.zip
Normal file
BIN
public/bios/saturn.zip
Normal file
Binary file not shown.
BIN
public/bios/segacd.zip
Normal file
BIN
public/bios/segacd.zip
Normal file
Binary file not shown.
BIN
public/bios/turbografx-cd.zip
Normal file
BIN
public/bios/turbografx-cd.zip
Normal file
Binary file not shown.
2
public/coi-serviceworker.min.js
vendored
Normal file
2
public/coi-serviceworker.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
||||||
|
let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})();
|
||||||
BIN
public/emu/cores/fceumm-legacy.data
Normal file
BIN
public/emu/cores/fceumm-legacy.data
Normal file
Binary file not shown.
BIN
public/emu/cores/fceumm.data
Normal file
BIN
public/emu/cores/fceumm.data
Normal file
Binary file not shown.
27
public/emu/cores/nes.js
Normal file
27
public/emu/cores/nes.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404: Page not found</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
h2, p {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0000EE;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #800080;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>404: Page not found</h2>
|
||||||
|
<p>Idk where you're trying to go, but its not here....</p>
|
||||||
|
<a herf="#" onclick="history.back()">Go Back?</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
public/emu/cores/nes.wasm
Normal file
27
public/emu/cores/nes.wasm
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404: Page not found</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
h2, p {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0000EE;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #800080;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>404: Page not found</h2>
|
||||||
|
<p>Idk where you're trying to go, but its not here....</p>
|
||||||
|
<a herf="#" onclick="history.back()">Go Back?</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
public/emu/cores/reports/fceumm.json
Normal file
1
public/emu/cores/reports/fceumm.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "core": "fceumm", "buildStart": "2026-02-04T02:44:07+00:00", "buildEnd": "2026-02-04T02:45:32+00:00", "options": {} }
|
||||||
27
public/emu/emulator.js
Normal file
27
public/emu/emulator.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404: Page not found</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
h2, p {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0000EE;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #800080;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>404: Page not found</h2>
|
||||||
|
<p>Idk where you're trying to go, but its not here....</p>
|
||||||
|
<a herf="#" onclick="history.back()">Go Back?</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
public/emu/emulator.min.css
vendored
Normal file
1
public/emu/emulator.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/emu/emulator.min.js
vendored
Normal file
2
public/emu/emulator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/emu/emulator.min.js.map
Normal file
1
public/emu/emulator.min.js.map
Normal file
File diff suppressed because one or more lines are too long
264
public/emu/loader.js
Normal file
264
public/emu/loader.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
const folderPath = (path) => {
|
||||||
|
const filename = path.split("/").pop();
|
||||||
|
return path.substring(0, path.length - filename.length);
|
||||||
|
};
|
||||||
|
function isAbsoluteUrl(path) {
|
||||||
|
return /^[a-zA-Z][\w.+-]*:\/\//i.test(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let scriptPath = (typeof window.EJS_pathtodata === "string") ? window.EJS_pathtodata : folderPath((new URL(document.currentScript.src)).pathname);
|
||||||
|
if (!scriptPath.endsWith("/")) {
|
||||||
|
scriptPath += "/";
|
||||||
|
}
|
||||||
|
if (!scriptPath.startsWith("/") && !isAbsoluteUrl(scriptPath)) {
|
||||||
|
scriptPath = "../" + scriptPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debug = window.EJS_DEBUG_XX === true;
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log("Script Path:", scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(path) {
|
||||||
|
if ("undefined" != typeof EJS_paths && typeof EJS_paths[path] === "string") {
|
||||||
|
return EJS_paths[path];
|
||||||
|
} else if (path.endsWith("emulator.min.js") || path.endsWith("css")) {
|
||||||
|
return scriptPath + path;
|
||||||
|
} else {
|
||||||
|
return scriptPath + "src/" + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadScript(file) {
|
||||||
|
try {
|
||||||
|
const script = resolvePath(file);
|
||||||
|
const module = await import(script);
|
||||||
|
return module.default;
|
||||||
|
} catch(e) {
|
||||||
|
if (debug) console.error(e);
|
||||||
|
const module = await filesMissing(file);
|
||||||
|
return module.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStyle(file) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
let css = document.createElement("link");
|
||||||
|
css.rel = "stylesheet";
|
||||||
|
css.href = resolvePath(file);
|
||||||
|
css.onload = resolve;
|
||||||
|
css.onerror = () => {
|
||||||
|
filesMissing(file).then(e => resolve());
|
||||||
|
}
|
||||||
|
document.head.appendChild(css);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function filesMissing(file) {
|
||||||
|
console.error("Failed to load " + file);
|
||||||
|
let minifiedFailed = file.includes("min");
|
||||||
|
const errorMessage = `Failed to load ${file} because it's likely that the minified files are missing.
|
||||||
|
To fix this you have 3 options:
|
||||||
|
1. You can download the zip from the latest release here: https://github.com/EmulatorJS/EmulatorJS/releases/latest - Recommended
|
||||||
|
2. You can download the zip from here: https://cdn.emulatorjs.org/stable/data/emulator.min.zip and extract it to the data/ folder
|
||||||
|
3. You can build the files by running "npm i && npm run build" in the data/minify folder.`;
|
||||||
|
console[minifiedFailed ? "warn" : "error"](errorMessage);
|
||||||
|
if (minifiedFailed) {
|
||||||
|
console.log("Attempting to load non-minified files");
|
||||||
|
if (file === "emulator.min.js") {
|
||||||
|
return await loadScript("emulator.js");
|
||||||
|
} else {
|
||||||
|
await loadStyle("emulator.css");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLanguagePath(language) {
|
||||||
|
if ("undefined" != typeof EJS_paths && typeof EJS_paths[language] === "string") {
|
||||||
|
return { path: EJS_paths[language], fallback: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = scriptPath + "localization/" + language + ".json";
|
||||||
|
let fallback = null;
|
||||||
|
|
||||||
|
if (language.includes("-") || language.includes("_")) {
|
||||||
|
fallback = scriptPath + "localization/" + language.split(/[-_]/)[0] + ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { path: base, fallback };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(path) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(path);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to fetch language file:", path, e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLanguages(baseJson, overrideJson) {
|
||||||
|
if (!baseJson || !overrideJson) return baseJson || overrideJson || {};
|
||||||
|
return { ...baseJson, ...overrideJson };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLanguage(config) {
|
||||||
|
const defaultLangs = ["en", "en-US"];
|
||||||
|
|
||||||
|
if (!config.language || defaultLangs.includes(config.language)) return config;
|
||||||
|
|
||||||
|
console.log("Language:", config.language);
|
||||||
|
|
||||||
|
let langData = {};
|
||||||
|
const paths = getLanguagePath(config.language);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const specificJson = await fetchJson(paths.path);
|
||||||
|
|
||||||
|
if (paths.fallback) {
|
||||||
|
const fallbackJson = await fetchJson(paths.fallback);
|
||||||
|
langData = mergeLanguages(fallbackJson, specificJson || {});
|
||||||
|
} else {
|
||||||
|
langData = specificJson || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
config.langJson = langData;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (paths.fallback) {
|
||||||
|
const fallbackLang = config.language.split(/[-_]/)[0];
|
||||||
|
console.warn(`Language '${config.language}' not found, trying '${fallbackLang}'`);
|
||||||
|
|
||||||
|
const fallbackJson = await fetchJson(paths.fallback);
|
||||||
|
if (fallbackJson) {
|
||||||
|
langData = fallbackJson;
|
||||||
|
config.langJson = langData;
|
||||||
|
config.language = fallbackLang;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`No language file found for '${config.language}'`);
|
||||||
|
delete config.language;
|
||||||
|
delete config.langJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
debug: debug,
|
||||||
|
gameUrl: window.EJS_gameUrl,
|
||||||
|
dataPath: scriptPath,
|
||||||
|
system: window.EJS_core,
|
||||||
|
biosUrl: window.EJS_biosUrl,
|
||||||
|
gameName: window.EJS_gameName,
|
||||||
|
color: window.EJS_color,
|
||||||
|
adUrl: window.EJS_AdUrl,
|
||||||
|
adMode: window.EJS_AdMode,
|
||||||
|
adTimer: window.EJS_AdTimer,
|
||||||
|
adSize: window.EJS_AdSize,
|
||||||
|
alignStartButton: window.EJS_alignStartButton,
|
||||||
|
VirtualGamepadSettings: window.EJS_VirtualGamepadSettings,
|
||||||
|
buttonOpts: window.EJS_Buttons,
|
||||||
|
volume: window.EJS_volume,
|
||||||
|
defaultControllers: window.EJS_defaultControls,
|
||||||
|
startOnLoad: window.EJS_startOnLoaded,
|
||||||
|
fullscreenOnLoad: window.EJS_fullscreenOnLoaded,
|
||||||
|
filePaths: window.EJS_paths,
|
||||||
|
loadState: window.EJS_loadStateURL,
|
||||||
|
cacheLimit: window.EJS_CacheLimit,
|
||||||
|
cacheConfig: window.EJS_cacheConfig,
|
||||||
|
cheats: window.EJS_cheats,
|
||||||
|
cheatPath: window.EJS_cheatPath,
|
||||||
|
defaultOptions: window.EJS_defaultOptions,
|
||||||
|
gamePatchUrl: window.EJS_gamePatchUrl,
|
||||||
|
gameParentUrl: window.EJS_gameParentUrl,
|
||||||
|
netplayUrl: window.EJS_netplayServer,
|
||||||
|
netplayICEServers: window.EJS_netplayICEServers,
|
||||||
|
gameId: window.EJS_gameID,
|
||||||
|
backgroundImg: window.EJS_backgroundImage,
|
||||||
|
backgroundBlur: window.EJS_backgroundBlur,
|
||||||
|
backgroundColor: window.EJS_backgroundColor,
|
||||||
|
controlScheme: window.EJS_controlScheme,
|
||||||
|
threads: window.EJS_threads,
|
||||||
|
disableCue: window.EJS_disableCue,
|
||||||
|
startBtnName: window.EJS_startButtonName,
|
||||||
|
softLoad: window.EJS_softLoad,
|
||||||
|
capture: window.EJS_screenCapture,
|
||||||
|
externalFiles: window.EJS_externalFiles,
|
||||||
|
dontExtractRom: window.EJS_dontExtractRom,
|
||||||
|
dontExtractBIOS: window.EJS_dontExtractBIOS,
|
||||||
|
disableLocalStorage: window.EJS_disableLocalStorage,
|
||||||
|
forceLegacyCores: window.EJS_forceLegacyCores,
|
||||||
|
noAutoFocus: window.EJS_noAutoFocus,
|
||||||
|
videoRotation: window.EJS_videoRotation,
|
||||||
|
hideSettings: window.EJS_hideSettings,
|
||||||
|
browserMode: window.EJS_browserMode,
|
||||||
|
additionalShaders: window.EJS_shaders,
|
||||||
|
fixedSaveInterval: window.EJS_fixedSaveInterval,
|
||||||
|
disableAutoUnload: window.EJS_disableAutoUnload,
|
||||||
|
disableBatchBootup: window.EJS_disableBatchBootup
|
||||||
|
};
|
||||||
|
|
||||||
|
async function prepareLanguage() {
|
||||||
|
try {
|
||||||
|
const systemLang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
|
config.language = window.EJS_language || systemLang;
|
||||||
|
|
||||||
|
if (config.language && window.EJS_disableAutoLang !== false) {
|
||||||
|
return await loadLanguage(config);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Language detection failed:", e.message);
|
||||||
|
delete config.language;
|
||||||
|
delete config.langJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function() {
|
||||||
|
let EmulatorJS;
|
||||||
|
if (debug) {
|
||||||
|
EmulatorJS = await loadScript("emulator.js");
|
||||||
|
await loadStyle("emulator.css");
|
||||||
|
} else {
|
||||||
|
EmulatorJS = await loadScript("emulator.min.js");
|
||||||
|
await loadStyle("emulator.min.css");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EmulatorJS) {
|
||||||
|
console.error("EmulatorJS failed to load. Check for missing files.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prepareLanguage();
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log("Language:", config.language);
|
||||||
|
console.log("Language JSON loaded:", !!config.langJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.EJS_emulator = new EmulatorJS(EJS_player, config);
|
||||||
|
window.EJS_adBlocked = (url, del) => window.EJS_emulator.adBlocked(url, del);
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
["ready", window.EJS_ready],
|
||||||
|
["start", window.EJS_onGameStart],
|
||||||
|
["loadState", window.EJS_onLoadState],
|
||||||
|
["saveState", window.EJS_onSaveState],
|
||||||
|
["loadSave", window.EJS_onLoadSave],
|
||||||
|
["saveSave", window.EJS_onSaveSave]
|
||||||
|
];
|
||||||
|
|
||||||
|
handlers.forEach(([event, callback]) => {
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
window.EJS_emulator.on(event, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window.EJS_onSaveUpdate === "function") {
|
||||||
|
window.EJS_emulator.on("saveUpdate", window.EJS_onSaveUpdate);
|
||||||
|
window.EJS_emulator.enableSaveUpdateEvent();
|
||||||
|
}
|
||||||
|
})();
|
||||||
27
public/emu/localization/en-US.json
Normal file
27
public/emu/localization/en-US.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404: Page not found</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
h2, p {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0000EE;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #800080;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>404: Page not found</h2>
|
||||||
|
<p>Idk where you're trying to go, but its not here....</p>
|
||||||
|
<a herf="#" onclick="history.back()">Go Back?</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
340
public/emu/localization/en.json
Normal file
340
public/emu/localization/en.json
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
{
|
||||||
|
"0": "0",
|
||||||
|
"1": "1",
|
||||||
|
"2": "2",
|
||||||
|
"3": "3",
|
||||||
|
"4": "4",
|
||||||
|
"5": "5",
|
||||||
|
"6": "6",
|
||||||
|
"7": "7",
|
||||||
|
"8": "8",
|
||||||
|
"9": "9",
|
||||||
|
"Restart": "Restart",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Play": "Play",
|
||||||
|
"Save State": "Save State",
|
||||||
|
"Load State": "Load State",
|
||||||
|
"Control Settings": "Control Settings",
|
||||||
|
"Cheats": "Cheats",
|
||||||
|
"Cache Manager": "Cache Manager",
|
||||||
|
"Export Save File": "Export Save File",
|
||||||
|
"Import Save File": "Import Save File",
|
||||||
|
"Netplay": "Netplay",
|
||||||
|
"Mute": "Mute",
|
||||||
|
"Unmute": "Unmute",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"Enter Fullscreen": "Enter Fullscreen",
|
||||||
|
"Exit Fullscreen": "Exit Fullscreen",
|
||||||
|
"Context Menu": "Context Menu",
|
||||||
|
"Reset": "Reset",
|
||||||
|
"Clear": "Clear",
|
||||||
|
"Close": "Close",
|
||||||
|
"QUICK SAVE STATE": "QUICK SAVE STATE",
|
||||||
|
"QUICK LOAD STATE": "QUICK LOAD STATE",
|
||||||
|
"CHANGE STATE SLOT": "CHANGE STATE SLOT",
|
||||||
|
"FAST FORWARD": "FAST FORWARD",
|
||||||
|
"Player": "Player",
|
||||||
|
"Connected Gamepad": "Connected Gamepad",
|
||||||
|
"Gamepad": "Gamepad",
|
||||||
|
"Keyboard": "Keyboard",
|
||||||
|
"Set": "Set",
|
||||||
|
"Add Cheat": "Add Cheat",
|
||||||
|
"Note that some cheats require a restart to disable": "Note that some cheats require a restart to disable",
|
||||||
|
"Create a Room": "Create a Room",
|
||||||
|
"Rooms": "Rooms",
|
||||||
|
"Start Game": "Start Game",
|
||||||
|
"Click to resume Emulator": "Click to resume Emulator",
|
||||||
|
"Drop save state here to load": "Drop save state here to load",
|
||||||
|
"Loading...": "Loading...",
|
||||||
|
"Download Game Core": "Download Game Core",
|
||||||
|
"Outdated graphics driver": "Outdated graphics driver",
|
||||||
|
"Decompress Game Core": "Decompress Game Core",
|
||||||
|
"Download Game Data": "Download Game Data",
|
||||||
|
"Decompress Game Data": "Decompress Game Data",
|
||||||
|
"Shaders": "Shaders",
|
||||||
|
"Disabled": "Disabled",
|
||||||
|
"2xScaleHQ": "2xScaleHQ",
|
||||||
|
"4xScaleHQ": "4xScaleHQ",
|
||||||
|
"CRT easymode": "CRT easymode",
|
||||||
|
"CRT aperture": "CRT aperture",
|
||||||
|
"CRT geom": "CRT geom",
|
||||||
|
"CRT mattias": "CRT mattias",
|
||||||
|
"FPS": "FPS",
|
||||||
|
"show": "show",
|
||||||
|
"hide": "hide",
|
||||||
|
"Fast Forward Ratio": "Fast Forward Ratio",
|
||||||
|
"Fast Forward": "Fast Forward",
|
||||||
|
"Enabled": "Enabled",
|
||||||
|
"Save State Slot": "Save State Slot",
|
||||||
|
"Save State Location": "Save State Location",
|
||||||
|
"Download": "Download",
|
||||||
|
"Keep in Browser": "Keep in Browser",
|
||||||
|
"Auto": "Auto",
|
||||||
|
"NTSC": "NTSC",
|
||||||
|
"PAL": "PAL",
|
||||||
|
"Dendy": "Dendy",
|
||||||
|
"8:7 PAR": "8:7 PAR",
|
||||||
|
"4:3": "4:3",
|
||||||
|
"Low": "Low",
|
||||||
|
"High": "High",
|
||||||
|
"Very High": "Very High",
|
||||||
|
"None": "None",
|
||||||
|
"Player 1": "Player 1",
|
||||||
|
"Player 2": "Player 2",
|
||||||
|
"Both": "Both",
|
||||||
|
"SAVED STATE TO SLOT": "SAVED STATE TO SLOT",
|
||||||
|
"LOADED STATE FROM SLOT": "LOADED STATE FROM SLOT",
|
||||||
|
"SET SAVE STATE SLOT TO": "SET SAVE STATE SLOT TO",
|
||||||
|
"Network Error": "Network Error",
|
||||||
|
"Submit": "Submit",
|
||||||
|
"Description": "Description",
|
||||||
|
"Code": "Code",
|
||||||
|
"Add Cheat Code": "Add Cheat Code",
|
||||||
|
"Leave Room": "Leave Room",
|
||||||
|
"Password": "Password",
|
||||||
|
"Password (optional)": "Password (optional)",
|
||||||
|
"Max Players": "Max Players",
|
||||||
|
"Room Name": "Room Name",
|
||||||
|
"Join": "Join",
|
||||||
|
"Player Name": "Player Name",
|
||||||
|
"Set Player Name": "Set Player Name",
|
||||||
|
"Left Handed Mode": "Left Handed Mode",
|
||||||
|
"Virtual Gamepad": "Virtual Gamepad",
|
||||||
|
"Disk": "Disk",
|
||||||
|
"Press Keyboard": "Press Keyboard",
|
||||||
|
"INSERT COIN": "INSERT COIN",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"LOADED STATE FROM BROWSER": "LOADED STATE FROM BROWSER",
|
||||||
|
"SAVED STATE TO BROWSER": "SAVED STATE TO BROWSER",
|
||||||
|
"Join the discord": "Join the discord",
|
||||||
|
"View on GitHub": "View on GitHub",
|
||||||
|
"Failed to start game": "Failed to start game",
|
||||||
|
"Download Game BIOS": "Download Game BIOS",
|
||||||
|
"Decompress Game BIOS": "Decompress Game BIOS",
|
||||||
|
"Download Game Parent": "Download Game Parent",
|
||||||
|
"Decompress Game Parent": "Decompress Game Parent",
|
||||||
|
"Download Game Patch": "Download Game Patch",
|
||||||
|
"Decompress Game Patch": "Decompress Game Patch",
|
||||||
|
"Download Game State": "Download Game State",
|
||||||
|
"Check console": "Check console",
|
||||||
|
"Error for site owner": "Error for site owner",
|
||||||
|
"EmulatorJS": "EmulatorJS",
|
||||||
|
"Clear All": "Clear All",
|
||||||
|
"Take Screenshot": "Take Screenshot",
|
||||||
|
"Start Screen Recording": "Start Screen Recording",
|
||||||
|
"Stop Screen Recording": "Stop Screen Recording",
|
||||||
|
"Quick Save": "Quick Save",
|
||||||
|
"Quick Load": "Quick Load",
|
||||||
|
"REWIND": "REWIND",
|
||||||
|
"Rewind Enabled (requires restart)": "Rewind Enabled (requires restart)",
|
||||||
|
"Rewind Granularity": "Rewind Granularity",
|
||||||
|
"Slow Motion Ratio": "Slow Motion Ratio",
|
||||||
|
"Slow Motion": "Slow Motion",
|
||||||
|
"Home": "Home",
|
||||||
|
"EmulatorJS License": "EmulatorJS License",
|
||||||
|
"RetroArch License": "RetroArch License",
|
||||||
|
"This project is powered by": "This project is powered by",
|
||||||
|
"View the RetroArch license here": "View the RetroArch license here",
|
||||||
|
"SLOW MOTION": "SLOW MOTION",
|
||||||
|
"A": "A",
|
||||||
|
"B": "B",
|
||||||
|
"SELECT": "SELECT",
|
||||||
|
"START": "START",
|
||||||
|
"UP": "UP",
|
||||||
|
"DOWN": "DOWN",
|
||||||
|
"LEFT": "LEFT",
|
||||||
|
"RIGHT": "RIGHT",
|
||||||
|
"X": "X",
|
||||||
|
"Y": "Y",
|
||||||
|
"L": "L",
|
||||||
|
"R": "R",
|
||||||
|
"Z": "Z",
|
||||||
|
"STICK UP": "STICK UP",
|
||||||
|
"STICK DOWN": "STICK DOWN",
|
||||||
|
"STICK LEFT": "STICK LEFT",
|
||||||
|
"STICK RIGHT": "STICK RIGHT",
|
||||||
|
"C-PAD UP": "C-PAD UP",
|
||||||
|
"C-PAD DOWN": "C-PAD DOWN",
|
||||||
|
"C-PAD LEFT": "C-PAD LEFT",
|
||||||
|
"C-PAD RIGHT": "C-PAD RIGHT",
|
||||||
|
"MICROPHONE": "MICROPHONE",
|
||||||
|
"BUTTON 1 / START": "BUTTON 1 / START",
|
||||||
|
"BUTTON 2": "BUTTON 2",
|
||||||
|
"BUTTON": "BUTTON",
|
||||||
|
"LEFT D-PAD UP": "LEFT D-PAD UP",
|
||||||
|
"LEFT D-PAD DOWN": "LEFT D-PAD DOWN",
|
||||||
|
"LEFT D-PAD LEFT": "LEFT D-PAD LEFT",
|
||||||
|
"LEFT D-PAD RIGHT": "LEFT D-PAD RIGHT",
|
||||||
|
"RIGHT D-PAD UP": "RIGHT D-PAD UP",
|
||||||
|
"RIGHT D-PAD DOWN": "RIGHT D-PAD DOWN",
|
||||||
|
"RIGHT D-PAD LEFT": "RIGHT D-PAD LEFT",
|
||||||
|
"RIGHT D-PAD RIGHT": "RIGHT D-PAD RIGHT",
|
||||||
|
"C": "C",
|
||||||
|
"MODE": "MODE",
|
||||||
|
"FIRE": "FIRE",
|
||||||
|
"RESET": "RESET",
|
||||||
|
"LEFT DIFFICULTY A": "LEFT DIFFICULTY A",
|
||||||
|
"LEFT DIFFICULTY B": "LEFT DIFFICULTY B",
|
||||||
|
"RIGHT DIFFICULTY A": "RIGHT DIFFICULTY A",
|
||||||
|
"RIGHT DIFFICULTY B": "RIGHT DIFFICULTY B",
|
||||||
|
"COLOR": "COLOR",
|
||||||
|
"B/W": "B/W",
|
||||||
|
"PAUSE": "PAUSE",
|
||||||
|
"OPTION": "OPTION",
|
||||||
|
"OPTION 1": "OPTION 1",
|
||||||
|
"OPTION 2": "OPTION 2",
|
||||||
|
"L2": "L2",
|
||||||
|
"R2": "R2",
|
||||||
|
"L3": "L3",
|
||||||
|
"R3": "R3",
|
||||||
|
"L STICK UP": "L STICK UP",
|
||||||
|
"L STICK DOWN": "L STICK DOWN",
|
||||||
|
"L STICK LEFT": "L STICK LEFT",
|
||||||
|
"L STICK RIGHT": "L STICK RIGHT",
|
||||||
|
"R STICK UP": "R STICK UP",
|
||||||
|
"R STICK DOWN": "R STICK DOWN",
|
||||||
|
"R STICK LEFT": "R STICK LEFT",
|
||||||
|
"R STICK RIGHT": "R STICK RIGHT",
|
||||||
|
"Start": "Start",
|
||||||
|
"Select": "Select",
|
||||||
|
"Fast": "Fast",
|
||||||
|
"Slow": "Slow",
|
||||||
|
"a": "a",
|
||||||
|
"b": "b",
|
||||||
|
"c": "c",
|
||||||
|
"d": "d",
|
||||||
|
"e": "e",
|
||||||
|
"f": "f",
|
||||||
|
"g": "g",
|
||||||
|
"h": "h",
|
||||||
|
"i": "i",
|
||||||
|
"j": "j",
|
||||||
|
"k": "k",
|
||||||
|
"l": "l",
|
||||||
|
"m": "m",
|
||||||
|
"n": "n",
|
||||||
|
"o": "o",
|
||||||
|
"p": "p",
|
||||||
|
"q": "q",
|
||||||
|
"r": "r",
|
||||||
|
"s": "s",
|
||||||
|
"t": "t",
|
||||||
|
"u": "u",
|
||||||
|
"v": "v",
|
||||||
|
"w": "w",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z",
|
||||||
|
"enter": "enter",
|
||||||
|
"escape": "escape",
|
||||||
|
"space": "space",
|
||||||
|
"tab": "tab",
|
||||||
|
"backspace": "backspace",
|
||||||
|
"delete": "delete",
|
||||||
|
"arrowup": "arrowup",
|
||||||
|
"arrowdown": "arrowdown",
|
||||||
|
"arrowleft": "arrowleft",
|
||||||
|
"arrowright": "arrowright",
|
||||||
|
"f1": "f1",
|
||||||
|
"f2": "f2",
|
||||||
|
"f3": "f3",
|
||||||
|
"f4": "f4",
|
||||||
|
"f5": "f5",
|
||||||
|
"f6": "f6",
|
||||||
|
"f7": "f7",
|
||||||
|
"f8": "f8",
|
||||||
|
"f9": "f9",
|
||||||
|
"f10": "f10",
|
||||||
|
"f11": "f11",
|
||||||
|
"f12": "f12",
|
||||||
|
"shift": "shift",
|
||||||
|
"control": "control",
|
||||||
|
"alt": "alt",
|
||||||
|
"meta": "meta",
|
||||||
|
"capslock": "capslock",
|
||||||
|
"insert": "insert",
|
||||||
|
"home": "home",
|
||||||
|
"end": "end",
|
||||||
|
"pageup": "pageup",
|
||||||
|
"pagedown": "pagedown",
|
||||||
|
"!": "!",
|
||||||
|
"@": "@",
|
||||||
|
"#": "#",
|
||||||
|
"$": "$",
|
||||||
|
"%": "%",
|
||||||
|
"^": "^",
|
||||||
|
"&": "&",
|
||||||
|
"*": "*",
|
||||||
|
"(": "(",
|
||||||
|
")": ")",
|
||||||
|
"-": "-",
|
||||||
|
"_": "_",
|
||||||
|
"+": "+",
|
||||||
|
"=": "=",
|
||||||
|
"[": "[",
|
||||||
|
"]": "]",
|
||||||
|
"{": "{",
|
||||||
|
"}": "}",
|
||||||
|
";": ";",
|
||||||
|
":": ":",
|
||||||
|
"'": "'",
|
||||||
|
"\"": "\"",
|
||||||
|
",": ",",
|
||||||
|
".": ".",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
"/": "/",
|
||||||
|
"?": "?",
|
||||||
|
"LEFT_STICK_X": "LEFT_STICK_X",
|
||||||
|
"LEFT_STICK_Y": "LEFT_STICK_Y",
|
||||||
|
"RIGHT_STICK_X": "RIGHT_STICK_X",
|
||||||
|
"RIGHT_STICK_Y": "RIGHT_STICK_Y",
|
||||||
|
"LEFT_TRIGGER": "LEFT_TRIGGER",
|
||||||
|
"RIGHT_TRIGGER": "RIGHT_TRIGGER",
|
||||||
|
"A_BUTTON": "A_BUTTON",
|
||||||
|
"B_BUTTON": "B_BUTTON",
|
||||||
|
"X_BUTTON": "X_BUTTON",
|
||||||
|
"Y_BUTTON": "Y_BUTTON",
|
||||||
|
"START_BUTTON": "START_BUTTON",
|
||||||
|
"SELECT_BUTTON": "SELECT_BUTTON",
|
||||||
|
"L1_BUTTON": "L1_BUTTON",
|
||||||
|
"R1_BUTTON": "R1_BUTTON",
|
||||||
|
"L2_BUTTON": "L2_BUTTON",
|
||||||
|
"R2_BUTTON": "R2_BUTTON",
|
||||||
|
"LEFT_THUMB_BUTTON": "LEFT_THUMB_BUTTON",
|
||||||
|
"RIGHT_THUMB_BUTTON": "RIGHT_THUMB_BUTTON",
|
||||||
|
"DPAD_UP": "DPAD_UP",
|
||||||
|
"DPAD_DOWN": "DPAD_DOWN",
|
||||||
|
"DPAD_LEFT": "DPAD_LEFT",
|
||||||
|
"DPAD_RIGHT": "DPAD_RIGHT",
|
||||||
|
"Disks": "Disks",
|
||||||
|
"Exit EmulatorJS": "Exit EmulatorJS",
|
||||||
|
"BUTTON_1": "BUTTON_1",
|
||||||
|
"BUTTON_2": "BUTTON_2",
|
||||||
|
"BUTTON_3": "BUTTON_3",
|
||||||
|
"BUTTON_4": "BUTTON_4",
|
||||||
|
"up arrow": "up arrow",
|
||||||
|
"down arrow": "down arrow",
|
||||||
|
"left arrow": "left arrow",
|
||||||
|
"right arrow": "right arrow",
|
||||||
|
"LEFT_TOP_SHOULDER": "LEFT_TOP_SHOULDER",
|
||||||
|
"RIGHT_TOP_SHOULDER": "RIGHT_TOP_SHOULDER",
|
||||||
|
"CRT beam": "CRT beam",
|
||||||
|
"CRT caligari": "CRT caligari",
|
||||||
|
"CRT lottes": "CRT lottes",
|
||||||
|
"CRT yeetron": "CRT yeetron",
|
||||||
|
"CRT zfast": "CRT zfast",
|
||||||
|
"SABR": "SABR",
|
||||||
|
"Bicubic": "Bicubic",
|
||||||
|
"Mix frames": "Mix frames",
|
||||||
|
"WebGL2": "WebGL2",
|
||||||
|
"Requires restart": "Requires restart",
|
||||||
|
"VSync": "VSync",
|
||||||
|
"Video Rotation": "Video Rotation",
|
||||||
|
"Rewind Enabled (Requires restart)": "Rewind Enabled (Requires restart)",
|
||||||
|
"System Save interval": "System Save interval",
|
||||||
|
"Menu Bar Button": "Menu Bar Button",
|
||||||
|
"visible": "visible",
|
||||||
|
"hidden": "hidden",
|
||||||
|
"Autofire": "Autofire"
|
||||||
|
}
|
||||||
69
public/emulator.html
Normal file
69
public/emulator.html
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EmulatorJS Player</title>
|
||||||
|
<!-- Explicitly invoke COOP/COEP isolation from the sandbox boundary to guarantee SharedArrayBuffer inheritance for MSX -->
|
||||||
|
<script src="/coi-serviceworker.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: black;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#game {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game"></div>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
let config = {};
|
||||||
|
try {
|
||||||
|
config = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('EmulatorJS: Failed to parse hash config', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.EJS_player = '#game';
|
||||||
|
window.EJS_gameUrl = config.gameUrl;
|
||||||
|
window.EJS_core = config.core || config.system;
|
||||||
|
window.EJS_pathtodata = 'https://cdn.emulatorjs.org/stable/data/';
|
||||||
|
window.EJS_startOnLoaded = true;
|
||||||
|
if (config.biosUrl) {
|
||||||
|
window.EJS_biosUrl = config.biosUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = (data) => {
|
||||||
|
const blobContent = data.data || data;
|
||||||
|
window.parent.postMessage({ type: 'EJS_SAVE', data: blobContent }, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExit = () => {
|
||||||
|
window.parent.postMessage({ type: 'EJS_EXIT' }, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.EJS_conf = {
|
||||||
|
...config,
|
||||||
|
dataPath: 'https://cdn.emulatorjs.org/stable/data/',
|
||||||
|
onSave: handleSave,
|
||||||
|
onExit: handleExit
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Official Static Loader Tag (CDN) -->
|
||||||
|
<script src="https://cdn.emulatorjs.org/stable/data/loader.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.parent.postMessage({ type: 'EJS_READY' }, '*');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -403,7 +403,7 @@ export const rommApiClient = {
|
|||||||
genres: getAll('genres').map(sanitize).filter(Boolean),
|
genres: getAll('genres').map(sanitize).filter(Boolean),
|
||||||
franchises: getAll('franchises').map(sanitize).filter(Boolean),
|
franchises: getAll('franchises').map(sanitize).filter(Boolean),
|
||||||
releaseDate: formatDate(json.release_date || getFirst('first_release_date') || getFirst('release_date')),
|
releaseDate: formatDate(json.release_date || getFirst('first_release_date') || getFirst('release_date')),
|
||||||
fsName: json.fs_name,
|
fsName: json.fs_name || json.file || json.fs_path,
|
||||||
regions: regions.length > 0 ? regions : ['Global'],
|
regions: regions.length > 0 ? regions : ['Global'],
|
||||||
players: (() => {
|
players: (() => {
|
||||||
let val = json.metadatum?.player_count || json.players || getFirst('player_count') || getFirst('total_players') || getFirst('players') || getFirst('max_players');
|
let val = json.metadatum?.player_count || json.players || getFirst('player_count') || getFirst('total_players') || getFirst('players') || getFirst('max_players');
|
||||||
@@ -547,7 +547,7 @@ export const rommApiClient = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getPlayUrl(gameId: string): string {
|
getPlayUrl(gameId: string): string {
|
||||||
return `${this.apiBase}/roms/${gameId}/play`;
|
return `/rom/${gameId}/ejs`;
|
||||||
},
|
},
|
||||||
|
|
||||||
getManualUrl(gameId: string, platformId?: number): string {
|
getManualUrl(gameId: string, platformId?: number): string {
|
||||||
@@ -595,5 +595,37 @@ export const rommApiClient = {
|
|||||||
const res = await fetch(`${this.apiBase}/roms/${id}/notes`, { headers: this.headers });
|
const res = await fetch(`${this.apiBase}/roms/${id}/notes`, { headers: this.headers });
|
||||||
if (!res.ok) throw new Error('Failed to fetch notes.');
|
if (!res.ok) throw new Error('Failed to fetch notes.');
|
||||||
return res.json();
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
getRomFileUrl(romId: string, fileName: string = 'rom.zip'): string {
|
||||||
|
const encodedName = fileName.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||||
|
return `${this.apiBase}/roms/${romId}/content/token/${this.token}/${encodedName}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSaveFileUrl(saveId: number): string {
|
||||||
|
return `${this.apiBase}/saves/${saveId}/content?access_token=${this.token}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStateFileUrl(stateId: number): string {
|
||||||
|
return `${this.apiBase}/states/${stateId}/content?access_token=${this.token}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadSave(romId: string, file: Blob, emulator: string): Promise<any> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file, 'save.srm');
|
||||||
|
formData.append('rom_id', romId);
|
||||||
|
formData.append('emulator', emulator);
|
||||||
|
|
||||||
|
const res = await fetch(`${this.apiBase}/saves`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
// Do not set Content-Type, browser will set it with boundary
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to upload save.');
|
||||||
|
return res.json();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,76 +1,215 @@
|
|||||||
import { useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useFocusable, FocusContext, pause, resume } from '@noriginmedia/norigin-spatial-navigation';
|
import { useFocusable, FocusContext, setFocus } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { rommApiClient } from '../api/client';
|
import { rommApiClient } from '../api/client';
|
||||||
|
import { EmulatorPlayer } from './EmulatorPlayer';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
interface EmulatorOverlayProps {
|
interface EmulatorOverlayProps {
|
||||||
gameId: string;
|
gameId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FocusableItem = ({ children, onClick, onEnterPress, focusKey, onFocus, className }: any) => {
|
||||||
|
const { ref, focused } = useFocusable({
|
||||||
|
onEnterPress,
|
||||||
|
focusKey
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focused && onFocus) onFocus();
|
||||||
|
}, [focused]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} onClick={onClick} className={className}>
|
||||||
|
{children(focused)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const EmulatorOverlay = ({ gameId, onClose }: EmulatorOverlayProps) => {
|
export const EmulatorOverlay = ({ gameId, onClose }: EmulatorOverlayProps) => {
|
||||||
const { ref, focusKey } = useFocusable({
|
const { ref: overlayRef, focusKey: overlayFocusKey } = useFocusable({
|
||||||
isFocusBoundary: true,
|
isFocusBoundary: true,
|
||||||
focusKey: 'EMULATOR_OVERLAY'
|
focusKey: 'EMULATOR_OVERLAY'
|
||||||
});
|
});
|
||||||
|
|
||||||
const playUrl = rommApiClient.getPlayUrl(gameId);
|
const [gameState, setGameState] = useState<'config' | 'playing'>('config');
|
||||||
|
const [selectedSaveId, setSelectedSaveId] = useState<number | undefined>();
|
||||||
|
const [selectedStateId, setSelectedStateId] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const { data: game, isLoading } = useQuery({
|
||||||
|
queryKey: ['gameDetails', gameId],
|
||||||
|
queryFn: () => rommApiClient.fetchGameDetails(gameId)
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Pause spatial navigation for the browser during gameplay
|
if (gameState === 'config') {
|
||||||
// This prevents d-pad inputs from moving focus behind the iframe
|
setFocus('START_SESSION_BTN');
|
||||||
pause();
|
}
|
||||||
|
}, [gameState]);
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
// Allow ESC to exit the emulator
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
if (isLoading || !game) {
|
||||||
|
return (
|
||||||
// Auto-focus the iframe area for immediate controller interaction
|
<div className="fixed inset-0 z-[200] bg-black/90 backdrop-blur-3xl flex items-center justify-center">
|
||||||
const timer = setTimeout(() => {
|
<div className="text-center">
|
||||||
const iframe = document.querySelector('iframe');
|
<div className="material-symbols-outlined text-4xl text-[#2563eb] animate-spin mb-4">progress_activity</div>
|
||||||
iframe?.focus();
|
<div className="text-[0.625rem] geist-mono text-white/40 uppercase tracking-[0.3em]">Preparing Atelier Session...</div>
|
||||||
}, 500);
|
</div>
|
||||||
|
</div>
|
||||||
return () => {
|
);
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
}
|
||||||
clearTimeout(timer);
|
|
||||||
resume();
|
const handleLaunch = () => {
|
||||||
};
|
setGameState('playing');
|
||||||
}, [onClose]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={overlayFocusKey}>
|
||||||
<div
|
<div ref={overlayRef} className="fixed inset-0 z-[200] bg-[#0a0c14] animate-in fade-in duration-500 overflow-hidden">
|
||||||
ref={ref}
|
{gameState === 'config' ? (
|
||||||
className="fixed inset-0 z-[200] bg-black animate-in fade-in duration-500 flex flex-col"
|
<div className="h-full flex flex-col items-center justify-center p-20 relative">
|
||||||
>
|
{/* Background Atmosphere */}
|
||||||
{/* HUD - Minimalist Atelier style overlay */}
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="absolute top-6 right-6 z-[210] flex items-center gap-4 group">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[60rem] h-[60rem] bg-[#2563eb]/5 rounded-full blur-[120px]" />
|
||||||
<div className="px-4 py-2 rounded-full bg-black/60 backdrop-blur-xl border border-white/10 flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
|
<img src={game.coverUrl} className="absolute inset-0 w-full h-full object-cover opacity-5 blur-3xl scale-110" alt="" />
|
||||||
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-white/40 geist-mono">Session Active</span>
|
</div>
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-[#2563eb] shadow-[0_0_8px_#2563eb] animate-pulse" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="w-12 h-12 flex items-center justify-center rounded-[14px] bg-black/60 backdrop-blur-xl border border-white/10 text-white/40 hover:text-white hover:bg-red-500/80 hover:border-red-500/50 transition-all hover:scale-110 active:scale-95"
|
|
||||||
title="Exit to Browser (ESC)"
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Emulator Iframe */}
|
<div className="max-w-[80rem] w-full grid grid-cols-12 gap-16 relative z-10">
|
||||||
<iframe
|
{/* Left Side: Game Art & Meta */}
|
||||||
src={playUrl}
|
<div className="col-span-4 flex flex-col items-center">
|
||||||
className="w-full h-full border-none shadow-[0_0_128px_rgba(0,0,0,1)]"
|
<div className="w-[20rem] aspect-[3/4] rounded-2xl overflow-hidden shadow-[0_0_80px_rgba(0,0,0,0.5)] border border-white/10 mb-8 transform -rotate-2 hover:rotate-0 transition-transform duration-700">
|
||||||
allow="autoplay; gamepad; fullscreen; keyboard"
|
<img src={game.coverUrl} className="w-full h-full object-cover" alt={game.title} />
|
||||||
title="Game Player"
|
</div>
|
||||||
/>
|
<h1 className="text-3xl font-black text-white text-center mb-2 geist-mono uppercase tracking-tighter leading-none">
|
||||||
|
{game.title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-[#2563eb] text-[0.625rem] font-black uppercase tracking-[0.4em] geist-mono mb-6">
|
||||||
|
{game.system}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-2 max-w-xs">
|
||||||
|
{game.genres?.slice(0, 3).map(g => (
|
||||||
|
<span key={g} className="px-3 py-1 bg-white/5 border border-white/5 rounded-full text-[0.5rem] font-black uppercase tracking-widest text-white/30 geist-mono whitespace-nowrap">{g}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side: Configuration */}
|
||||||
|
<div className="col-span-8 flex flex-col justify-center">
|
||||||
|
<div className="mb-12">
|
||||||
|
<h2 className="text-[0.75rem] font-black text-white/30 uppercase tracking-[0.4em] mb-6 geist-mono border-b border-white/5 pb-2">Archival Data Selection</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-8">
|
||||||
|
{/* Saves Column */}
|
||||||
|
<div>
|
||||||
|
<div className="text-[0.625rem] font-black text-[#2563eb] uppercase tracking-widest mb-4 geist-mono">Native Saves</div>
|
||||||
|
<div className="space-y-3 max-h-[12rem] overflow-y-auto pr-2 scrollbar-hoverable scrollbar-active">
|
||||||
|
{game.saves && game.saves.length > 0 ? game.saves.map(save => (
|
||||||
|
<FocusableItem key={save.id} onClick={() => setSelectedSaveId(save.id)} onEnterPress={() => setSelectedSaveId(save.id)}>
|
||||||
|
{(focused: boolean) => (
|
||||||
|
<div className={`p-3 rounded-xl border transition-all cursor-pointer ${selectedSaveId === save.id ? 'bg-[#2563eb]/20 border-[#2563eb] scale-105' : 'bg-white/5 border-white/5 hover:bg-white/10'} ${focused ? 'ring-2 ring-white/20' : ''}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="material-symbols-outlined text-[#2563eb] text-lg">save</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-[0.625rem] font-bold text-white uppercase truncate">{save.file_name}</div>
|
||||||
|
<div className="text-[0.5rem] text-white/40 geist-mono uppercase">{new Date(save.updated_at).toLocaleDateString()}</div>
|
||||||
|
</div>
|
||||||
|
{selectedSaveId === save.id && <div className="w-2 h-2 rounded-full bg-[#2563eb] shadow-[0_0_8px_#2563eb]" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
|
)) : (
|
||||||
|
<div className="p-8 rounded-xl border border-dashed border-white/5 text-center">
|
||||||
|
<span className="material-symbols-outlined text-white/10 text-3xl mb-2">inbox</span>
|
||||||
|
<div className="text-[0.5rem] font-black text-white/20 uppercase tracking-[0.2em] geist-mono">No Local Saves</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* States Column */}
|
||||||
|
<div>
|
||||||
|
<div className="text-[0.625rem] font-black text-[#2563eb] uppercase tracking-widest mb-4 geist-mono">Instant States</div>
|
||||||
|
<div className="space-y-3 max-h-[12rem] overflow-y-auto pr-2 scrollbar-hoverable scrollbar-active">
|
||||||
|
{game.states && game.states.length > 0 ? game.states.map(state => (
|
||||||
|
<FocusableItem key={state.id} onClick={() => setSelectedStateId(state.id)} onEnterPress={() => setSelectedStateId(state.id)}>
|
||||||
|
{(focused: boolean) => (
|
||||||
|
<div className={`p-3 rounded-xl border transition-all cursor-pointer ${selectedStateId === state.id ? 'bg-[#2563eb]/20 border-[#2563eb] scale-105' : 'bg-white/5 border-white/5 hover:bg-white/10'} ${focused ? 'ring-2 ring-white/20' : ''}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="material-symbols-outlined text-[#2563eb] text-lg">history</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-[0.625rem] font-bold text-white uppercase truncate">{state.file_name}</div>
|
||||||
|
<div className="text-[0.5rem] text-white/40 geist-mono uppercase">{new Date(state.updated_at).toLocaleDateString()}</div>
|
||||||
|
</div>
|
||||||
|
{selectedStateId === state.id && <div className="w-2 h-2 rounded-full bg-[#2563eb] shadow-[0_0_8px_#2563eb]" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
|
)) : (
|
||||||
|
<div className="p-8 rounded-xl border border-dashed border-white/5 text-center">
|
||||||
|
<span className="material-symbols-outlined text-white/10 text-3xl mb-2">timer</span>
|
||||||
|
<div className="text-[0.5rem] font-black text-white/20 uppercase tracking-[0.2em] geist-mono">No Cached States</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Launch Controls */}
|
||||||
|
<div className="flex items-center gap-6 mt-4">
|
||||||
|
<FocusableItem focusKey="START_SESSION_BTN" onEnterPress={handleLaunch} onClick={handleLaunch}>
|
||||||
|
{(focused: boolean) => (
|
||||||
|
<button className={`px-12 h-16 rounded-2xl font-black text-[0.875rem] uppercase tracking-[0.4em] geist-mono transition-all flex items-center gap-4 ${focused ? 'bg-[#2563eb] text-white scale-110 shadow-[0_0_40px_rgba(37,99,235,0.4)] ring-4 ring-white/20' : 'bg-white text-black'}`}>
|
||||||
|
<span className="material-symbols-outlined filled" style={{ fontVariationSettings: "'FILL' 1" }}>play_arrow</span>
|
||||||
|
Start Session
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
|
<FocusableItem focusKey="CANCEL_SESSION_BTN" onEnterPress={onClose} onClick={onClose}>
|
||||||
|
{(focused: boolean) => (
|
||||||
|
<button className={`px-8 h-16 rounded-2xl font-black text-[0.75rem] uppercase tracking-[0.2em] geist-mono transition-all border ${focused ? 'bg-white/10 border-white text-white' : 'bg-transparent border-white/10 text-white/30'}`}>
|
||||||
|
Abort
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hint bar */}
|
||||||
|
<div className="absolute bottom-12 left-12 flex items-center gap-8">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-6 h-6 rounded flex items-center justify-center bg-white/10 text-[10px] font-bold text-white/50 border border-white/5">ENTER</div>
|
||||||
|
<div className="text-[0.625rem] font-black text-white/20 uppercase tracking-widest">Select Data</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-6 h-6 rounded flex items-center justify-center bg-white/10 text-[10px] font-bold text-white/50 border border-white/5">ESC</div>
|
||||||
|
<div className="text-[0.625rem] font-black text-white/20 uppercase tracking-widest">Back to Archives</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(() => {
|
||||||
|
const platformSlug = game.system.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const defaultExt = ['nds', 'nintendo-ds'].includes(platformSlug) ? 'nds'
|
||||||
|
: ['psx', 'playstation', 'saturn', '3do', 'sega-cd'].includes(platformSlug) ? 'chd'
|
||||||
|
: 'zip';
|
||||||
|
const ext = game.fsName ? game.fsName.split('.').pop() : defaultExt;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmulatorPlayer
|
||||||
|
romId={gameId}
|
||||||
|
romUrl={rommApiClient.getRomFileUrl(gameId, `game.${ext}`)}
|
||||||
|
platformSlug={platformSlug}
|
||||||
|
saveId={selectedSaveId}
|
||||||
|
stateId={selectedStateId}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FocusContext.Provider>
|
</FocusContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
217
src/components/EmulatorPlayer.tsx
Normal file
217
src/components/EmulatorPlayer.tsx
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { rommApiClient } from '../api/client';
|
||||||
|
import { pause, resume } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
|
|
||||||
|
interface EmulatorPlayerProps {
|
||||||
|
romId: string; // Kept for Save uploading
|
||||||
|
romUrl: string; // Exact static asset URL
|
||||||
|
platformSlug: string;
|
||||||
|
core?: string;
|
||||||
|
saveId?: number;
|
||||||
|
stateId?: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmulatorPlayer = ({ romId, romUrl, platformSlug, core, saveId, stateId, onClose }: EmulatorPlayerProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
// 1. Pause spatial navigation
|
||||||
|
pause();
|
||||||
|
|
||||||
|
// 2. Listen for messages from the iframe
|
||||||
|
const handleMessage = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type === 'EJS_SAVE') {
|
||||||
|
try {
|
||||||
|
const blob = new Blob([event.data.data], { type: 'application/octet-stream' });
|
||||||
|
await rommApiClient.uploadSave(romId, blob, core || 'default');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('EmulatorJS: Failed to upload save to RomM:', err);
|
||||||
|
}
|
||||||
|
} else if (event.data.type === 'EJS_EXIT') {
|
||||||
|
onClose();
|
||||||
|
} else if (event.data.type === 'EJS_ERROR') {
|
||||||
|
console.error('EmulatorJS: Error from iframe:', event.data.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
resume();
|
||||||
|
};
|
||||||
|
}, [romId, platformSlug, core, saveId, stateId, onClose]);
|
||||||
|
|
||||||
|
const coreMapping: Record<string, string> = {
|
||||||
|
'nes': 'fceumm',
|
||||||
|
'nintendo-entertainment-system': 'fceumm',
|
||||||
|
'snes': 'snes9x',
|
||||||
|
'super-nintendo-entertainment-system': 'snes9x',
|
||||||
|
'gb': 'gambatte',
|
||||||
|
'game-boy': 'gambatte',
|
||||||
|
'gbc': 'gambatte',
|
||||||
|
'game-boy-color': 'gambatte',
|
||||||
|
'gba': 'mgba',
|
||||||
|
'game-boy-advance': 'mgba',
|
||||||
|
'nds': 'desmume2015',
|
||||||
|
'nintendo-ds': 'desmume2015',
|
||||||
|
'nintendo-dsi': 'desmume2015',
|
||||||
|
|
||||||
|
// Sega Systems
|
||||||
|
'genesis': 'genesis_plus_gx',
|
||||||
|
'megadrive': 'genesis_plus_gx',
|
||||||
|
'sega-genesis': 'genesis_plus_gx',
|
||||||
|
'sega-mega-drive': 'genesis_plus_gx',
|
||||||
|
'mega-drive': 'genesis_plus_gx',
|
||||||
|
'smd': 'genesis_plus_gx',
|
||||||
|
'sms': 'genesis_plus_gx',
|
||||||
|
'sega-master-system': 'genesis_plus_gx',
|
||||||
|
'master-system': 'genesis_plus_gx',
|
||||||
|
'gg': 'genesis_plus_gx',
|
||||||
|
'game-gear': 'genesis_plus_gx',
|
||||||
|
'sega-game-gear': 'genesis_plus_gx',
|
||||||
|
'sega-cd': 'genesis_plus_gx',
|
||||||
|
'mega-cd': 'genesis_plus_gx',
|
||||||
|
'sega-32x': 'picodrive',
|
||||||
|
'32x': 'picodrive',
|
||||||
|
'sega-cd-32x': 'picodrive',
|
||||||
|
'mega-cd-32x': 'picodrive',
|
||||||
|
'saturn': 'yabause',
|
||||||
|
'sega-saturn': 'yabause',
|
||||||
|
'sg-1000': 'genesis_plus_gx',
|
||||||
|
'sg1000': 'genesis_plus_gx',
|
||||||
|
|
||||||
|
// Nintendo / NEC / Others
|
||||||
|
'n64': 'mupen64plus_next',
|
||||||
|
'nintendo-64': 'mupen64plus_next',
|
||||||
|
'virtual-boy': 'mednafen_vb',
|
||||||
|
'virtualboy': 'mednafen_vb',
|
||||||
|
'vb': 'mednafen_vb',
|
||||||
|
'turbografx-16': 'mednafen_pce_fast',
|
||||||
|
'turbografx': 'mednafen_pce_fast',
|
||||||
|
'tg16': 'mednafen_pce_fast',
|
||||||
|
'pc-engine': 'mednafen_pce_fast',
|
||||||
|
'pce': 'mednafen_pce_fast',
|
||||||
|
'pc-engine-cd': 'mednafen_pce_fast',
|
||||||
|
|
||||||
|
// Sony
|
||||||
|
'psx': 'pcsx_rearmed',
|
||||||
|
'playstation': 'pcsx_rearmed',
|
||||||
|
'sony-playstation': 'pcsx_rearmed',
|
||||||
|
'psp': 'ppsspp',
|
||||||
|
'playstation-portable': 'ppsspp',
|
||||||
|
'sony-playstation-portable': 'ppsspp',
|
||||||
|
|
||||||
|
// Panasonic / 3DO
|
||||||
|
'3do': 'opera',
|
||||||
|
'3do-interactive-multiplayer': 'opera',
|
||||||
|
|
||||||
|
// Atari
|
||||||
|
'atari-2600': 'stella2014',
|
||||||
|
'atari-5200': 'a5200',
|
||||||
|
'atari-7800': 'prosystem',
|
||||||
|
'jaguar': 'virtualjaguar',
|
||||||
|
'atari-jaguar': 'virtualjaguar',
|
||||||
|
'lynx': 'handy',
|
||||||
|
'atari-lynx': 'handy',
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
'arcade': 'mame2003_plus',
|
||||||
|
'mame': 'mame2003_plus',
|
||||||
|
'neogeo': 'fbneo',
|
||||||
|
'neo-geo': 'fbneo',
|
||||||
|
'colecovision': 'gearcoleco',
|
||||||
|
'vectrex': 'vecx',
|
||||||
|
'msx': 'fmsx',
|
||||||
|
'msx1': 'fmsx',
|
||||||
|
'msx2': 'fmsx',
|
||||||
|
'neo-geo-pocket': 'mednafen_ngp',
|
||||||
|
'ngp': 'mednafen_ngp',
|
||||||
|
'neo-geo-pocket-color': 'mednafen_ngp',
|
||||||
|
'ngpc': 'mednafen_ngp',
|
||||||
|
'odyssey-2': 'o2em',
|
||||||
|
'odyssey2': 'o2em',
|
||||||
|
'videopac': 'o2em'
|
||||||
|
};
|
||||||
|
const activeCore = core || coreMapping[platformSlug] || 'default';
|
||||||
|
|
||||||
|
// Map slugs to exactly match the generated bios/XXXX.zip names
|
||||||
|
const biosFolderMapping: Record<string, string> = {
|
||||||
|
'3do': '3do',
|
||||||
|
'3do-interactive-multiplayer': '3do',
|
||||||
|
'saturn': 'saturn',
|
||||||
|
'sega-saturn': 'saturn',
|
||||||
|
'sega-cd': 'segacd',
|
||||||
|
'mega-cd': 'segacd',
|
||||||
|
'sega-cd-32x': 'segacd',
|
||||||
|
'mega-cd-32x': 'segacd',
|
||||||
|
'turbografx-cd': 'turbografx-cd',
|
||||||
|
'pc-engine-cd': 'turbografx-cd',
|
||||||
|
'psx': 'psx',
|
||||||
|
'playstation': 'psx',
|
||||||
|
'sony-playstation': 'psx',
|
||||||
|
'ps2': 'ps2',
|
||||||
|
'psp': 'psp',
|
||||||
|
'playstation-portable': 'psp',
|
||||||
|
'sony-playstation-portable': 'psp',
|
||||||
|
'gba': 'gba',
|
||||||
|
'game-boy-advance': 'gba',
|
||||||
|
'nds': 'nds',
|
||||||
|
'nintendo-ds': 'nds',
|
||||||
|
'nintendo-dsi': 'nintendo-dsi',
|
||||||
|
'msx': 'msx',
|
||||||
|
'msx1': 'msx',
|
||||||
|
'msx2': 'msx',
|
||||||
|
'amiga': 'amiga',
|
||||||
|
'jaguar': 'jaguar',
|
||||||
|
'atari-jaguar': 'jaguar',
|
||||||
|
'odyssey-2': 'odyssey-2',
|
||||||
|
'odyssey2': 'odyssey-2',
|
||||||
|
'videopac': 'odyssey-2',
|
||||||
|
'colecovision': 'colecovision',
|
||||||
|
'fds': 'fds',
|
||||||
|
'neogeoaes': 'neogeoaes',
|
||||||
|
'neogeo': 'arcade', // Fallback for neogeo.zip
|
||||||
|
'neo-geo': 'arcade',
|
||||||
|
'arcade': 'arcade',
|
||||||
|
'dc': 'dc'
|
||||||
|
};
|
||||||
|
const activeBios = biosFolderMapping[platformSlug];
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
windowNum: 1,
|
||||||
|
language: 'en-US',
|
||||||
|
showControls: true,
|
||||||
|
showMenu: true,
|
||||||
|
allowFullscreen: true,
|
||||||
|
screenshot: true,
|
||||||
|
cache: false,
|
||||||
|
gameUrl: romUrl,
|
||||||
|
gameId: romId,
|
||||||
|
system: activeCore,
|
||||||
|
core: activeCore,
|
||||||
|
dataPath: '/emu/',
|
||||||
|
...(activeBios && { biosUrl: `/bios/${activeBios}.zip` }),
|
||||||
|
...(saveId && { loadSaveUrl: rommApiClient.getSaveFileUrl(saveId) }),
|
||||||
|
...(stateId && { loadStateUrl: rommApiClient.getStateFileUrl(stateId) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const iframeSrc = `/emulator.html?v=${Date.now()}#${encodeURIComponent(JSON.stringify(config))}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full bg-black relative">
|
||||||
|
<iframe
|
||||||
|
src={iframeSrc}
|
||||||
|
className="w-full h-full border-0"
|
||||||
|
allow="autoplay; fullscreen; cross-origin-isolated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Fallback exit button */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 z-[300] w-10 h-10 flex items-center justify-center rounded-full bg-black/50 text-white/50 hover:bg-red-500 hover:text-white transition-all opacity-0 hover:opacity-100"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,16 +9,48 @@ export default defineConfig({
|
|||||||
watch: {
|
watch: {
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
},
|
},
|
||||||
|
cors: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/assets/romm': {
|
|
||||||
target: 'https://retro.chieflix.com',
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
},
|
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'https://retro.chieflix.com',
|
target: 'https://retro.chieflix.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
configure: (proxy: any) => {
|
||||||
|
proxy.on('proxyReq', (proxyReq: any, req: any) => {
|
||||||
|
// New Token Path Strategy: Extract token from URL path to prevent EmulatorJS extension parsing bugs
|
||||||
|
// Match format: /api/roms/123/content/token/XYZ/rom.chd
|
||||||
|
const tokenMatch = req.url?.match(/\/content\/token\/([^/]+)\/(.*)/);
|
||||||
|
if (tokenMatch) {
|
||||||
|
const token = tokenMatch[1];
|
||||||
|
const restOfPath = tokenMatch[2];
|
||||||
|
|
||||||
|
// Inject the token securely
|
||||||
|
proxyReq.setHeader('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
// Rewrite the proxy request path to hide the token and the false filename from the RomM backend
|
||||||
|
// E.g., /api/roms/123/content/token/TOKEN/game.nds -> /api/roms/123/content
|
||||||
|
let newPath = req.url.replace(/\/content\/token\/[^/]+\/.*/, '/content');
|
||||||
|
|
||||||
|
// EJS loader often fires HEAD requests to check content-length. RomM FastAPI natively drops HEAD methods on binary routes returning 404s.
|
||||||
|
// Mutate HEAD queries safely into GET queries so the backend responds with accurate Header structures.
|
||||||
|
if (req.method === 'HEAD') {
|
||||||
|
proxyReq.method = 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyReq.path = newPath;
|
||||||
|
} else {
|
||||||
|
// Fallback for regular query params (Saves/States)
|
||||||
|
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
||||||
|
const token = url.searchParams.get('access_token');
|
||||||
|
if (token) {
|
||||||
|
proxyReq.setHeader('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
proxy.on('proxyRes', (proxyRes: any) => {
|
||||||
|
proxyRes.headers['Cross-Origin-Resource-Policy'] = 'cross-origin';
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user