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

43 lines
3.0 KiB
Markdown

# 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](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.