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]>
3.0 KiB
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_packetandreceive_packetto 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(likeself.joined) to signal state changes to the calling code. - Callback System: Provides hooks like
on_chatto 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 monotonicrequest_id, parks a future, and letsreceive_ServerGamescriptresolve it when the matching reply arrives. - Push Events:
subscribe_events()opens the one stream that flows the other way. Event batches carry norequest_id, soreceive_ServerGamescriptroutes them to the event consumers instead: each event goes to the longest-waiting matchingwait_for_event()caller, or into a bounded buffer if nobody is waiting, and to theon_eventobserver either way. See EVENTS.md.
Handshake Flow
- Connection: TCP connection established to Port 3979.
- Information:
ClientGameInfosent to verify server version. - Join:
ClientJoinsent with the specific JGRPP revision string. - Authentication: Server sends
ServerAuthenticationRequest(Type 1: PAKE). - PAKE Exchange:
- Shared Secret derived via X25519.
- Session Keys derived via Blake2b hashing of (SharedSecret + ServerPub + OurPub + Password).
- Encrypted challenge sent back via
ClientAuthenticationResponse.
- Encryption: Server sends
ServerEnableEncryption. The Protocol layer activates the AEAD stream. - Identification:
ClientIdentifysent (now encrypted). - Map Synchronization:
ServerWelcomereceived ->ClientGetMapsent -> Map segments received ->ClientMapOksent. - Active State:
client.joinedis set. The client responds toServerFramewithClientAckto prevent timeouts.
Error Handling
- Shutdown Event: Every fatal error or manual quit triggers a
shutdown_event. - Graceful Exit: The
quit()method sends aClientQuitpacket before closing the transport. - Fallback: Unknown packets are caught and mapped to
receive_ServerUnusedto prevent the simulation loop from crashing.