Add ScummVM support and generalize manifest zip lookup
Add scummvm to the Dockerfile. Its binary lives at /usr/games/scummvm (Debian's convention for game packages), not reliably on $PATH under a non-login shell, so manifests reference it by absolute path. Generalize the manifest schema from a bare "zip" filename (implicitly under Games\dosbox\ on the SMB share) to "zip_path" (a full path relative to the share root), since the SCUMM games' zips live in their own folders (Games\Monkey Island\, Games\Indiana Jones\, etc.) rather than being colocated with their manifest like stuntcar/t7g were. Manifests themselves still all live in the Games\dosbox\ catalog directory regardless of where the actual zip sits. Added and verified (install/start/stop/uninstall via monkey2) manifests for: monkey, monkey2, atlantis, indy3, tentacle. Skipped the German CD release of Day of the Tentacle (loose files at the zip root, no single top-level folder - incompatible with the current extraction convention) and Curse of Monkey Island (untested, much larger). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
This commit is contained in:
@@ -4,9 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## What this is
|
||||
|
||||
A Docker image definition for headless DOSBox games exposed over noVNC (VNC-over-websockets in the browser). It builds on the public base image `ich777/novnc-baseimage`, which already provides TurboVNC, websockify, fluxbox, and noVNC — this repo adds `dosbox`/`alsa-utils` plus `unzip`/`smbclient`/`python3` for game management, a TLS cert for noVNC, and the entrypoint/lifecycle scripts. Published as `jayfield/dosbox-novnc` on Docker Hub.
|
||||
A Docker image definition for headless DOS/SCUMM games exposed over noVNC (VNC-over-websockets in the browser). It builds on the public base image `ich777/novnc-baseimage`, which already provides TurboVNC, websockify, fluxbox, and noVNC — this repo adds `dosbox`/`scummvm`/`alsa-utils` (game runtimes) plus `unzip`/`smbclient`/`python3` for game management, a TLS cert for noVNC, and the entrypoint/lifecycle scripts. Published as `jayfield/dosbox-novnc` on Docker Hub. `scummvm`'s binary is at `/usr/games/scummvm`, not `/usr/bin` (Debian's convention for game packages) and not reliably on `$PATH` under a non-login shell — manifests reference it by absolute path.
|
||||
|
||||
Game data (DOS game installs, sourced as zips from an internal SMB share `//vlda-01/software/Games/dosbox/`) is deliberately **not** baked into the image — one of the two games (`t7g`, "The 7th Guest") is ~930MB of CD-ROM ISOs on its own, more than the rest of the image combined. Instead the image ships empty and games are installed at runtime into a volume, via an HTML setup page (see below).
|
||||
Game data (game installs, sourced as zips from an internal SMB share `//vlda-01/software/`) is deliberately **not** baked into the image — one of the games (`t7g`, "The 7th Guest") is ~930MB of CD-ROM ISOs on its own, more than the rest of the image combined. Instead the image ships empty and games are installed at runtime into a volume, via an HTML setup page (see below).
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -47,14 +47,14 @@ There's no automated way to exercise `scripts/server.sh`, `scripts/setup_server.
|
||||
|
||||
**Game installation & lifecycle** (`scripts/setup_server.py`), the key thing that differs from the other noVNC-family projects:
|
||||
- A stdlib-only Python `ThreadingHTTPServer` (no third-party deps, no pip installs) started in the background by `server.sh` alongside Xvnc/fluxbox/websockify.
|
||||
- Games are **not** hardcoded anywhere in the image or discovered from raw zip files. Each game is described by its own `<name>.json` manifest living next to its zip on the SMB share (e.g. `Games\dosbox\stuntcar.json`), with `name`/`title`/`zip`/`start_cmd` fields — see the module docstring in `setup_server.py` for the exact schema. `discover_games()` lists `*.json` on the share (`smbclient -N ... -c 'ls Games\dosbox\*.json'`), `smbclient get`s each one into `/tmp/manifests/`, and parses it — so dropping a new `<name>.json` + `<name>.zip` pair on the share is enough to make a game appear, no image rebuild needed.
|
||||
- Games are **not** hardcoded anywhere in the image or discovered from raw zip files. Each game is described by its own `<name>.json` manifest living in the `Games\dosbox\` catalog directory on the SMB share, with `name`/`title`/`zip_path`/`start_cmd` fields — see the module docstring in `setup_server.py` for the exact schema. `discover_games()` lists `*.json` under `Games\dosbox\` (`smbclient -N ... -c 'ls Games\dosbox\*.json'`), `smbclient get`s each one into `/tmp/manifests/`, and parses it — so dropping a new `<name>.json` on the share is enough to make a game appear, no image rebuild needed. `zip_path` is a full path relative to the share root (e.g. `Games/Monkey Island/The Secret of Monkey Island.zip`), **not** assumed to live next to its manifest — manifests always live in the `Games\dosbox\` catalog regardless of where the actual game data sits on the share (the SCUMM games' zips live in their own folders, e.g. `Games\Monkey Island\`, `Games\Indiana Jones\`).
|
||||
- `GET /` renders one row per discovered manifest with its live status (Not installed / downloading / extracting / Installed / Running / error) and the matching action buttons.
|
||||
- `POST /install`: `smbget -au` (guest auth) the manifest's `zip` into a `/tmp/setup-<name>` scratch dir, then `unzip -o -d ${GAMES_HOME}`. `smbget -a` (guest) cannot be combined with `-o` (output-file) — they conflict on the underlying `-U` option — so the download `cd`s into the scratch dir and lets `smbget` save under its default filename instead. Installed-ness is just "non-empty `${GAMES_HOME}/<name>` directory exists"; a zip is expected to contain one top-level folder matching its own basename.
|
||||
- `POST /install`: `smbget -au` (guest auth) the manifest's `zip_path` into a `/tmp/setup-<name>` scratch dir, then `unzip -o -d ${GAMES_HOME}`. `smbget -a` (guest) cannot be combined with `-o` (output-file) — they conflict on the underlying `-U` option — so the download `cd`s into the scratch dir and lets `smbget` save under its default filename instead. Installed-ness is just "non-empty `${GAMES_HOME}/<name>` directory exists"; a zip is expected to contain one top-level folder matching its own basename (this happens to already match ScummVM's own game-target IDs for the SCUMM titles, e.g. `monkey2.zip` → `monkey2/` — convenient, not enforced by the code).
|
||||
- `POST /uninstall`: `rm -rf ${GAMES_HOME}/<name>`. Refused while the game is running (stop it first) — there's no auto-stop-then-delete.
|
||||
- `POST /start`: `subprocess.Popen(manifest["start_cmd"], cwd=GAMES_HOME/<name>)`, tracked in an in-process dict keyed by game name (`running_procs`). This inherits `setup_server.py`'s own environment, including `$DISPLAY`, so the launched process renders onto the same Xvnc display fluxbox/noVNC are already serving — no separate X setup needed.
|
||||
- `POST /stop`: `terminate()`s the tracked process, escalating to `kill()` after a 5s grace period.
|
||||
- Per-game install progress (`install_state`) and the running-process table (`running_procs`) are both guarded by one `threading.RLock` (reentrant — several code paths call `is_running()` from inside a block that already holds the lock). The page does a `<meta http-equiv="refresh">` every 3s while any install is in flight; no client-side JS anywhere.
|
||||
- **Only `stuntcar.json` exists on the share right now** — this whole manifest/start/stop/uninstall mechanism was rolled out as a pilot on that one (small, fast-to-test) game before being extended to `t7g`. Until `t7g.json` is added, `t7g` simply won't appear on the setup page even though its zip is still there.
|
||||
- **Manifests on the share today**: `stuntcar` (DOSBox), `monkey`/`monkey2`/`atlantis`/`indy3`/`tentacle` (ScummVM). `t7g` still has no manifest and won't appear on the setup page until one's added.
|
||||
|
||||
**Playing a game — two overlapping paths right now**:
|
||||
1. The setup page's Start/Stop buttons (above) — the current way, works for any manifest-driven game.
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ ENV PATH=${PATH}:${SCRIPTS_HOME}
|
||||
# image, setup_server.py serves an HTML install UI (backed by smbget+unzip) that the user
|
||||
# drives on ${SETUP_PORT} instead of anything being auto-downloaded at container start.
|
||||
RUN apt-get update && \
|
||||
apt-get -y install --no-install-recommends dosbox alsa-utils unzip smbclient python3 && \
|
||||
apt-get -y install --no-install-recommends dosbox scummvm alsa-utils unzip smbclient python3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN openssl req -x509 -nodes -newkey rsa:2048 -keyout /tmp/novnc.key -out /tmp/novnc.pem -days 3650 -subj "/C=US/ST=NY/L=NY/O=NY/OU=NY/CN=NY emailAddress=email@example.com"
|
||||
|
||||
@@ -14,24 +14,44 @@
|
||||
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 (test balloon, `stuntcar` only)**: games are
|
||||
no longer discovered from raw `*.zip` files on the share. `setup_server.py` now lists
|
||||
`*.json` manifests instead (one per game, e.g. `Games\dosbox\stuntcar.json`), each
|
||||
declaring `name`/`title`/`zip`/`start_cmd`. The web page grew Start/Stop/Uninstall buttons
|
||||
- **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. Only `stuntcar.json` exists on the share
|
||||
so far (as a deliberate pilot); `t7g` has no manifest yet and will not 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 for this
|
||||
pilot and now overlap with the new Start button — worth reconciling (or removing) once the
|
||||
manifest approach is rolled out to all games.
|
||||
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 (Monkey Island, Day of the Tentacle, Indiana Jones,
|
||||
etc.). Note these are usually best run via ScummVM rather than raw DOSBox — `scummvm` isn't
|
||||
installed in the image yet, so this may need a Dockerfile change (add the `scummvm`
|
||||
package) alongside the manifests themselves, and each manifest's `start_cmd` would invoke
|
||||
`scummvm` with the right game-id instead of `dosbox run.bat -conf ...`.
|
||||
- ~~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
|
||||
|
||||
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.
|
||||
|
||||
- Get game sound actually audible during play. `--device /dev/snd` is passed through and
|
||||
`alsa-utils` is installed, so DOSBox can write to an ALSA device inside the container, but
|
||||
|
||||
@@ -11,11 +11,15 @@ Manifest schema (all fields required):
|
||||
{
|
||||
"name": "stuntcar", # must match the manifest's own filename
|
||||
"title": "Stuntcar Racer",
|
||||
"zip": "stuntcar.zip", # filename under SMB_DIR to fetch on install
|
||||
"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
|
||||
"start_cmd": ["dosbox", "run.bat", "-conf", "dosbox-0.74-3.conf"]
|
||||
}
|
||||
Uninstall is implicit (remove GAMES_HOME/<name>); stop is implicit (terminate
|
||||
the process tracked from the last start_cmd).
|
||||
the process tracked from the last start_cmd). Manifests themselves always live
|
||||
in SMB_DIR (the catalog directory), even though the zip_path they point at may
|
||||
not.
|
||||
"""
|
||||
|
||||
import html
|
||||
@@ -95,12 +99,12 @@ def install_game(name, manifest):
|
||||
tmpdir = f"/tmp/setup-{name}"
|
||||
try:
|
||||
os.makedirs(tmpdir, exist_ok=True)
|
||||
zip_name = manifest["zip"]
|
||||
zip_name = manifest["zip_path"].rsplit("/", 1)[-1]
|
||||
zip_path = os.path.join(tmpdir, zip_name)
|
||||
# smbget's -a (guest) can't be combined with -o (output file), so let it save
|
||||
# under its default name in tmpdir instead.
|
||||
subprocess.run(
|
||||
["smbget", "-au", f"smb://{SMB_SERVER}/{SMB_SHARE}/Games/dosbox/{zip_name}"],
|
||||
["smbget", "-au", f"smb://{SMB_SERVER}/{SMB_SHARE}/{manifest['zip_path']}"],
|
||||
check=True, timeout=1800, cwd=tmpdir,
|
||||
)
|
||||
with state_lock:
|
||||
|
||||
Reference in New Issue
Block a user