# TODO - ~~Reduce image size of `jayfield/dosbox-novnc` (currently ~2.3GB).~~ Done in two steps: 1. Merged the game download/extract steps and the unzip/smbclient install into a single layer (so the downloaded zips and the packages only needed to extract them no longer persist in the final image). 2.31GB -> 1.6GB. 2. Stopped baking the games into the image at all (927MB of that was just t7g's two CD ISOs). `run.sh` mounts a named volume (`dosbox-games`) so installed games persist across `--rm` restarts. 1.6GB -> ~690MB. Container now needs network access to `vlda-01` at runtime (not just build time) to install games. 3. Dropped the auto-download at container startup entirely. `scripts/setup_server.py` is a small stdlib-only HTTP server that serves an HTML page on `${SETUP_PORT}` (`70${DISPLAY_NUM}`, published as `7099` by `run.sh`) listing every `*.zip` on the SMB share with an Installed/Install status per game; the user clicks "Install" to fetch+extract a specific game into `${GAMES_HOME}` on demand. - **Manifest-driven games + Start/Stop/Uninstall**: games are no longer discovered from raw `*.zip` files on the share. `setup_server.py` lists `*.json` manifests instead (one per game, in the `Games\dosbox\` catalog directory), each declaring `name`/`title`/`zip_path`/`start_cmd`. The web page grew Start/Stop/Uninstall buttons alongside Install, driven entirely by that manifest — nothing about a game's identity or how to run it is hardcoded in the image anymore. Started as a `stuntcar`-only pilot, now extended to 6 games (see the SCUMM item below). `t7g` still has no manifest and won't show up in the setup page until one is added (`t7g.json` with a `start_cmd` for its `dosbox-0.74-3.conf`). `game.sh`/`scripts/start_game.sh` (the old `docker exec`-based launch path, with its hardcoded `DOSBOX_VERSION`/`run.bat` convention) were intentionally left untouched during the pilot and still overlap with the new Start button — worth reconciling (or removing) once every remaining game has a manifest. - ~~Create manifests for SCUMM engine games~~ Done: `scummvm` added to the Dockerfile (`/usr/games/scummvm`, not on `$PATH` by default under a non-login shell — manifests use the absolute path). Manifests live in the usual `Games\dosbox\*.json` catalog directory on the share, but their `zip_path` now points at wherever the game's zip actually already lived (`Games\Monkey Island\...`, `Games\Indiana Jones\...`, etc.) — this required generalizing the manifest schema from a bare `zip` filename (implicitly under `Games\dosbox\`) to a full `zip_path` relative to the share root, since these games weren't colocated with dosbox's. Added and verified end-to-end (install/start/stop/uninstall) via `monkey2`: - `monkey` — The Secret of Monkey Island - `monkey2` — Monkey Island 2: LeChuck's Revenge - `atlantis` — Indiana Jones and the Fate of Atlantis - `indy3` — Indiana Jones and the Last Crusade - `tentacle` — Day of the Tentacle **Real bug found later (all 5 of the above), reported as "Indiana Jones and the Fate of Atlantis doesn't start properly"**: every one of these manifests' command was `["/usr/games/scummvm", "-p", ".", "-f", "atlantis"]`-shaped, i.e. `-f `. `-f` is ScummVM's `--fullscreen` flag, not "select this game" — there's no flag that takes a bare game-id positionally either; passing one is rejected outright ("Stray argument 'atlantis'"), and ScummVM exits near-instantly with a nonzero status. Because the exit happens faster than the setup page's ~3s poll interval, `is_running()`'s lazy cleanup silently reaped it before anyone saw a "Running" status or an error message — clicking Start just looked like nothing happened, no error surfaced anywhere. (This also means the "verified end-to-end" claim above, from when these were first added, was wrong — Stop/Uninstall working afterward isn't evidence the game itself ran; it only proves `is_running()` correctly saw it wasn't.) Root-caused via `docker exec` running the exact command by hand and reading ScummVM's own stderr, rather than through the setup page. Fixed by switching every one of these 5 manifests' command to `["/usr/games/scummvm", "-p", ".", "--auto-detect"]` — ScummVM's own flag for "scan the given path and start whatever single game it finds there", rather than fighting its target/config-file model, which fits this container's one-game-per-directory layout exactly. Verified for real this time: manually launched under a live Xvnc, screenshotted, and confirmed the actual Indiana Jones and the Fate of Atlantis title screen renders (not just "process didn't crash") - then re-verified through the real setup page end-to-end (`POST /start` -> "Running (Start)" -> `POST /stop`). Skipped the German CD release of Day of the Tentacle on the share (`Day Of The Tentacle (CD DOS, German).zip`) — unlike every other zip here, it doesn't wrap its contents in a single top-level folder (loose files at the zip root plus a stray `MANIAC/` dir for the embedded Maniac Mansion easter egg), so it doesn't fit the current "zip's top-level folder == install name" extraction convention. Also skipped Curse of Monkey Island (much larger, untested) — could be added the same way if wanted. ScummVM's own game-target IDs (`monkey`, `monkey2`, `atlantis`, `indy3`, `tentacle`) happened to exactly match each zip's existing top-level folder name, so no change was needed to the install/extraction logic itself, only to where zips are looked up from. - ~~Handle multiple games running at once~~ Done: each running game now gets its own ephemeral X session (`Xvnc`+`fluxbox`+`websockify`, its own display `:90`-`:99` and noVNC port `8090`-`8099`) spun up by `start_game()` and torn down by `stop_game()` (or lazily on the next page load, if the game exited/crashed on its own) — instead of every game sharing one screen and fighting over focus/audio. Capped at `MAX_CONCURRENT_GAMES = 10`; starting an 11th game while all 10 slots are in use is refused with an error shown on its row rather than silently doing nothing. There is no more a single shared "default" desktop or global noVNC link — `server.sh` no longer starts Xvnc/fluxbox/websockify at container startup at all, only `setup_server.py` itself; each game's own "Open Screen" link appears on its row only while it's running, built with a little client-side JS (the noVNC port differs from the setup port and can't be known server-side without knowing which hostname the browser used to reach the container). `run.sh` publishes the whole `8090-8099` range up front, since Docker can't add port mappings to an already-running container. Verified end-to-end with two different games running concurrently (separate processes, separate displays, separate noVNC ports, independent stop/teardown) and the 10-slot cap logic. - ~~Get game sound actually audible during play~~ Done: PulseAudio + a hand-rolled stdlib WebSocket bridge (`pcm_ws_bridge.py`, in `docker-common` — reusable, not dosbox-specific, per the earlier plan for this item) carries raw PCM to the browser, played back via a Web Audio `AudioWorkletNode` (`pcm-worklet.js`, also in `docker-common`). Verified real end-to-end audio: DOSBox → ALSA (default device, routed through Pulse via `/etc/asound.conf`) → PulseAudio's one default sink → `parec` → the bridge → a raw WebSocket client, measured RMS ≈9292 while `stuntcar` was actually playing (vs. silence before advancing past its title screen) — not just a plumbing check, actual game audio. **Audio is a single shared mix, not per-game** — deliberately simplified from an earlier per-slot-isolated design (considered and dropped as unnecessary complexity): every running game's audio mixes into Pulse's one default sink, and every `/screen/` page connects to the same single `AUDIO_PORT` (`71${DISPLAY_NUM}`, `7199` by default). Trade-off: with more than one game running, you can't tell their audio apart. **Real bug found and fixed along the way**: `parec --device=@DEFAULT_SINK@.monitor` (the seemingly-obvious macro syntax) fails with "Stream error: Invalid argument" — PulseAudio has a *separate* single-token macro, `@DEFAULT_MONITOR@`, for exactly this; concatenating `@DEFAULT_SINK@` with a literal `.monitor` suffix isn't valid. `server.sh` now starts `pulseaudio --start --exit-idle-time=-1` and the bridge (`pcm_ws_bridge.py 7199 parec --device=@DEFAULT_MONITOR@ ...`) once, before `exec`-ing `setup_server.py` — both live for the container's whole lifetime, independent of any game's start/stop. **No iframe, no per-game screen page** — tried an `