Conflicted only on the README feature list, where main's list_cargo()
bullet and this branch's bridge bullet were added at the same spot. Kept
both: list_cargo() with the other cargo queries, and the bridge one last,
since it is about what all of them run against.
list_cargo() landing also makes tests/test_gamescript.py's command check
meaningful in the other direction -- the bridge has answered list_cargo
all along with nothing sending it, which is exactly the asymmetry that
check tolerates on purpose.
Co-Authored-By: Claude <[email protected]>
ruff's isort rules want the openttd import grouped with the other
third-party imports, as the rest of the test suite has it.
Co-Authored-By: Claude <[email protected]>
The station queries and the cargo events name a cargo only by a bare
numeric id -- get_station()'s and get_station_cargo()'s cargo_id, the
cargo_waiting events, the per-cargo load on vehicle events. Those ids
index the cargo table the loaded NewGRFs build for the running game, so
the same id is coal in one save and grain in another and callers had no
way to resolve them. The AdminBridge GameScript has answered a list_cargo
command all along; no method on OpenTTDAdminClient sent it. This adds the
missing half, so no GameScript change is needed for it to work.
It goes through _gs_query() like get_station(), inheriting the request_id
correlation and the Gamescript auto-subscribe, with one difference worth
knowing: the GS handler defines no error reply for this command, so unlike
the other queries it can time out but can never raise ValueError.
The reply lists cargo in GSCargoList order rather than by id -- against
the dev server the ids come back 10 down to 0 -- so the docstring and
PROTOCOL.md both warn to index the list by cargo_id and not by position.
Also teaches the main_admin.py demo to resolve the labels before printing
a station's cargo, which is what the bare ids in its output were asking
for all along.
Co-Authored-By: Claude <[email protected]>
2026-08-31 18:33:05 +02:00
9 changed files with 121 additions and 9 deletions
@@ -17,8 +17,9 @@ A high-performance, Object-Oriented Python client for OpenTTD servers, specifica
- **Station Listing:** Enumerate stations via the Admin GameScript channel with `list_stations()`.
- **Station Cargo Snapshots:** `OpenTTDAdminClient.get_station()` returns a station's live per-cargo state from the running game — both the **real-time** amount waiting and the **planned** flow through the cargodist link graph — over the AdminBridge GS (stock GameScript API, no server patch needed).
- **Cargo Flow Breakdown:** `OpenTTDAdminClient.get_station_cargo()` breaks one cargo type down by **source station** and **next hop** (routing destination) for both waiting (real-time) and planned amounts, with optional `from_station`/`via_station` filters.
- **Version-checked Server Bridge:** the server-side AdminBridge GameScript that answers all of the above lives in [`gamescript/AdminBridge/`](gamescript/AdminBridge/README.md) and is mounted into the Docker server automatically. `get_bridge_version()` verifies at startup that the running bridge is new enough, so an outdated one fails by name instead of hanging every query.
- **Cargo Table:** `OpenTTDAdminClient.list_cargo()` names the bare `cargo_id`s the station queries and cargo events return — every cargo type in the running game with its label (`PASS`, `COAL`, …) and freight flag, read live so NewGRF-specific ids resolve correctly.
- **Game Events:** react to the game instead of polling it — `subscribe_events()` streams events over the AdminBridge GS as they happen: a vehicle reaching or leaving a stop (`vehicle_arrive`/`vehicle_depart`, with dwell time and cargo aboard), a station's waiting cargo changing (`cargo_waiting`), plus crashes, industries opening/closing, towns, companies and subsidies. Consume them with `await wait_for_event()` or an `on_event` callback; filter by kind, company, vehicle, station or cargo.
- **Version-checked Server Bridge:** the server-side AdminBridge GameScript that answers all of the above lives in [`gamescript/AdminBridge/`](gamescript/AdminBridge/README.md) and is mounted into the Docker server automatically. `get_bridge_version()` verifies at startup that the running bridge is new enough, so an outdated one fails by name instead of hanging every query.
@@ -92,6 +92,15 @@ Correlation, the `update_frequency` subscription requirement (auto-subscribed on
- **Filters:** `via_station` restricts the query (and the `*_by_from` breakdowns) to cargo whose next hop is that station; `from_station` restricts it (and the `*_by_via` breakdowns) to cargo from that source; supplying both makes `waiting`/`planned` the exact source-and-next-hop amount. Pass `65535` to target `STATION_INVALID`.
- **Reply (error):** same envelope with an `"error"` field: `"invalid_station"`, `"invalid_cargo"`, `"invalid_from_station"`/`"invalid_via_station"` (a filter that is neither a valid station nor `STATION_INVALID`), or `"response_too_large"`. `get_station_cargo()` raises `ValueError` for these.
### Cargo Listing
Cargo appears in the replies above (and in the `cargo_waiting` / vehicle events) as a bare numeric `cargo_id`. Those ids index the cargo table the loaded NewGRFs build for the running game, so they are **not stable across games** — the same id can be coal in one save and grain in another. `list_cargo()` resolves them, sending a `list_cargo` command over the same GameScript JSON channel and awaiting the correlated reply. The GS reads the table with the stock `GSCargoList`, `GSCargo.GetCargoLabel` and `GSCargo.IsFreight`, so this needs no server patch.
- **Reply (success):** `{"command": "list_cargo", "request_id": X, "cargo": [{"cargo_id": N, "label": "COAL", "freight": 0|1}, ...]}` — one entry per cargo type in the game, in `GSCargoList` order rather than by id, so index the list by `cargo_id`. `label` is the four-character NewGRF cargo label (`GetCargoLabel`, underscore-padded: `OIL_`), or `""` if the GS could not read it; `freight` is 1 for freight cargo and 0 for the rest (passengers, mail, …).
- **Reply (error):** the GS defines no error for this command, so `list_cargo()` never raises `ValueError` — only `asyncio.TimeoutError` (paused game, GS not loaded) and `ConnectionError`.
Correlation and the `update_frequency` subscription requirement (auto-subscribed on first use) are as described for the Timetable Query above. Unlike `list_vehicles()`/`list_stations()` — which are fire-and-forget despite the similar name — this one is awaitable and its reply does **not** reach the `on_gamescript` callback.
### Dispatch Query
`get_dispatch()` fetches an authoritative snapshot of a vehicle's **scheduled dispatch** state over the GameScript JSON channel, the vehicle analogue of `get_timetable()` for JGRPP's scheduled dispatch feature. Like the timetable getters, the dispatch getters it relies on are added by a **server patch** (`docker/patches/0002-*`, adding `GSOrder.GetScheduledDispatch*` / `IsScheduledDispatchEnabled`), so it needs the patched JGRPP build. Correlation, the auto-subscribe, and the paused-game timeout behave exactly as for the Timetable Query.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.