Files
openttd-client/docs/ARCHITECTURE.md
T
kovagoadiandClaude d87c779d94
Continuous Integration / lint-and-security (pull_request) Successful in 41s
Continuous Integration / tests-and-coverage (pull_request) Successful in 28s
Add game event support to the admin client
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]>
2026-08-31 18:14:40 +02:00

3.0 KiB

Architecture and Design

This project follows a strict Object-Oriented approach to manage the complexity of the OpenTTD network protocol.

Component Overview

1. OpenTTDProtocol (Low-Level)

Inherits from openttd_protocol.wire.tcp.TCPProtocol.

  • Responsibility: Binary serialization/deserialization and stream encryption.
  • Encryption Layer: It overrides send_packet and receive_packet to wrap/unwrap AEAD (XChaCha20-Poly1305) payloads.
  • Manual Dispatch: Uses a manual dispatch mechanism to ensure that even encrypted packets are correctly mapped to their high-level handlers.

2. OpenTTDClient (High-Level)

The primary API for developers.

  • Responsibility: Handshake orchestration, state management, and keep-alive.
  • Event-Driven: Uses asyncio.Event (like self.joined) to signal state changes to the calling code.
  • Callback System: Provides hooks like on_chat to allow users to react to game events without modifying the core library.

3. OpenTTDAdminClient (Admin Network)

Talks to the Admin port (TCP 3977) and, through the AdminBridge GameScript's JSON channel, to the running game itself.

  • Correlated Queries: get_timetable(), get_station(), get_dispatch() and friends all funnel through _gs_query(), which tags each request with a monotonic request_id, parks a future, and lets receive_ServerGamescript resolve it when the matching reply arrives.
  • Push Events: subscribe_events() opens the one stream that flows the other way. Event batches carry no request_id, so receive_ServerGamescript routes them to the event consumers instead: each event goes to the longest-waiting matching wait_for_event() caller, or into a bounded buffer if nobody is waiting, and to the on_event observer either way. See EVENTS.md.

Handshake Flow

  1. Connection: TCP connection established to Port 3979.
  2. Information: ClientGameInfo sent to verify server version.
  3. Join: ClientJoin sent with the specific JGRPP revision string.
  4. Authentication: Server sends ServerAuthenticationRequest (Type 1: PAKE).
  5. PAKE Exchange:
    • Shared Secret derived via X25519.
    • Session Keys derived via Blake2b hashing of (SharedSecret + ServerPub + OurPub + Password).
    • Encrypted challenge sent back via ClientAuthenticationResponse.
  6. Encryption: Server sends ServerEnableEncryption. The Protocol layer activates the AEAD stream.
  7. Identification: ClientIdentify sent (now encrypted).
  8. Map Synchronization: ServerWelcome received -> ClientGetMap sent -> Map segments received -> ClientMapOk sent.
  9. Active State: client.joined is set. The client responds to ServerFrame with ClientAck to prevent timeouts.

Error Handling

  • Shutdown Event: Every fatal error or manual quit triggers a shutdown_event.
  • Graceful Exit: The quit() method sends a ClientQuit packet before closing the transport.
  • Fallback: Unknown packets are caught and mapped to receive_ServerUnused to prevent the simulation loop from crashing.