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]>
126 lines
5.4 KiB
Python
126 lines
5.4 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
# 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 AdminUpdateFrequency, AdminUpdateType, GameEventType
|
|
|
|
# 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:
|
|
# Cargo ids come from the loaded NewGRFs, so resolve them to labels to print.
|
|
labels = {c["cargo_id"]: c["label"]
|
|
for c in (await admin.list_cargo(timeout=10.0))["cargo"]}
|
|
|
|
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" {labels.get(cargo['cargo_id'], cargo['cargo_id'])}: "
|
|
f"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 {labels.get(cid, cid)} flow breakdown "
|
|
f"(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: # noqa: BLE001 - demo script: one failed station query should not abort the walk
|
|
print(f"!!! station query failed: {e}")
|
|
|
|
# Watch the game live: vehicles reaching/leaving stops and cargo piling up at stations.
|
|
print("--- Subscribing to game events ---")
|
|
try:
|
|
accepted = await admin.subscribe_events(
|
|
events=[GameEventType.VehicleArrive, GameEventType.VehicleDepart,
|
|
GameEventType.CargoWaiting],
|
|
interval=5, timeout=10.0)
|
|
print(f" subscribed to {accepted['events']} every {accepted['interval']} ticks")
|
|
|
|
for _ in range(5):
|
|
try:
|
|
event = await admin.wait_for_event(timeout=15.0)
|
|
except asyncio.TimeoutError:
|
|
print(" (nothing happened -- is the server paused or idle?)")
|
|
break
|
|
print(f">>> [EVENT] {event}")
|
|
await admin.unsubscribe_events()
|
|
except Exception as e: # noqa: BLE001 - demo script: report and carry on to a clean quit
|
|
print(f"!!! event subscription failed: {e}")
|
|
|
|
print("--- Quitting ---")
|
|
await admin.quit()
|
|
|
|
except Exception as e: # noqa: BLE001 - top-level demo handler: report any failure instead of dumping a traceback
|
|
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
|