Skip to content

Emulators

Retro-Jack edited this page May 8, 2026 · 71 revisions

Emulators

Three emulators are vendored under emulators/ with bundled local ROMs. Once python3 -m http.server is running, they need no network — every byte is on the same origin.

Folder System Vendor Runtime ROMs
emulators/jsbeeb/ BBC Micro mattgodbolt/jsbeeb v1.12.0 Vite-built dist/ 10× .ssd in dist/discs/<vendor>/
emulators/jsnes/ NES bfirsh/jsnes v2.1.0 dist/jsnes.min.js (~135 KB) 10× .nes in roms/
emulators/genesis/ Sega Genesis lrusso/Genesis (Emscripten port of PicoDrive) Genesis.min.js (~2.1 MB) 10× .bin in roms/

How a game launches

  1. The user picks a number in a GAMES menu.
  2. N.bat runs the named launcher, e.g. sonic1.
  3. sonic1.bat is a link: entry pointing at the emulator's URL.
  4. The terminal opens that URL in a new tab via window.open(link, "_blank").

For NES and Genesis the URL pattern is:

../emulators/<name>/play.html?game=<key>

The wrapper page reads ?game=<key>, looks it up in games.json, fetches roms/<key>.<ext>, and starts the emulator.

For BBC Micro the URL pattern is:

../emulators/jsbeeb/dist/?disc1=<vendor>/<title>.ssd&autoboot

?autoboot triggers *EXEC !BOOT. Drop it to land at the BASIC > prompt instead.

Adding a new emulator

Same recipe each time:

  1. Vendor the emulator under emulators/<name>/. Drop the upstream node_modules, the inner .git, and any .gitignore that excludes deploy artefacts (e.g. *.ssd, /dist).

  2. Strip telemetry from the source's index.html before deploying (jsbeeb shipped Google Analytics; we removed it).

  3. Write play.html — a thin wrapper that:

    • reads ?game=<key>,
    • fetches games.json,
    • looks up {title, rom},
    • boots the emulator with that ROM.

    Use absolute paths via location.pathname.replace(/[^/]*$/, "") for any framework that does its own path mangling (EmulatorJS's loader does — bit us once).

  4. Bundle the ROMs under emulators/<name>/roms/. archive.org doesn't set CORS for direct browser fetch, so play.html can't pull at runtime — they must be on the same origin.

  5. Wire fs.js: each game's .bat gets a link: field pointing at ../emulators/<name>/play.html?game=<key>.

Gotchas

jsbeeb's empty-schema disc URLs don't unzip. ?disc1=path/file.zip falls through to the default: branch in loadDiscImage() (main.js), which calls fdc.load("discs/" + path) and hands raw bytes to discFor(). Only the sth: / http: / https: schemas unzip. Workaround: extract the inner .ssd from each upstream .zip and serve that raw — URL becomes ?disc1=<path>.ssd&autoboot.

Vendored .gitignore files silently exclude deploy artefacts. emulators/jsbeeb/.gitignore had /dist (upstream's build-artefact rule); dist/discs/.gitignore had *.ssd (anti-piracy guard). Both meant the deployed jsbeeb on GitHub was actually broken for several commits — only worked locally where the files existed on disk. When vendoring an upstream tree, audit every .gitignore it ships.

EmulatorJS was tried first and dropped. Its data/loader.js prepends "../" to relative EJS_pathtodata, which conflicts with how dynamic-import vs. document-relative paths resolve, blank-screening silently. JSNES's pure-JS API was a half-hour drop-in instead.

archive.org collection access varies. nintendo-entertainment-system-nes-roms-europeusa and sega-genesis-romset-ultra-usa allow direct file download via https://archive.org/download/<item>/<path>. Some other items return 401/403 (auth-gated). Probe with curl -I before scripting bulk downloads.

Testing changes

python3 -m http.server 8765

After editing fs.js or any emulator wrapper, hard-refresh the prompt page (Ctrl+Shift+R). Browsers aggressively cache fs.js between visits.

For BBC Micro there's a headless test harness from upstream at emulators/jsbeeb/.claude/skills/emulator/SKILL.md. It needs the upstream dev tree (src/, plus npm install sharp) which we strip in the deploy build, so re-clone fresh if you want to drive the BBC headlessly.

Clone this wiki locally