Everything on the admin GameScript channel so far has been request/reply. This adds the other direction: subscribe_events() opens a push stream so a bot can react to the game instead of polling it, consumed either by awaiting wait_for_event() or via an on_event callback. Both see every event; an event goes to at most one waiter, and unclaimed ones sit in a bounded buffer. Sixteen kinds, from two sources. The engine raises no GameScript event for a vehicle reaching a stop or cargo arriving, so vehicle_arrive, vehicle_depart and cargo_waiting are synthesised by the bridge sampling state every `interval` ticks and diffing against the previous sample -- which means a stop shorter than the interval is never reported, and the first sample only establishes a baseline. The rest (crashes, industries, towns, companies, subsidies) are engine events forwarded verbatim. vehicle_lost, vehicle_waiting_in_depot and vehicle_unprofitable are deliberately absent: the engine raises those only for AI companies, so a GameScript can never observe them. The server-side half lives in the AdminBridge GameScript, which is not in this repo -- docker/config is gitignored -- so it has to be updated separately for any of this to work. Also repoints the scheduled-dispatch E2E test at a dedicated vehicle (DISPATCH_VEHICLE_ID). It had been silently skipping because vehicle 7 carries a hand-built annual dispatch schedule, which left eight dispatch methods unverified end to end while check_public_calls.py reported them green off static analysis of the call sites. Co-Authored-By: Claude <[email protected]>
120 lines
5.1 KiB
Python
120 lines
5.1 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:
|
|
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: # 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
|