37 lines
5.2 KiB
Org Mode
37 lines
5.2 KiB
Org Mode
Part of [[id:c87d8901-bbee-4397-bff1-af8432b4f66e][homey]]. EmulatorJS self-hosted web game emulator at =games.zakobar.com= (port 8085), Authelia one_factor. A Python stdlib sidecar (=modules/services/emulatorjs-server.py=) serves the launcher, ROMs, per-user saves, and a PWA; static assets/cores come from GitHub via =fetchFromGitHub= (=modules/services/emulatorjs.nix=, currently v4.2.3). ROMs/saves/thumbnails live on the external HD under =/mnt/data/emulatorjs/=, outside Nix.
|
|
|
|
** Architecture — offline PWA (added 2026-07-31)
|
|
|
|
Installable PWA with offline play, deliberately *network-first*: the Service Worker only reads cache when the network is unreachable, so offline support can never alter or break online play. (An earlier cache-first version rebuilt/buffered responses, broke EmulatorJS game loading, and was ripped out — the working tree that "fixed" it was simply the SW removed. Rebuilding responses via =arrayBuffer()=+=new Response()= also stripped range-request support and OOM'd large loads on iOS.)
|
|
|
|
Server serves =/sw.js= (Service Worker, =Service-Worker-Allowed: /=), =/manifest.json=, and a stdlib-generated =/icon-256.png= (PNG encoded by hand with =zlib=/=struct=, built once at startup — no image deps). Both HTML pages carry a shared PWA head fragment.
|
|
|
|
Two SW caches:
|
|
- =emujs-shell-<BUILD_ID>= — app shell + =/static/= cores, cache-first. =BUILD_ID= = short sha256 of the server .py file + =STATIC_DIR=, so a redeploy auto-invalidates stale assets (no manual cache-version bump, unlike the old =CACHE_NAME='v4'=). NOTE: BUILD_ID changes on *any* edit to the .py file, so every deploy drops the shell cache and it must be repopulated by one online play.
|
|
- =emujs-roms= — persistent across builds. ROMs are NOT auto-cached; download is opt-in per game via the launcher's ⬇ button (also caches the thumbnail + the =/play/<name>= document). Non-downloaded games dim out when offline.
|
|
|
|
Core caching (the non-obvious bit — fixed 2026-07-31): the emulator core (e.g. =mgba-legacy-wasm.data=, shared by GB/GBC/GBA) is NOT reliably cached by the Service Worker, because on the first visit the SW is not yet controlling the page, so the core fetched during that first online play is never intercepted. Instead the *player page* caches it directly: after game start, =cacheLoadedStatic()= reads =performance.getEntriesByType('resource')=, filters to same-origin =/static/= URLs (the real files the browser loaded), and writes them into the shell cache from the page — bypassing SW-interception timing entirely and version-proof. Runs on game start (+5s delayed pass) and on the =online= event.
|
|
|
|
Full design rationale is in the module docstring at the top of =emulatorjs-server.py=.
|
|
|
|
** Conventions
|
|
|
|
*Adding/removing GB/GBC/GBA ROMs*: just drop/delete files in =/mnt/data/emulatorjs/roms/=. The =/roms/= listing does a live =os.listdir= on every request, so a page refresh picks up changes — no restart, no =nixos-rebuild=. Filename (minus extension) is the display title. Optional cover art: =<title>.png= in =/mnt/data/emulatorjs/thumbnails/= (else 🎮 placeholder). Files must be readable by the =emulatorjs= service user.
|
|
|
|
*Upgrading EmulatorJS assets*: bump =rev= in =emulatorjs.nix=, re-prefetch (=nix-prefetch-url --unpack=), convert to SRI, update =hash=. Instructions are in a comment in the file.
|
|
|
|
*To make a game fully offline-ready*: (1) tap ⬇ to download the ROM, and (2) play it once while online so the core caches. After a redeploy, step 2 must be repeated (shell cache is build-scoped).
|
|
|
|
** Gotchas
|
|
|
|
*Save sync*: saves are independent of ROM download state. Two-tier: online → =PUT /saves/<game>= (keyed by =Remote-User=); offline/failed → queued in IndexedDB (=pending-saves= store). Queue flushes via =syncPendingSaves()= on the =online= event, on =EJS_onGameStart=, and on launcher load. NO Background Sync API — sync only runs while a page is open / on next app open. Last-write-wins, no timestamp merge (both deemed acceptable by owner 2026-07-31).
|
|
|
|
*Core not cached → "Error downloading core (…-wasm.data)" offline*: symptom of the SW-timing gap above. Fixed by page-side =cacheLoadedStatic()=; requires one successful online play (after each deploy) to populate.
|
|
|
|
*Filename charset*: a ROM loads only if its name matches =SAFE_RE= (=[A-Za-z0-9 _\-\.\(\)\[\]',!?:&+#]=). A name with other characters (accents, ~, @, %, ;) still appears in the listing but 404s/400s on open. Keep names in that set.
|
|
|
|
*Offline library staleness*: the =/roms/= listing is network-first cached, so an offline device shows its last-cached list until it's online once.
|
|
|
|
** Future — hosting disc-based systems (e.g. PS1)
|
|
|
|
Not done (owner declined 2026-07-31), but scoped. Would require: (1) map disc extensions to the =psx= core in =ROM_SYSTEMS= + zip detection (prefer single-file =.chd=; =.bin=/=.iso= are ambiguous across systems); (2) a user-supplied BIOS (copyrighted) served via a new =/bios/= route + =EJS_biosUrl=; (3) **stream ROM serving** — =send_body= currently does =f.read()=, loading the whole file into RAM, so a 600 MB disc image spikes 600 MB per request on the Pi. Fine for tiny cartridge ROMs; must switch to chunked/Range serving for large images. Offline download also impractical for 100s-of-MB ROMs (iOS Cache API quota). |