Add vehicle timetable get/set support
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:
@@ -13,9 +13,17 @@ from openttd.protocol import (
|
||||
OpenTTDAdminProtocol,
|
||||
AdminUpdateType,
|
||||
AdminUpdateFrequency,
|
||||
PacketGameType
|
||||
PacketGameType,
|
||||
ModifyTimetableFlags
|
||||
)
|
||||
|
||||
# These identify a vehicle/order that already exists in the local dev server's persisted
|
||||
# save (company 0, unprotected, owns vehicle 7 with 2 orders) -- required for the timetable
|
||||
# command tests below, since DoCommands are rejected unless issued by the owning company.
|
||||
TIMETABLE_COMPANY_ID = 0
|
||||
TIMETABLE_VEHICLE_ID = 7
|
||||
TIMETABLE_ORDER_POSITION = 0
|
||||
|
||||
|
||||
# --- Pytest Fixtures ---
|
||||
|
||||
@@ -52,6 +60,22 @@ async def connected_client(server_config):
|
||||
if hasattr(client, '_transport') and not client.shutdown_event.is_set():
|
||||
await client.quit()
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def connected_owner_client(server_config):
|
||||
"""Fixture to yield a client joined to TIMETABLE_COMPANY_ID (owns a real vehicle for command tests)."""
|
||||
client_name = f"E2E_Owner_{random.randint(1000, 9999)}"
|
||||
client = OpenTTDClient(
|
||||
host=server_config["host"],
|
||||
port=server_config["game_port"],
|
||||
username=client_name
|
||||
)
|
||||
await client.connect(server_password=server_config["password"])
|
||||
await client.join_company(company_id=TIMETABLE_COMPANY_ID, company_password="")
|
||||
await asyncio.wait_for(client.joined.wait(), timeout=15.0)
|
||||
yield client
|
||||
if hasattr(client, '_transport') and not client.shutdown_event.is_set():
|
||||
await client.quit()
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# --- End-to-End Tests (Covering all public functions with multiple inputs) ---
|
||||
@@ -129,6 +153,110 @@ async def test_e2e_client_quit_and_disconnect_multiple_inputs(server_config):
|
||||
# Input 2: quit already inactive client
|
||||
await client2.quit()
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_change_timetable_wait_time(connected_owner_client):
|
||||
# Public function: change_timetable()
|
||||
# Input 1: set wait time
|
||||
await connected_owner_client.change_timetable(TIMETABLE_VEHICLE_ID, TIMETABLE_ORDER_POSITION, ModifyTimetableFlags.WaitTime, 42)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
entry = connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)
|
||||
assert entry["orders"][TIMETABLE_ORDER_POSITION]["wait_time"] == 42
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_change_timetable_travel_time(connected_owner_client):
|
||||
# Public function: change_timetable()
|
||||
# Input 2: set travel time
|
||||
await connected_owner_client.change_timetable(TIMETABLE_VEHICLE_ID, TIMETABLE_ORDER_POSITION, ModifyTimetableFlags.TravelTime, 99)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
entry = connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)
|
||||
assert entry["orders"][TIMETABLE_ORDER_POSITION]["travel_time"] == 99
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_autofill_timetable_enable(connected_owner_client):
|
||||
# Public function: autofill_timetable()
|
||||
# Input 1: enable autofill
|
||||
await connected_owner_client.autofill_timetable(TIMETABLE_VEHICLE_ID, autofill=True, preserve_wait_time=False)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
assert connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)["autofill"] is True
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_autofill_timetable_disable(connected_owner_client):
|
||||
# Public function: autofill_timetable()
|
||||
# Input 2: disable autofill, preserve wait time
|
||||
await connected_owner_client.autofill_timetable(TIMETABLE_VEHICLE_ID, autofill=False, preserve_wait_time=True)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
entry = connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)
|
||||
assert entry["autofill"] is False
|
||||
assert entry["autofill_preserve_wait_time"] is True
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_set_timetable_start_single_vehicle(connected_owner_client):
|
||||
# Public function: set_timetable_start()
|
||||
# Input 1: this vehicle only
|
||||
await connected_owner_client.set_timetable_start(TIMETABLE_VEHICLE_ID, False, 500000)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
entry = connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)
|
||||
assert entry["timetable_start"] == 500000
|
||||
assert entry["timetable_all"] is False
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_set_timetable_start_all_shared(connected_owner_client):
|
||||
# Public function: set_timetable_start()
|
||||
# Input 2: all vehicles sharing this order list
|
||||
await connected_owner_client.set_timetable_start(TIMETABLE_VEHICLE_ID, True, 600000)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
entry = connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)
|
||||
assert entry["timetable_start"] == 600000
|
||||
assert entry["timetable_all"] is True
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_set_vehicle_on_time_single_vehicle(connected_owner_client):
|
||||
# Public function: set_vehicle_on_time()
|
||||
# Input 1: reset lateness for this vehicle only
|
||||
await connected_owner_client.set_vehicle_on_time(TIMETABLE_VEHICLE_ID, apply_to_group=False)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
assert connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)["on_time_apply_to_group"] is False
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_set_vehicle_on_time_apply_to_group(connected_owner_client):
|
||||
# Public function: set_vehicle_on_time()
|
||||
# Input 2: reset lateness for every vehicle sharing these orders
|
||||
await connected_owner_client.set_vehicle_on_time(TIMETABLE_VEHICLE_ID, apply_to_group=True)
|
||||
await asyncio.sleep(1.0)
|
||||
assert not connected_owner_client.shutdown_event.is_set()
|
||||
assert connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID)["on_time_apply_to_group"] is True
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_get_vehicle_timetable_after_change(connected_owner_client):
|
||||
# Public function: get_vehicle_timetable()
|
||||
# Input 1: a vehicle with observed state
|
||||
await connected_owner_client.change_timetable(TIMETABLE_VEHICLE_ID, TIMETABLE_ORDER_POSITION, ModifyTimetableFlags.WaitTime, 15)
|
||||
await asyncio.sleep(1.0)
|
||||
assert connected_owner_client.get_vehicle_timetable(TIMETABLE_VEHICLE_ID) is not None
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_client_get_vehicle_timetable_unknown_vehicle(connected_owner_client):
|
||||
# Public function: get_vehicle_timetable()
|
||||
# Input 2: a vehicle id with no observed state
|
||||
assert connected_owner_client.get_vehicle_timetable(999999) is None
|
||||
|
||||
|
||||
# --- Admin Client Public Functions ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user