Files
openttd-client/main_admin.py
kovagoadi 81a4d9333d
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 20s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s
Add station listing and cargo queries to admin client
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>
2026-07-23 21:48:20 +02:00

101 lines
3.9 KiB
Python

import asyncio
import logging
import sys
import os
# Add the lib directory to sys.path so we can import the openttd package
sys.path.append(os.path.join(os.path.dirname(__file__), 'lib'))
from openttd import OpenTTDAdminClient
from openttd.protocol import AdminUpdateType, AdminUpdateFrequency
# Configuration
SERVER_HOST = "127.0.0.1"
ADMIN_PORT = 3977
ADMIN_PASSWORD = "asd"
async def run_admin():
admin = OpenTTDAdminClient(host=SERVER_HOST, port=ADMIN_PORT, admin_name="GeminiAdmin")
# Setup callbacks
def chat_logger(**kwargs):
print(f">>> [ADMIN CHAT] <{kwargs.get('client_id')}> {kwargs.get('message')}")
def console_logger(**kwargs):
print(f">>> [CONSOLE] [{kwargs.get('origin')}] {kwargs.get('text')}")
def gamescript_logger(data):
print(f">>> [GAMESCRIPT] {data}")
admin.on_chat = chat_logger
admin.on_console = console_logger
admin.on_gamescript = gamescript_logger
try:
await admin.connect(admin_password=ADMIN_PASSWORD, secure=True)
await admin.joined.wait()
print("--- Admin joined ---")
# Initial poll
print("--- Initial Poll ---")
await admin.poll_companies()
await admin.poll_clients()
# Subscribe to everything
await admin.update_frequency(AdminUpdateType.ClientInfo, AdminUpdateFrequency.Automatic)
await admin.update_frequency(AdminUpdateType.CompanyInfo, AdminUpdateFrequency.Automatic)
await admin.update_frequency(AdminUpdateType.Chat, AdminUpdateFrequency.Automatic)
await admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
print("--- Requesting vehicle info via GameScript ---")
await admin.list_vehicles()
# Capture station-list replies (delivered to on_gamescript, like list_vehicles) while
# still logging every other GameScript message.
stations = []
def gamescript_capture(data):
if isinstance(data, dict) and "stations" in data:
stations.append(data["stations"])
gamescript_logger(data)
admin.on_gamescript = gamescript_capture
print("--- Requesting station info via GameScript ---")
await admin.list_stations()
await asyncio.sleep(1)
# Fetch one station's authoritative live cargo (real-time waiting + planned).
if stations and stations[-1]:
sid = stations[-1][0]["id"]
try:
data = await admin.get_station(sid, timeout=10.0)
print(f"--- Station {sid} ({data.get('name')}) cargo: real-time waiting vs planned ---")
for cargo in data.get("cargo", []):
print(f" cargo {cargo['cargo_id']}: waiting={cargo['waiting']} "
f"planned={cargo['planned']} rating={cargo['rating']}")
# Break the first cargo down by source station and by next hop (routing destination).
if data.get("cargo"):
cid = data["cargo"][0]["cargo_id"]
flow = await admin.get_station_cargo(sid, cid, timeout=10.0)
print(f"--- Station {sid} cargo {cid} flow breakdown (station 65535 = none/deleted) ---")
print(f" waiting by source: {flow['waiting_by_from']}")
print(f" waiting by next hop: {flow['waiting_by_via']}")
print(f" planned by source: {flow['planned_by_from']}")
print(f" planned by next hop: {flow['planned_by_via']}")
except Exception as e:
print(f"!!! station query failed: {e}")
await asyncio.sleep(5)
print("--- Quitting ---")
await admin.quit()
except Exception as e:
print(f"!!! Error: {e}")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(name)s:%(message)s')
try:
asyncio.run(run_admin())
except KeyboardInterrupt:
pass