-
Notifications
You must be signed in to change notification settings - Fork 0
Emulators
24 emulators are copied under emulators/ with bundled local games/discs/cartridges, driving 26 sub-systems. Seven of the emulator dirs are the VICE-libretro family — PET, VIC-20, MAX, C64, C16, Plus/4, C128 — and an eighth EmulatorJS bundle is coleco/ (gearcoleco). All eight share one copy of the EmulatorJS framework via emulators/_shared-ejs/ (saves ~13 MB vs per-bundle copies; the 5 unique VICE cores + the gearcoleco core live alongside the framework there too). The Intellivision bundle (intellivision/) is its own thing — jzIntv compiled to WebAssembly with a custom emscripten loader, since EmulatorJS doesn't ship an Intellivision core. Once python3 -m http.server is running (or ./serve.sh), 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/js7800/ |
Atari 7800 | raz0red/JS7800 v0.0.9 |
js/js7800.min.js + js/bupboop.wasm (YM-2151 sound chip, ~590 KB) |
10× .a78 in roms/
|
emulators/jsdos/ |
MS-DOS | caiiiycuk/js-dos v7.0.0 |
js-dos.js + wdosbox.wasm
|
10 flat titles + DUKE submenu (3) + KEEN submenu (7) — 20 .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/
|
emulators/jsvecx/ |
GCE Vectrex (Smith Engineering, 1982) | DrSnuggles/jsvecx (raz0red's JS port of Valavan Manohararajah's VecX) |
index.html + js/jsvecx.js (~31 MB total) |
~100 commercial+homebrew .bin organised by publisher/author folder under roms/
|
emulators/atari800/ |
Atari 400 + Atari 800XL (same emulator, different boot machine) | atari800/atari800 v5.2.0 (GPL-2.0+) — built from source to WASM via emscripten |
atari800.js (186 KB) + atari800.wasm (1.7 MB), AltirraOS-XL 3.41 + AltirraOS-800 (OS-B) + AltirraBASIC baked in (no copyrighted Atari OS ROM needed) |
800XL: 9× .atr + 1× .rom cart (disk-era titles). 400: 5× .rom carts + 5× .atr (cart-era titles, shares starraid.rom with 800XL). |
emulators/c64/ |
Commodore 64 | EmulatorJS (GPL-3.0, EmulatorJS/EmulatorJS — modern fork of emularity) + VICE x64 libretro core (GPL-2.0). Mirrored from cdn.emulatorjs.org/stable/. |
play.html sets EJS globals (EJS_pathtodata, EJS_core='vice_x64', EJS_gameUrl) and injects ejs/data/loader.js
|
10× .d64 disk images (games/) — VICE auto-mounts and runs |
emulators/estyjs/ |
Atari ST | Kai Eckert / Darren Coles' EstyJS 2.0 (GPL-2.0+) | 14 pure-JS modules in estyjs/ (~456 KB) + etos256us.img (EmuTOS) |
10× .st floppy images in disks/
|
emulators/jsspeccy/ |
Sinclair Spectrum | Matt Westcott / gasman's JSSpeccy 3 v3.2 (GPL-3.0) | WASM Z80 core in Web Worker (jsspeccy-core.wasm 139 KB + jsspeccy-worker.js) + jsspeccy.js shell |
10× .z80 snapshots in games/, 5 ROMs + 5 tape-loader SZX in jsspeccy/
|
emulators/jtyone/ |
Sinclair ZX81 | Simon Holdsworth's JtyOne (port of Mike Wynne's EightyOne) | Pure-JS Z80 core zx81_emu.js (141 KB) + roms/zx81.rom (8 KB) |
10× .p.hex tapes in tapes/ (raw bytes hex-encoded — JtyOne's load format) |
emulators/xroar/ |
Tandy Color Computer | Ciaran Anscomb's XRoar v1.10 (GPL-3.0+) |
xroar.js (180 KB) + xroar.wasm (1.2 MB) + 9 system ROMs |
10× .ccc cartridges in roms/
|
emulators/pet/ |
Commodore PET (4032 default, 3032 most games, per-game override for PETSCII Robots → 4032) | EmulatorJS + VICE xpet libretro core (GPL-2.0). Mirrored from cdn.emulatorjs.org/stable/. |
play.html sets EJS globals incl. vice_pet_model + reads optional per-game model field from games.json |
9× .prg classics + PETSCII Robots .d64 (games/) |
emulators/vic20/ |
Commodore VIC-20 | EmulatorJS + VICE xvic libretro core (GPL-2.0) |
play.html sets EJS_core='vice_xvic' + vice_vic20_memory_expansions='all' for full 35K expansion |
10× cart .prg (games/) |
emulators/max/ |
Commodore MAX Machine (1982 Japan-only console) | EmulatorJS + VICE x64 libretro core in Ultimax mode (vice_c64_model='ultimax') |
play.html sets EJS globals + Ultimax model |
8× .crt carts + MAX BASIC cart (games/). Sources: TOSEC + Internet Archive Slalom + bank-extracted from the Multimax EasyFlash compilation. |
emulators/c64/ |
Commodore 64 | EmulatorJS + VICE x64 libretro core (GPL-2.0). Mirrored from cdn.emulatorjs.org/stable/. |
play.html sets EJS globals (EJS_pathtodata, EJS_core='vice_x64', EJS_gameUrl) and injects ejs/data/loader.js
|
10× .d64 disk images (games/) — VICE auto-mounts and runs |
emulators/c16/ |
Commodore 16 | EmulatorJS + VICE xplus4 libretro core in C16 mode (vice_plus4_model='c16pal') |
play.html shares xplus4 core w/ Plus/4, switches into 16K C16 model |
10× .prg first-in-series C16 16K titles (games/) |
emulators/plus4/ |
Commodore Plus/4 | EmulatorJS + VICE xplus4 libretro core (GPL-2.0) |
play.html sets EJS_core='vice_xplus4'
|
10× .prg titles (games/) |
emulators/c128/ |
Commodore 128 | EmulatorJS + VICE x128 in native mode (vice_c128_go64='disabled') |
play.html sets EJS_core='vice_x128' + per-game video field toggles VICII (40-col) vs VDC (80-col); CSS scaleY(2.084) on the canvas + overflow:hidden on #game works around VICE x128's 856×288 framebuffer letterboxing in 4:3 |
5 commercial C128-distinctive builds + 5 homebrew 80-col titles (games/) |
emulators/coleco/ |
ColecoVision (Coleco, August 1982) | EmulatorJS + gearcoleco libretro core (GPL-3.0, Drhelius). Mirrored from cdn.emulatorjs.org/stable/. |
play.html sets EJS_core='coleco' + EJS_biosUrl='colecovision.rom' (bundle-local, ©1982 Coleco, MD5 2c66f5911e5b42b8ebe113403548eee7) |
10× .col cart images in games/ — 1982/83 launch-era classics, Donkey Kong is the pack-in title |
emulators/amstradcpc/ |
Amstrad CPC (1984, 6128 / 128 K / Locomotive BASIC 1.1) | floooh/chips-test "tiny8bit" CPC WASM (MIT, Andre Weissflog). Mirrored from floooh.github.io/tiny8bit/. |
Custom play.html rewrites the URL's ?game=KEY into ?file=<dsk>&input=<boot-cmd> so floooh's sokol_args reads them at WASM init. No BIOS needed; tiny8bit auto-mounts .dsk to drive A:. No ?game= → plain Locomotive BASIC. |
10× .dsk disk images in games/ — first-in-series CPC classics from TOSEC |
emulators/intellivision/ |
Mattel Intellivision (1979) | jzIntv (Joe Zbiciak) compiled to WebAssembly. WASM bundle mirrored from mholzinger/intellivision-overlay-editor. |
Custom play.html — fetches exec.bin + grom.bin + selected ROM, hands them to Module.launchJzintv() which writes them into the WASM FS and runs jzintv. Not an EmulatorJS-framework bundle (no coreintv core exists in EJS). |
10× .int cart images in games/ — Astrosmash, BurgerTime, TRON, Snafu, B-17 Bomber, Night Stalker, Adventure (AD&D), Atlantis, Lock 'N' Chase, Star Strike |
Shared-EJS note: all 7 VICE bundles + coleco/ share one copy of the EmulatorJS framework at emulators/_shared-ejs/, along with all 5 unique VICE cores and the gearcoleco core. Each <bundle>/ keeps only play.html, controls.html, games.json, and games/* (plus colecovision.rom for coleco). Total shared dir: ~9 MB.
- The user picks a number in a
GAMESmenu. -
N.batruns the named launcher, e.g.sonic1. -
sonic1.batis alink:entry pointing at the emulator's URL. - 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, js7800, jsdos, estyjs, jsspeccy, jtyone, xroar, atari800
|
../emulators/<name>/play.html?game=<key> — wrapper reads ?game=<key>, looks up {title, rom} in games.json, fetches the ROM, starts the emulator. For atari800, games.json entries can include a cartType field (e.g. Star Raiders cartType:1 = Standard 8KB) — the wrapper passes -cart-type N -cart /game.rom for headerless cart dumps and /game.atr for disk images. atari800 also accepts a ?machine= URL param to pick the boot machine: 400/800 (→ -atari / OS-B / AltirraOS-800), xl/800xl (→ -xl / AltirraOS-XL, the default), xe/130xe (→ -xe), xegs (→ -xegs). One atari800.wasm build, two visible sub-systems (HOMECOMP\ATARI\400 and HOMECOMP\ATARI\800XL), separated only by the URL param each menu's .bat links pass. |
pet, vic20, max, c64, c16, plus4, c128
|
../emulators/<name>/play.html?game=<key> — same wrapper pattern as above. games.json entries support optional per-game model (PET 3032 default → 4032 for PETSCII Robots) and video (C128 VICII default → VDC for 80-col titles) overrides. All seven VICE bundles ship the same input config: keyboardInput: 'enabled' + altKeyboardInput: 'enabled' (typed keys → VICE keyboard callback) + vice_joyport_type: '1' for the six with a joystick port (PET has no joystick). Numpad 8/4/6/2 drive joystick directions; 0 or 5 is fire; everything else types. Esc is browser-captured so RUN/STOP is remapped to Scroll Lock (+ Pause/Break + a top-left soft button) via emulators/_shared/genx-vice-softkeys.js. |
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 |
jsvecx |
../emulators/jsvecx/?rom=<dir>/<title> — note: use index.html (the default /), never seamless.html. seamless is iframe-embed-only; standalone it loads the ROM but renders the canvas at zero size due to a sizer/init race |
amstradcpc |
../emulators/amstradcpc/play.html?game=<key> — wrapper looks up {title, rom, input} in games.json and rewrites the URL to ?file=<rom>&input=<boot-cmd> before tiny8bit's WASM init. floooh's sokol_args reads ?file= to mount a .dsk and ?input= to type a run"<file> boot command. No ?game= lands at plain Locomotive BASIC. |
intellivision |
../emulators/intellivision/play.html?game=<key> — wrapper fetches exec.bin + grom.bin + the chosen .int, writes them into the WASM FS, calls Module.launchJzintv(). Custom desk.kbd keyboard map preloaded into jzintv.data provides numpad-as-keypad + arrow-keys-on-disc mappings the stock minimal map omits. |
Each emulator's GAMES menu has a "boot a clean machine" entry as item N+1:
-
PROMPT / BASIC for command-line systems — labels match what the user actually sees:
BBCboots to BBC BASIC>,MSX1/MSX2boots to MSX BASICOk,COCOboots to Color BASIC,800XLboots to AltirraOS-XLREADY(entry label:BASIC),SPECTRUM/ZX81/CPCeach boot to their ROM BASIC (ZX BASIC / ZX81 BASIC / Locomotive BASIC) when the emulator is launched without a?game=query string. All eight VICE-family + 800XL bundles route through a smallempty.prgfor the BASIC entry; Spectrum/ZX81/CPC pass no query at all (their emulators already drop to ROM BASIC with no software inserted). -
DESKTOP for GUI systems —
archimedes-liveboots a stock A3000 to the RISC OS desktop;estyjsboots EmuTOS.
Convention going forward: PROMPT for > / Ok style command-line, DESKTOP for graphical OS shells.
Every emulator dir has a controls.html documenting the keyboard / joystick mapping for that system. Same dark-amber DOS-flavoured style across all 20; each page has a system header, an intro line, a few sections of <kbd>-styled key / action tables, and a footer linking back to the GenX-DOS site. The page-level CSS lives in emulators/_shared/genx-controls.css (one shared file, all pages link to it).
Every emulator's main entry HTML carries a small bottom-right controls link (fixed position, semi-transparent, Wikipedia-style external-link SVG icon). Clicking it opens that emulator's controls.html in a new tab. The link sits in the entry HTML itself (play.html for the wrapped emulators, the upstream index.html for the URL-driven ones — jsbeeb, archimedes-live, webmsx, jsvecx) — not in the virtual filesystem. The DOS prompt's dir output shows only .bat launchers and menu scripts, never raw .html files; HTML stays an in-emulator concept, reachable from the emulator screen rather than from the menu tree. The link's styles live in emulators/_shared/genx-controls-link.css; every entry HTML includes that one stylesheet plus an inline <a class="gx-controls-link"> containing the SVG.
When adding a new emulator, write its controls.html alongside the other site assets, have it <link> the shared genx-controls.css, and inject the same <a class="gx-controls-link"> snippet + the shared genx-controls-link.css stylesheet into its entry HTML. Don't add a .bat for it in fs.js.
Same recipe each time:
-
Copy the emulator's deployed bundle into
emulators/<name>/(mirror withwgetif it's hosted, or build from source if you must). Drop the upstreamnode_modules, the inner.git, any.gitignorethat excludes deploy artefacts, and any upstreamREADME.md/ dev tooling (.claude/, source-map.mapfiles, etc.). -
Strip telemetry and any other external runtime fetches from the source's
index.htmland bundled JS/CSS before deploying. Patterns to check for (each one has bitten us at least once):- Inline analytics (
gtag(,umami, etc.) — jsbeeb and webmsx both shipped Google Analytics; removed both. - Framework-version checks (
fetch("https://cdn.<framework>.org/.../version.json")) — EmulatorJS in c64/vic20 ran this on every bootstrap. PatchedcheckForUpdates()to a no-opreturn;. - Hardcoded NetPlay / multiplayer relay servers (Javatari had
SERVER_ADDRESS: "webmsx.azurewebsites.net/"for keep-alive + WebSocket). Set the address to""so any guardedaddress && …short-circuits and the wss:// URL becomes invalid. - CSS web-font
@importrules pointing atfonts.googleapis.comor similar — jsbeeb's Vite-built CSS had a Lato import. Strip the directive; fall through to the existing system-font fallback chain rather than bundle the font locally unless the visual difference matters.
- Inline analytics (
-
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. -
Write
play.htmlif the emulator needs a key-based launcher. Skip this if the emulator's own page already accepts URL params (BBC, Archimedes, WebMSX, JSVecX patterns). For wrappers:- reads
?game=<key>, - fetches
games.json, - looks up
{title, rom}, - boots the emulator with that ROM.
- reads
-
Bundle the ROMs under
emulators/<name>/<roms-dir>/. archive.org and most preservation sites don't set CORS for direct browser fetch, so wrappers can't pull at runtime — every byte must be on the same origin. -
Wire
fs.js: each game's.batgets alink:field pointing at the URL pattern above. Per-systemGAMESmenu follows the gold-standard 45-char box format (see Customising the Filesystem#menu-box-format) with 10 titles + a PROMPT/DESKTOP entry.
Some platforms have multiple machines under the same vendor — e.g. ATARI has 2600 / 7800 as siblings under CONSOLE and 400 / 800XL / ST as siblings under HOMECOMP, ACORN has BBC and ARCHIMDS, MSX has MSX1 and MSX2, SINCLAIR has SPECTRUM and ZX81. 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. Single-platform manufacturers (NES, GENESIS, INTV, CVISION, ODYSSEY2, VECTREX, COCO) are listed directly under their CONSOLE or HOMECOMP parent — no manufacturer middle level. Note that 400 + 800XL share one emulator build (emulators/atari800/) — the menus differ only in which ?machine= URL param their launchers pass.
One sub-system remains in the navigation without a local emulator: Magnavox Odyssey² (ODYSSEY2). Each has a navigation shell but its GAMES menu collapses to a single (No locally-hosted titles yet) line + Back option. Categories stay visible so future local-emulator additions can drop into existing slots without restructuring.
Same hierarchical idea applied to a single emulator: a single menu entry can promote into a submenu of related titles. DOS uses this for DUKE (Duke Nukem 1/2/3D) and KEEN (Commander Keen 1-6 + Keen Dreams) — each is a [GAMES] entry on the parent DOS menu that routes into its own subdir. The launcher recipe is unchanged from the flat case (each game still resolves to ?game=<key> in the same bundles/ directory); only the navigation layer adds a level.
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 and delete them.
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 off-domain.
EmulatorJS's EJS_pathtodata ../ prepend.
data/loader.js prepends "../" to any EJS_pathtodata that doesn't start with / and isn't an absolute URL, then dynamically imports src/*.js relative to that. From a page in a sub-directory, a value like "ejs/data/" ends up resolving one level above the current dir and the loader silently fails. Workaround: set EJS_pathtodata to an absolute path derived from the document URL, e.g. location.pathname.replace(/[^/]*$/, '') + 'ejs/data/'. (Was previously listed as a reason JSNES was preferred for NES; for C64 the fix is required and the integration works fine.)
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). Many items intermittently return tiny HTML 500 pages instead of the file — fall back to https://archive.org/cors/<item>/<path> for those. 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.
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.
JSVecX seamless.html only works inside an iframe.
The seamless launcher does load ROMs correctly but renders the canvas at zero size in a standalone tab — the sizer races with init and ends up shrinking the canvas to nothing. Always link index.html?rom=… (the bundle's full UI) instead.
JS7800's bupboop.wasm dependency is invisible from the script tags.
The upstream js/ directory ships bupboop.wasm (~590 KB YM-2151 sound chip emulator) referenced by bupboop.js but never listed in index.html's <script> tags. Without it: RuntimeError: abort(both async and sync fetching of the wasm failed) on every cart load. Probe the upstream js/ dir for sibling assets, not just the script tags. Also: roms/romlist-homebrew.json 404 is harmless — site.min.js looks for the upstream demo's homebrew cartlist; we don't ship one.
XRoar bundles the WASM but not the system ROMs.
Despite "free open-source replacement ROMs" claims, the WASM does NOT ship them — slots come up (unpopulated) CRC32 INVALID until the BASIC ROM files are mirrored alongside. Also requires a Module.locateFile shim to strip leading slashes (WASM requests system ROMs via absolute paths like /d64_1.rom). And: upstream defines 15 ui_* callback functions the WASM invokes for UI updates — without them, you get Uncaught ReferenceError: ui_menu_clear is not defined and the emulator silently halts. Stub all 15 as no-ops if you're not rendering the upstream UI panels.
JtyOne loads tapes from custom .hex format, not .p.
JtyOne's loader expects raw bytes encoded as a continuous ASCII hex string. Build with xxd -p file.p | tr -d '\n' > file.p.hex. The original .p files aren't fetched at runtime — only the .hex companions. Also: FileToLoad opens system-ROM URLs with a hardcoded leading slash ("/" + this.fileName); patch zx81_emu.js to drop the leading slash so paths resolve relative to the emulator dir. And: the error path references statusLabel which is undefined in upstream — masks all the actual loading errors until the absolute-path bug is fixed.
JSSpeccy 3 always shows a play-button overlay despite autoStart:true.
Web Audio can't start without a user gesture (browser autoplay policy). The "play" overlay is unavoidable across emulators that use audio; not a bug.
Microvision attempted and dropped. The bundled JS Microvision emulators all only emulate ONE of the two CPUs the system shipped with: Berrry-app does Intel 8021, Paul Robson's MVEM does TMS1100. Picking one strands the other half of the library. Until a JS emulator covers both CPUs, Microvision sits out. Vectrex sits directly under CONSOLE for now; if a Microvision emulator surfaces later, a Smith Engineering parent menu can be re-introduced.
atari800: no pre-built WASM port exists, so we built our own.
Three off-the-shelf options were ruled out before building from source: jsA8E (AnimaInCorpore/A8E, live at jsa8e.anides.de) ships no LICENSE file in its GitHub repo — all rights reserved by default, can't legally redistribute. EmulatorJS ships a5200 but no atari800 core. The official RetroArch emscripten nightly (buildbot.libretro.com/nightly/emscripten/RetroArch.7z, 760 MB) does NOT include atari800 in its ~90-core web build. So we build atari800/atari800 from source. Configure with the SDL2 software renderer (--without-opengl — emscripten's eglCreateContext fails and the canvas stays black), disable host-only features (--disable-netsio --disable-riodevice --disable-rserial --disable-rnetwork — they need clock_gettime / TTY_DEV_NAME), and bake in AltirraOS via --enable-altirra_bios (Avery Lee's freely-redistributable open-source OS, falls back automatically when no real ROM is found). Link with -sUSE_SDL=2 -sASYNCIFY -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=64MB — ASYNCIFY is mandatory because atari800's main loop in sdl/main.c is a for(;;) that would freeze the browser without it; ASYNCIFY suspends/resumes at SDL_Delay. Output: 1.7 MB WASM + 186 KB JS. Custom play.html wrapper preloads the ROM into the in-WASM virtual FS via Module.FS.createPreloadedFile and passes the path as atari800's first positional arg (autoboots disks) or -cart + -cart-type N (for raw .rom cart dumps).
Star Raiders is a cartridge masquerading as a disk on archive.org.
The a8b_Star_Raiders_1979_Atari_US_k_file item ships an 8.7 KB .atr that wraps the 8 KB cart binary in an empty-disk container — boots straight into a 6502 CIM at $A184. Use a8b_cart_Star_Raiders_1979_Atari_US/Star_Raiders_1979_Atari_US.rom (8 KB raw cart) with cartType:1 (Standard 8KB) in games.json so the wrapper passes -cart-type 1 -cart /game.rom. Pattern applies to any Atari 8-bit cart-only title — check the a8b_cart_* items first.
./serve.sh # or: python3 -m http.server 8765After editing fs.js or any emulator wrapper, hard-refresh the prompt page (Ctrl+Shift+R). Browsers aggressively cache fs.js between visits.