Skip to content

Emulators

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

Emulators

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

Folder System Source Runtime ROMs / discs
emulators/jsbeeb/ BBC Micro mattgodbolt/jsbeeb v1.12.0 Vite-built dist/ 10× .ssd in dist/discs/<publisher>/
emulators/archimedes-live/ Acorn Archimedes (RISC OS) pdjstone/archimedes-live Arculator WASM bundle (mirror of archi.medes.live) 10× zipped discs in software/ + 13 ROM images under emu/roms/
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/
emulators/javatari/ Atari 2600 ppeccin/javatari.js v5.0.4 javatari.js 10× .a26 in roms/
emulators/jsdos/ MS-DOS caiiiycuk/js-dos v7.0.0 js-dos.js + wdosbox.wasm 10× .jsdos bundles in bundles/
emulators/webmsx/ MSX / MSX2 / MSX2+ ppeccin/WebMSX (mirror of webmsx.org) Single 5 MB index.html (engine + UI + BIOSes inlined) 10× MSX1 zips in games/msx1/ + 10× MSX2 zips in games/msx2/

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").

URL patterns vary by emulator:

Emulator URL pattern
jsnes, genesis, javatari, jsdos ../emulators/<name>/play.html?game=<key> — wrapper reads ?game=<key>, looks up {title, rom} in games.json, fetches roms/<key>.<ext> (or bundles/<key>.jsdos), starts the emulator
jsbeeb ../emulators/jsbeeb/dist/?disc1=<publisher>/<title>.ssd&autoboot?autoboot triggers *EXEC !BOOT; drop it to land at the BASIC > prompt
archimedes-live ../emulators/archimedes-live/#disc=<catalogue-id>&autoboot — uses URL hash (not query string); <catalogue-id> is looked up in the bundled software/software.json and per-title ff-ms is honoured automatically so games skip the RISC OS boot
webmsx ../emulators/webmsx/?ROM=games/<sub>/<name>.zip[&M=MSX1] — WebMSX auto-detects the cartridge mapper; MSX1 menu adds &M=MSX1 for authentic 8KB BIOS; SD Snatcher uses ?ANY=… because its zip contains 4 .DSK images, not a cart ROM

DESKTOP vs PROMPT entries

Each self-hosted emulator's GAMES menu has a "boot a clean machine" entry as item N+1:

  • PROMPT for command-line systems — labels match what the user actually sees: BBC boots to BBC BASIC >, MSX1/MSX2 boots to MSX BASIC Ok.
  • DESKTOP for GUI systems — only archimedes-live so far, boots a stock A3000 to the RISC OS desktop.

Convention going forward: PROMPT for > / Ok style command-line, DESKTOP for graphical OS shells.

Adding a new emulator

Same recipe each time:

  1. Copy the emulator's deployed bundle into emulators/<name>/ (mirror with wget if it's hosted, or build from source if you must — archimedes-live and webmsx were both straightforward wget mirrors of the public deployment). 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 and webmsx both shipped Google Analytics; we removed both).

  3. Add the standard <noscript> overlay to every new HTML entry point — copy the block verbatim from any existing emulator page; it's identical across the project.

  4. Write play.html if the emulator needs a key-based launcher (NES, Genesis, Atari 2600, jsdos pattern). Skip this if the emulator's own page already accepts URL params (BBC, Archimedes, WebMSX patterns). For wrappers:

    • 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).

  5. Bundle the ROMs under emulators/<name>/<roms-dir>/. archive.org and most preservation sites (e.g. file-hunter.com for MSX, files-archi.medes.live for Archimedes) don't set CORS for direct browser fetch, so wrappers can't pull at runtime — every byte must be on the same origin.

  6. Wire fs.js: each game's .bat gets a link: field pointing at the URL pattern above. Per-system GAMES menu follows the gold-standard 45-char CP437 box format with 10 titles + a PROMPT/DESKTOP entry.

Multi-model platforms

Some platforms have multiple machines under the same vendor — e.g. ATARI has 800XL and ST as siblings, ACORN has BBC and ARCHIMEDES, MSX has MSX1 and MSX2. The hierarchical pattern: a parent menu with 1.<MODEL1> / 2.<MODEL2> rows, each pointing into its own GAMES dir. See prompt/javascript/fs.js for the exact tree shape.

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.

Copied .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 copying an upstream tree, audit every .gitignore it ships.

Cross-origin ROM fetch is consistently blocked. Tested for archi.medes.live's files-archi.medes.live (no CORS), file-hunter's file-hunter.com/MSX/ (no CORS), archive.org direct downloads (varies — some allow, some 401). Always mirror locally; don't rely on runtime CORS fetches. WebMSX's local-domain fallback (SOFTWARE_BASE_DEV='software/', ROM_BASE_DEV='emu/roms/') and Arculator's equivalent are both lifesavers — neither needed a source patch when self-hosted off-domain.

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.

WebMSX bundles everything inline. The deployed index.html is ~5 MB with the engine, UI, and commercial MSX/MSX2/MSX2+ BIOSes all baked in as JS. Mirror is just that one file plus the manifest, cache.manifest, and two icons — no separate .wasm to chase. Strip the inline Google Analytics block (find the (function(i,s,o,g,r,a,m){…}) snippet and replace with a /* GA stripped */ comment).

MSX2 disk titles need ?ANY= not ?ROM=. SD Snatcher's zip contains 4 .DSK images (3 game disks + USERDISK), not a cart ROM. WebMSX's ?ROM= parameter assumes cartridges; ?ANY= (alias for AUTODETECT_URL) inspects the contents and routes multi-disk zips to the Drive Stack so the user can swap mid-game.

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