# AdminBridge GameScript The server-side half of this project. Every `OpenTTDAdminClient` feature that goes over the GameScript JSON channel — `list_vehicles`, `list_stations`, `list_cargo`, `get_timetable`, `get_station`, `get_station_cargo`, `get_dispatch` and the `subscribe_events` push stream — is a command this script answers. The Python client is only half the protocol; this is the other half, and the two are documented together in [docs/PROTOCOL.md](../../docs/PROTOCOL.md). It is a **deity** GameScript: it joins no company, builds nothing, and only reads game state and replies on the admin port via `GSAdmin.Send()`. - `info.nut` — the manifest OpenTTD's script scanner reads (name, version, API version). - `main.nut` — the bridge itself: a command dispatch table, one handler per command, and the event poller. ## Why it lives here `docker/config/` is gitignored — it also holds savegames, downloaded content and generated config — so a GameScript kept there is invisible to review and to CI, and a fresh clone cannot reproduce the server side at all. This directory is the source of truth, the same arrangement [docker/patches/](../../docker/patches/README.md) uses for the local JGRPP patches. `docker-compose.yml` bind-mounts this directory read-only over the container's `game/AdminBridge`, so the server runs the tracked copy and nothing else: ```yaml - ../gamescript/AdminBridge:/home/openttd/.local/share/openttd/game/AdminBridge:ro ``` Editing the copy under `docker/config/game/AdminBridge/` therefore has no effect; that path is shadowed by the mount. On a server not started through this compose file, copy the directory into the OpenTTD data dir instead: ```bash cp -r gamescript/AdminBridge ~/.local/share/openttd/game/ ``` A GameScript is loaded when the game starts, so a change needs the server restarted (or the game reloaded) before it takes effect — the running instance keeps the old code. ## Protocol version `info.nut`'s `GetVersion()` is the version of the **JSON protocol**, not of the implementation: bump it when a command, field or event kind changes shape, not for a refactor. Three places carry it and all three must move together: | Where | What | | :--- | :--- | | `info.nut` → `GetVersion()` | what OpenTTD records in the savegame | | `main.nut` → `BRIDGE_VERSION` | what the `get_version` command reports | | `lib/openttd/protocol.py` → `GS_BRIDGE_VERSION` | what the client requires | A GameScript cannot read its own `info.nut` at runtime — `GSController.GetVersion()` returns the *OpenTTD* version — hence the duplication. [`tests/test_gamescript.py`](../../tests/test_gamescript.py) pins the three together, along with the event catalogue and the command list, so a half-finished bump fails in CI rather than against a live server. `OpenTTDAdminClient.get_bridge_version()` is the client end of this: it asks the running bridge and raises if it is too old, which is worth doing once at startup. Without it a stale bridge gives no error at all — it does not recognise the command, so the client just waits out its timeout. A bridge older than version 4 predates `get_version` itself and can only fail that way. ## Requirements - **JGRPP**, patched with `docker/patches/` — `get_timetable` and `get_dispatch` call `GSOrder` getters those patches add. Every other command uses stock GameScript API, so an unpatched server still answers them. - The admin client must subscribe with `update_frequency(Gamescript, Automatic)` or the server drops every reply. The client does this automatically on first use. - GameScripts do not tick while the game is **paused**, so a paused server answers nothing and every query times out. ## Which GameScript actually runs OpenTTD runs exactly **one** GameScript, and a savegame remembers the one it was played with by name: loading it overrides `[game_scripts]` in `openttd.cfg`. So a savegame that pins some other script leaves this bridge unloaded, and every command times out no matter what is mounted where. The symptom to recognise is commands timing out *uniformly* — as opposed to a stale bridge, where the older commands still answer and only newer ones hang. ## Verifying a change `docker/config` being untracked used to mean this file could only be tested against a running server. It still needs one for the real check — `pytest -m e2e` exercises every command end to end, and [docs/TESTING.md](../../docs/TESTING.md) covers starting the server. Before that, `pytest -m "not e2e" tests/test_gamescript.py` catches the drift a server would only reveal as a timeout. Note that a syntax error anywhere in `main.nut` stops the whole bridge from loading, so every command fails at once — check the server log (`docker compose logs`) for the compile error.