Add station listing and cargo queries to admin client
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 20s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s

Extends the AdminBridge GameScript JSON channel (the same relay used by
list_vehicles/get_timetable) with station support:

- list_stations(): enumerate stations, fire-and-forget like list_vehicles().
- get_station(): authoritative per-cargo snapshot of a station's live state,
  with both the real-time waiting amount (GSStation.GetCargoWaiting) and the
  planned cargodist link-graph flow (GetCargoPlanned), plus rating.
- get_station_cargo(): break one cargo type down by source station and by
  next hop (the cargodist routing destination) for both waiting and planned
  amounts, with optional from_station/via_station filters.

All three use stock GameScript API (no server patch, unlike timetables).
Refactors the shared GS request/reply correlation out of get_timetable and
get_station into a _gs_query() helper. Companion handlers must be added to
the server-side AdminBridge GameScript (not tracked in this repo).

Includes unit + e2e tests, a worked demo in main_admin.py, and protocol docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 21:48:20 +02:00
parent c39f970ef9
commit 81a4d9333d
6 changed files with 418 additions and 17 deletions

View File

@@ -46,6 +46,20 @@ async def test_admin_list_vehicles():
assert decode_gamescript_payload(proto.sent[0]) == {"command": "list_vehicles"}
assert decode_gamescript_payload(proto.sent[1]) == {"command": "list_vehicles", "company_id": 2}
@pytest.mark.asyncio
async def test_admin_list_stations():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
proto = MockProtocol()
client._protocol = proto
client._transport = MockTransport()
await client.list_stations()
await client.list_stations(company_id=2)
assert len(proto.sent) == 2
assert decode_gamescript_payload(proto.sent[0]) == {"command": "list_stations"}
assert decode_gamescript_payload(proto.sent[1]) == {"command": "list_stations", "company_id": 2}
@pytest.mark.asyncio
async def test_admin_client_connect_and_actions(monkeypatch):
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
@@ -258,6 +272,117 @@ async def test_admin_get_timetable_disconnect_fails_pending():
await task
assert client._gs_futures == {}
@pytest.mark.asyncio
async def test_admin_get_station_request_and_response():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
proto = MockProtocol()
client._protocol = proto
client._transport = MockTransport()
task = asyncio.ensure_future(client.get_station(3))
await asyncio.sleep(0) # let the task send the request
# First use auto-subscribes to Gamescript updates, then sends the query.
assert len(proto.sent) == 2
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
assert decode_gamescript_payload(proto.sent[1]) == {
"command": "get_station", "station_id": 3, "request_id": 1,
}
response = {"command": "get_station", "station_id": 3, "request_id": 1,
"name": "Test Central", "location": 12345, "owner": 0,
"cargo": [{"cargo_id": 0, "waiting": 42, "planned": 17, "rating": 71}]}
await client.receive_ServerGamescript(None, data=response)
assert await task == response
assert client._gs_futures == {}
@pytest.mark.asyncio
async def test_admin_get_station_error_response():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
client._protocol = MockProtocol()
client._transport = MockTransport()
task = asyncio.ensure_future(client.get_station(65535))
await asyncio.sleep(0)
await client.receive_ServerGamescript(
None, data={"command": "get_station", "station_id": 65535,
"request_id": 1, "error": "invalid_station"})
with pytest.raises(ValueError, match="invalid_station"):
await task
assert client._gs_futures == {}
@pytest.mark.asyncio
async def test_admin_get_station_timeout():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
client._protocol = MockProtocol()
client._transport = MockTransport()
with pytest.raises(asyncio.TimeoutError):
await client.get_station(3, timeout=0.05)
assert client._gs_futures == {}
@pytest.mark.asyncio
async def test_admin_get_station_cargo_request_and_response():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
proto = MockProtocol()
client._protocol = proto
client._transport = MockTransport()
task = asyncio.ensure_future(client.get_station_cargo(3, 0))
await asyncio.sleep(0) # let the task send the request
# First use auto-subscribes to Gamescript updates, then sends the query.
assert len(proto.sent) == 2
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
assert decode_gamescript_payload(proto.sent[1]) == {
"command": "get_station_cargo", "station_id": 3, "cargo_id": 0, "request_id": 1,
}
response = {"command": "get_station_cargo", "station_id": 3, "cargo_id": 0, "request_id": 1,
"waiting": 60, "planned": 40,
"waiting_by_from": [{"station": 5, "amount": 25}, {"station": 6, "amount": 35}],
"planned_by_from": [{"station": 5, "amount": 40}],
"waiting_by_via": [{"station": 7, "amount": 60}],
"planned_by_via": [{"station": 7, "amount": 40}]}
await client.receive_ServerGamescript(None, data=response)
assert await task == response
assert client._gs_futures == {}
@pytest.mark.asyncio
async def test_admin_get_station_cargo_with_filters_encoding():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
proto = MockProtocol()
client._protocol = proto
client._transport = MockTransport()
client._gs_subscribed = True # skip the auto-subscribe so only the query is sent
task = asyncio.ensure_future(client.get_station_cargo(3, 0, from_station=6, via_station=7))
await asyncio.sleep(0)
assert len(proto.sent) == 1
assert decode_gamescript_payload(proto.sent[0]) == {
"command": "get_station_cargo", "station_id": 3, "cargo_id": 0,
"from_station": 6, "via_station": 7, "request_id": 1,
}
await client.receive_ServerGamescript(
None, data={"request_id": 1, "waiting": 12, "planned": 8})
assert (await task)["waiting"] == 12
@pytest.mark.asyncio
async def test_admin_get_station_cargo_error_response():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
client._protocol = MockProtocol()
client._transport = MockTransport()
task = asyncio.ensure_future(client.get_station_cargo(3, 999))
await asyncio.sleep(0)
await client.receive_ServerGamescript(
None, data={"command": "get_station_cargo", "station_id": 3, "cargo_id": 999,
"request_id": 1, "error": "invalid_cargo"})
with pytest.raises(ValueError, match="invalid_cargo"):
await task
assert client._gs_futures == {}
@pytest.mark.asyncio
async def test_admin_gamescript_passthrough_unmatched():
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")