#!/usr/bin/env python3 """HTML setup routine for DOSBox/ScummVM games. Nothing about which games exist is hardcoded here: available games are discovered by listing *.json manifests on the SMB share, one per game, each describing how to install/uninstall/start/stop that game. Drop a new .json (and matching zip) on the share and it shows up here with no image changes needed. Manifest schema (all fields required): { "name": "stuntcar", # must match the manifest's own filename "title": "Stuntcar Racer", "zip_path": "Games/dosbox/stuntcar.zip", # path to the zip, relative to the share root - # games can live anywhere on the share, not just # next to their manifest "release_date": "1989", "publisher": "MicroStyle", "programs": [ {"id": "start", "label": "Start", "cmd": ["dosbox", "run.bat", "-conf", "dosbox-0.74-3.conf"]} ] } "programs" is a list, not a single command, because some games need more than one runnable binary - e.g. Duke Nukem 3D ships both the game itself and a separate SETUP.EXE for interactively picking DOS sound hardware, and the two aren't interchangeable: { "programs": [ {"id": "play", "label": "Play", "cmd": ["dosbox", "duke3d.exe"]}, {"id": "setup", "label": "Setup", "cmd": ["dosbox", "setup.exe"]} ] } The 5 ScummVM manifests use the same pattern for the SCUMM equivalent of a hardware-setup binary - ScummVM's own graphical Launcher (Game Options, Global Options, audio driver, MT-32 emulation, etc.), reached by a "manager" program instead of a "setup" one: { "programs": [ {"id": "start", "label": "Start", "cmd": ["/usr/games/scummvm", "-p", ".", "--auto-detect"]}, {"id": "manager", "label": "Manager", "cmd": ["bash", "-c", "/usr/games/scummvm -p . --add >/tmp/scummvm-add.log 2>&1; exec /usr/games/scummvm -p ."]} ] } Plain `scummvm -p .` with no game argument opens the Launcher, but its game list is empty until the game has been registered with `--add` (a separate one-shot command, confirmed idempotent - safe to run before every Manager launch); the `exec` at the end keeps the tracked process the real interactive `scummvm` instance rather than the wrapper shell, so stop/teardown targets the right PID. Each entry gets its own button in the "Installed" row; whichever one is clicked becomes the process tracked for that game's single slot (only one program per game can run at a time - starting a second one while the first is still running is refused the same way starting an already-running game is). "id" is what's sent back over the wire (POST start, name=&program=); "label" is only for display. Uninstall is implicit (remove GAMES_HOME/); stop is implicit (terminate whichever program's process is currently tracked). Manifests themselves always live in SMB_DIR (the catalog directory), even though the zip_path they point at may not. Install size isn't a manifest field - it's looked up live from the share (via `smbclient ls`) so it can't go stale. Each running game gets its own ephemeral X session (Xvnc + fluxbox + websockify on their own display/ports) instead of sharing one screen, so up to MAX_CONCURRENT_GAMES can run side by side without fighting over focus or being invisible to each other. Starting past that cap is refused. Audio is deliberately *not* per-game: server.sh starts one PulseAudio daemon and one pcm_ws_bridge.py (from docker-common) for the whole container's lifetime, capturing PulseAudio's single default sink. Every running game's audio mixes together there (normal PulseAudio behavior, no per-game routing needed) - simpler than per-slot audio isolation, at the cost of not being able to tell games' audio apart when more than one is running. Video and audio are deliberately on separate pages/tabs, not one wrapper page with an