Resolve 51 findings from the I/RUF/BLE/TRY002/S110/PLR0402 rule set: - Sort imports and __all__ (I001, RUF022, PLR0402). The sys.path.insert calls in check_public_calls.py and tests/test_e2e.py still precede the openttd imports that depend on them. - Replace unused unpacked values with _ (RUF059) and annotate the two timetable lookup tables as ClassVar (RUF012). - Narrow the best-effort excepts in OpenTTDClient.quit and OpenTTDAdminClient.quit to (OSError, SocketClosed) and log at debug rather than swallowing silently (BLE001, S110). The test doubles now raise an OSError subclass so they still exercise that branch. - Narrow the gamescript JSON fallback to json.JSONDecodeError. The broad catch in receive_packet keeps a noqa: it guards untrusted wire data and must degrade to a no-op packet instead of killing the connection. - Use contextlib.suppress instead of try/except/pass in tests. ruff check . is clean, 102 tests pass, coverage stays at 100%. Co-Authored-By: Claude <[email protected]>
186 lines
8.3 KiB
Python
186 lines
8.3 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 OpenTTDClient
|
|
from openttd.protocol import ModifyTimetableFlags
|
|
|
|
# Configuration
|
|
SERVER_HOST = "127.0.0.1"
|
|
SERVER_PORT = 3979
|
|
SERVER_PASSWORD = "asd"
|
|
|
|
# Company configuration
|
|
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
|
|
# A station DEMO_VEHICLE_ID can legally serve, used to demonstrate add_order/remove_order.
|
|
DEMO_STATION_ID = 6
|
|
|
|
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)")
|
|
|
|
# 10. Add an order to the front of the list, then remove it again (net-zero, so the
|
|
# vehicle's route is left unchanged). Inserting before position 0 and deleting
|
|
# position 0 needs no knowledge of the existing order count.
|
|
print(f"--- Step 10: add then remove a 'go to station {DEMO_STATION_ID}' order ---")
|
|
await client.add_order(vehicle_id, DEMO_STATION_ID, before_position=0)
|
|
await asyncio.sleep(0.5)
|
|
print(" -> inserted a goto-station order at position 0")
|
|
await client.remove_order(vehicle_id, 0)
|
|
await asyncio.sleep(0.5)
|
|
print(" -> removed it again (route restored)")
|
|
|
|
# 11. Scheduled dispatch: create a schedule with two departure slots, enable it, then tear it
|
|
# all down again so the vehicle is left as it started. Read it back with
|
|
# OpenTTDAdminClient.get_dispatch() (see main_admin.py); the game port has no dispatch read.
|
|
print("--- Step 11: scheduled dispatch create/enable, then clean up ---")
|
|
await client.add_dispatch_schedule(vehicle_id, start_tick=0, duration=3000)
|
|
await client.add_dispatch_slot(vehicle_id, 0, 500)
|
|
await client.add_dispatch_slot(vehicle_id, 0, 1500)
|
|
await client.set_scheduled_dispatch(vehicle_id, True)
|
|
await asyncio.sleep(0.5)
|
|
print(" -> created schedule 0 with 2 slots and enabled scheduled dispatch")
|
|
await client.set_scheduled_dispatch(vehicle_id, False)
|
|
await client.remove_dispatch_schedule(vehicle_id, 0)
|
|
await asyncio.sleep(0.5)
|
|
print(" -> disabled and removed the schedule (restored)")
|
|
|
|
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"
|
|
client = OpenTTDClient(host=SERVER_HOST, port=SERVER_PORT, username=username)
|
|
|
|
# 2. Setup chat callback (optional)
|
|
def chat_logger(cid, msg):
|
|
print(f">>> [CHAT] <{cid}> {msg}")
|
|
client.on_chat = chat_logger
|
|
|
|
try:
|
|
# 3. Connect and initiate handshake
|
|
# The client will handle PAKE auth and encryption automatically
|
|
await client.connect(server_password=SERVER_PASSWORD)
|
|
|
|
# 4. Configure company join
|
|
# This will happen automatically once the handshake is done
|
|
await client.join_company(company_id=COMPANY_ID, company_password=COMPANY_PASSWORD)
|
|
|
|
# 5. Wait for the client to be fully synced (map downloaded, states progressed)
|
|
print(f"--- Joining as {username}... ---")
|
|
await client.joined.wait()
|
|
print(f"--- Successfully joined! Client ID: {client.client_id} ---")
|
|
|
|
# 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)
|
|
except asyncio.TimeoutError:
|
|
print("--- Finished 10s stay, exiting gracefully ---")
|
|
await client.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__":
|
|
# Setup global logging
|
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(name)s:%(message)s')
|
|
|
|
# Run the async loop
|
|
try:
|
|
asyncio.run(run_client())
|
|
except KeyboardInterrupt:
|
|
pass
|