Add vehicle timetable get/set support
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 22s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s

Timetables have no GameScript API surface, so this implements real
DoCommands over the game port (ClientCommand/ServerCommand) instead of
the Admin GameScript relay used for list_vehicles(): change_timetable(),
autofill_timetable(), set_timetable_start(), and set_vehicle_on_time()
send commands, while get_vehicle_timetable() reconstructs state purely
by observing ServerCommand broadcasts, since no query command exists.

Includes the custom varuint wire codec these commands require, a full
usage guide (docs/TIMETABLES.md), and a worked demo in main.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 23:21:21 +02:00
parent 0a8271d57c
commit b33869334a
10 changed files with 998 additions and 10 deletions

96
main.py
View File

@@ -7,6 +7,7 @@ import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'lib'))
from openttd import OpenTTDClient
from openttd.protocol import ModifyTimetableFlags
# Configuration
SERVER_HOST = "127.0.0.1"
@@ -17,6 +18,95 @@ SERVER_PASSWORD = "asd"
COMPANY_ID = 0 # "Én transport"
COMPANY_PASSWORD = "asd123"
# A vehicle owned by COMPANY_ID, used to demonstrate timetable get/set below. Set to a real
# vehicle id to see it in action; leave as None to skip the demonstration.
DEMO_VEHICLE_ID = 7
async def demo_timetable_workflow(client, vehicle_id):
"""A deliberately thorough walk-through of the timetable API: every ModifyTimetableFlags
variant, clear_field, autofill, timetable start, and lateness reset, on a vehicle assumed
to have at least two orders (positions 0 and 1)."""
ORDER_A, ORDER_B = 0, 1
def show(label):
print(f" -> [{label}] {client.get_vehicle_timetable(vehicle_id)}")
print(f"=== Timetable demo starting for vehicle {vehicle_id} ===")
# 1. Wait/travel times, in game ticks.
print("--- Step 1: set wait/travel times ---")
await client.change_timetable(vehicle_id, ORDER_A, ModifyTimetableFlags.WaitTime, 90)
await client.change_timetable(vehicle_id, ORDER_A, ModifyTimetableFlags.TravelTime, 240)
await client.change_timetable(vehicle_id, ORDER_B, ModifyTimetableFlags.WaitTime, 45)
await asyncio.sleep(0.5)
show("wait/travel times set")
# 2. Lock order A's wait time so autofill won't overwrite it later.
print("--- Step 2: fix order A's wait time ---")
await client.change_timetable(vehicle_id, ORDER_A, ModifyTimetableFlags.SetWaitFixed, 1)
await asyncio.sleep(0.5)
show("order A wait time fixed")
# 3. Cap order B's speed, then remove the cap again (0 = uncapped).
print("--- Step 3: cap and uncap order B's speed ---")
await client.change_timetable(vehicle_id, ORDER_B, ModifyTimetableFlags.TravelSpeed, 80)
await asyncio.sleep(0.5)
show("order B speed capped at 80")
await client.change_timetable(vehicle_id, ORDER_B, ModifyTimetableFlags.TravelSpeed, 0)
await asyncio.sleep(0.5)
show("order B speed cap removed")
# 4. Let the vehicle leave order B early once any cargo is fully loaded.
print("--- Step 4: change order B's leave type ---")
await client.change_timetable(vehicle_id, ORDER_B, ModifyTimetableFlags.SetLeaveType, 2)
await asyncio.sleep(0.5)
show("order B leave type: leave early if any cargo full")
# 5. Assign order A to scheduled-dispatch schedule 0, then unassign it again.
print("--- Step 5: assign and unassign a dispatch schedule ---")
await client.change_timetable(vehicle_id, ORDER_A, ModifyTimetableFlags.AssignSchedule, 0)
await asyncio.sleep(0.5)
show("order A assigned to dispatch schedule 0")
await client.change_timetable(vehicle_id, ORDER_A, ModifyTimetableFlags.AssignSchedule, 0xFFFFFFFF)
await asyncio.sleep(0.5)
show("order A unassigned from dispatch schedule")
# 6. Clear order B's wait time entirely (distinct from setting it to 0).
print("--- Step 6: clear order B's wait time ---")
await client.change_timetable(vehicle_id, ORDER_B, ModifyTimetableFlags.WaitTime, 0, clear_field=True)
await asyncio.sleep(0.5)
show("order B wait time cleared")
# 7. Autofill: start it preserving existing (fixed) wait times, then turn it off again.
print("--- Step 7: toggle autofill ---")
await client.autofill_timetable(vehicle_id, autofill=True, preserve_wait_time=True)
await asyncio.sleep(0.5)
show("autofill enabled (preserving wait times)")
await client.autofill_timetable(vehicle_id, autofill=False)
await asyncio.sleep(0.5)
show("autofill disabled")
# 8. Start the timetable for this vehicle only, then restart it for the whole group.
print("--- Step 8: set timetable start ---")
await client.set_timetable_start(vehicle_id, timetable_all=False, start_date=1_000_000)
await asyncio.sleep(0.5)
show("timetable started (this vehicle only)")
await client.set_timetable_start(vehicle_id, timetable_all=True, start_date=1_500_000)
await asyncio.sleep(0.5)
show("timetable restarted (whole group)")
# 9. Reset lateness for this vehicle, then for the whole group sharing its orders.
print("--- Step 9: reset lateness ---")
await client.set_vehicle_on_time(vehicle_id, apply_to_group=False)
await asyncio.sleep(0.5)
show("lateness reset (this vehicle only)")
await client.set_vehicle_on_time(vehicle_id, apply_to_group=True)
await asyncio.sleep(0.5)
show("lateness reset (whole group)")
print(f"=== Timetable demo finished. Final state for vehicle {vehicle_id}: ===")
print(f" {client.get_vehicle_timetable(vehicle_id)}")
async def run_client():
# 1. Initialize high-level client
username = sys.argv[1] if len(sys.argv) > 1 else "Modular_Joiner"
@@ -41,7 +131,11 @@ async def run_client():
await client.joined.wait()
print(f"--- Successfully joined! Client ID: {client.client_id} ---")
# 6. Lifecycle management
# 6. Timetable demonstration (requires DEMO_VEHICLE_ID to be owned by COMPANY_ID)
if DEMO_VEHICLE_ID is not None:
await demo_timetable_workflow(client, DEMO_VEHICLE_ID)
# 7. Lifecycle management
# We wait for either a manual shutdown signal or a 10s timeout
try:
await asyncio.wait_for(client.shutdown_event.wait(), timeout=10.0)