Added real timetable support
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 33s
Continuous Integration / tests-and-coverage (pull_request) Successful in 26s

This commit is contained in:
2026-07-23 20:59:28 +02:00
parent 954663e80c
commit 3b54a722d6
10 changed files with 561 additions and 21 deletions

View File

@@ -347,6 +347,11 @@ class OpenTTDAdminClient:
self.on_console = None
self.on_gamescript = None
# GameScript request/response correlation
self._gs_request_id = 0
self._gs_futures = {}
self._gs_subscribed = False
async def connect(self, admin_password="", secure=False):
"""Connect to the admin port and initiate handshake."""
self._admin_password = admin_password
@@ -376,6 +381,10 @@ class OpenTTDAdminClient:
def disconnect(self, source):
"""Library callback for when connection is lost."""
self.log.info("Admin disconnected.")
for fut in self._gs_futures.values():
if not fut.done():
fut.set_exception(ConnectionError("admin disconnected"))
self._gs_futures.clear()
self.shutdown_event.set()
async def quit(self):
@@ -445,6 +454,39 @@ class OpenTTDAdminClient:
payload["company_id"] = company_id
await self.send_gamescript(payload)
async def get_timetable(self, vehicle_id, timeout=5.0):
"""Fetch an authoritative timetable snapshot for a vehicle via the AdminBridge GameScript.
Unlike the game client's passive observer, this queries the real game state: it works for
timetables set before this client connected and reflects the actual (not requested) values.
Auto-subscribes to Gamescript updates on first use; if you manage update frequencies
yourself, ensure update_frequency(Gamescript, Automatic) is active before calling.
Returns a dict with vehicle-level keys (lateness, start_tick, current_order_time,
total_duration) and an "orders" list of per-order dicts (position, wait_time, travel_time,
wait_timetabled, travel_timetabled, wait_fixed, travel_fixed, leave_type, max_speed).
Raises asyncio.TimeoutError if no reply arrives (e.g. game paused, GS not loaded),
ValueError on a GameScript-reported error (invalid_vehicle, response_too_large), and
ConnectionError if the admin connection drops while waiting.
"""
from .protocol import AdminUpdateType, AdminUpdateFrequency
if not self._gs_subscribed:
await self.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
self._gs_subscribed = True
self._gs_request_id += 1
rid = self._gs_request_id
fut = asyncio.get_running_loop().create_future()
self._gs_futures[rid] = fut
try:
await self.send_gamescript({"command": "get_timetable", "vehicle_id": vehicle_id, "request_id": rid})
data = await asyncio.wait_for(fut, timeout)
finally:
self._gs_futures.pop(rid, None)
if "error" in data:
raise ValueError(f"get_timetable({vehicle_id}): {data['error']}")
return data
async def send_gamescript(self, json_data):
"""Send a JSON string to the GameScript."""
import json
@@ -540,10 +582,17 @@ class OpenTTDAdminClient:
self.log.info(f"Admin: Company {kwargs.get('company_id')} Stats: Vehicles={kwargs.get('vehicles')}, Stations={kwargs.get('stations')}")
async def receive_ServerGamescript(self, source, **kwargs):
data = kwargs.get('data')
if isinstance(data, dict):
fut = self._gs_futures.get(data.get('request_id'))
if fut is not None:
if not fut.done():
fut.set_result(data)
return
if self.on_gamescript:
self.on_gamescript(kwargs.get('data'))
self.on_gamescript(data)
else:
self.log.info(f"GAMESCRIPT: {kwargs.get('data')}")
self.log.info(f"GAMESCRIPT: {data}")
async def receive_ServerDate(self, source, **kwargs): pass
async def receive_ServerFull(self, source, **kwargs): await self.quit()