Put the AdminBridge GameScript under version control
Continuous Integration / lint-and-security (pull_request) Failing after 19s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s

The last commit noted in passing that the server-side half of the admin
GameScript channel "is not in this repo -- docker/config is gitignored --
so it has to be updated separately for any of this to work." That was true
of all nine features the README documents: list_vehicles, list_stations,
list_cargo, get_timetable, get_station, get_station_cargo, get_dispatch and
the event stream all answer from 705 lines of Squirrel that no clone could
reproduce, no reviewer could see, and CI never touched.

The bridge now lives in gamescript/AdminBridge/ with its own README, the
same arrangement docker/patches/ uses for the local JGRPP patches, and
docker-compose.yml bind-mounts it read-only over the container's
game/AdminBridge. docker/config stays ignored -- it also holds savegames,
downloaded content and generated config -- so the copy under it is now
shadowed and can be deleted. main.nut is byte-identical to what was running,
apart from the version work below.

Adds a version handshake, because the channel gives no way to tell a stale
bridge from a hung one: a bridge that does not recognise a command drops it
silently, so a client ahead of the server sees nothing but timeouts. The
bridge now answers get_version with its protocol version plus its command
and event catalogues, and get_bridge_version() raises when that is below
GS_BRIDGE_VERSION. It is opt-in rather than checked on connect: GameScripts
do not tick while the game is paused, so an automatic check would refuse to
connect to a paused server. A bridge older than 4 predates get_version
itself and can only fail by timing out, so the E2E test catches that and
reports it by name instead.

tests/test_gamescript.py gives CI a foothold on the GameScript without a
Squirrel toolchain: it parses the .nut files and pins the protocol version
across info.nut, main.nut and protocol.py, the event catalogue against
GameEventType, and the command table against the commands client.py sends.
The version has to be declared three times because a GameScript cannot read
its own info.nut at runtime -- GSController.GetVersion() returns the OpenTTD
version, not the script's.

info.nut also gains MinVersionToLoad() { return 1; }. The engine defaults it
to GetVersion(), so without it this bump would orphan every savegame pinned
to version 3: the scanner finds no compatible script and falls back with a
warning. The bridge keeps no savegame state, so any version can take over.

HandleCommand now dispatches through the same table get_version reports,
rather than an if/else chain, so the catalogue a client feature-detects
against cannot drift from what is implemented.

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
2026-08-31 19:06:33 +02:00
co-authored by Claude
parent 90a07392cf
commit f7ca395a4f
17 changed files with 1110 additions and 7 deletions
+30
View File
@@ -26,6 +26,36 @@ Similar to the Game Port, the Admin Network uses X25519 PAKE for secure authenti
### Update Frequencies
Admins can subscribe to various updates (Date, Client Info, Company Info, etc.) at different frequencies (Poll, Daily, Weekly, Monthly, Quarterly, Annually, Automatic).
### The AdminBridge GameScript
Everything in this section rides on a companion GameScript running on the server — the other half
of the protocol, kept in [`gamescript/AdminBridge/`](../gamescript/AdminBridge/README.md). The
Admin Network itself offers no way to ask the game about individual vehicles, stations or orders;
what it does offer is an opaque JSON channel to whatever GameScript is loaded (`AdminGamescript`
out, `ServerGamescript` back), and this bridge is what gives that channel meaning.
Note what the channel does **not** give you: a bridge that does not recognise a command drops it
silently. There is no "unknown command" reply, so a client talking to a bridge older than itself
sees nothing but timeouts.
### Bridge Version
`get_bridge_version()` exists to turn that silence into an error. The bridge reports the version
of the JSON protocol it implements, and the client compares it against `GS_BRIDGE_VERSION` (the
version it was written against, in `protocol.py`).
- **Request:** `{"command": "get_version", "request_id": X}`.
- **Reply:** `{"command": "get_version", "request_id": X, "version": N, "commands": [...], "events": [...]}`
`commands` is every command name the bridge answers (sorted) and `events` every event kind it
can push, so a client can feature-detect a single command instead of comparing version numbers.
- **No reply at all** is the answer from a bridge older than version 4, which is when
`get_version` was added; `get_bridge_version()` surfaces that as `asyncio.TimeoutError`, the
same as a paused game or a server with no bridge loaded.
The version covers the shape of the JSON protocol, not the implementation, and is declared in
three places that [`tests/test_gamescript.py`](../tests/test_gamescript.py) keeps in step:
`info.nut`'s `GetVersion()`, `main.nut`'s `BRIDGE_VERSION` and `protocol.py`'s
`GS_BRIDGE_VERSION`. The same test checks the bridge's event catalogue against `GameEventType`
and its command table against the commands the client actually sends.
### Vehicle Listing
The Admin Network has no native packet or `AdminUpdateType` for listing individual vehicles — `ServerCompanyStats` only reports aggregate per-company vehicle counts (trains/lorries/buses/planes/ships). To retrieve an actual vehicle list, this client sends a `list_vehicles` command over the GameScript JSON channel (`AdminGamescript`/`ServerGamescript`) via `list_vehicles()`. This requires a companion GameScript running server-side that understands the `list_vehicles` command and replies with vehicle data through `ServerGamescript`.