Files
openttd-client/docs/PROTOCOL.md
kovagoadi b33869334a
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 22s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s
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>
2026-07-16 23:21:21 +02:00

72 lines
6.2 KiB
Markdown

# Protocol Internals
This client supports the modern OpenTTD Game Port protocol (TCP 3979), specifically as implemented in JGRPP.
## X25519 PAKE Authentication
OpenTTD 14+ and JGRPP use a Password-Authenticated Key Exchange to prevent plaintext password leakage.
### Key Derivation (KDF)
We use **Blake2b** (64-byte digest) to derive two 32-byte session keys.
- **Input:** `SharedSecret (32)` + `ServerPublicKey (32)` + `ClientPublicKey (32)` + `Password (string)`
- **Output:**
- `0..31`: Client-to-Server Key
- `32..63`: Server-to-Client Key
### Handshake Nonces
The server provides a 24-byte nonce in the `ServerAuthenticationRequest`. This nonce is used for the AEAD challenge during the auth response and for the initial stream encryption setup.
## Admin Network (TCP 3977)
The Admin Network allows external applications to monitor and control the server. It supports both unsecured and secure (X25519 PAKE) authentication.
### Secure Authentication
Similar to the Game Port, the Admin Network uses X25519 PAKE for secure authentication.
- **Packet:** `AdminJoinSecure` starts the handshake.
- **Encryption:** Once enabled via `ServerEnableEncryption`, all subsequent traffic is encrypted using XChaCha20-Poly1305.
### Update Frequencies
Admins can subscribe to various updates (Date, Client Info, Company Info, etc.) at different frequencies (Poll, Daily, Weekly, Monthly, Quarterly, Annually, Automatic).
### Vehicle Listing
The Admin Network has no native packet or `AdminUpdateType` for listing individual vehicles — `ServerCompanyStats` only reports aggregate per-company vehicle counts (trains/lorries/buses/planes/ships). To retrieve an actual vehicle list, this client sends a `list_vehicles` command over the GameScript JSON channel (`AdminGamescript`/`ServerGamescript`) via `list_vehicles()`. This requires a companion GameScript running server-side that understands the `list_vehicles` command and replies with vehicle data through `ServerGamescript`.
**Important:** the server only forwards `ServerGamescript` packets to admins that have subscribed with `update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)` (enforced server-side in `NetworkAdminGameScript`, which checks `update_frequency[ADMIN_UPDATE_GAMESCRIPT]`). Call `update_frequency()` for `Gamescript` before `list_vehicles()`, or the response is silently dropped.
## Vehicle Timetables (Game Port DoCommands)
Unlike vehicle listing, timetables have no GameScript API surface at all (no getters or setters exist in `GSOrder`/`GSVehicle`). Reading and modifying them requires real engine commands (`DoCommand`s) sent over the **game port** (TCP 3979) via `ClientCommand`/`ServerCommand` packets, not the Admin Network. This section covers the wire format; for how to call the methods and what each parameter means, see the [Vehicle Timetables Usage Guide](TIMETABLES.md).
### Command envelope
Both `ClientCommand` and `ServerCommand` share this body: `company (uint8)`, `cmd (uint16 LE, index into the `Commands` enum)`, `error_msg (uint16 LE, StringID, use 0)`, `tile (uint32 LE, always 0 for these commands)`, `payload_len (uint16 LE)`, `payload (payload_len bytes)`, `callback (uint8, use 0)`, `callback_param (uint32 LE, only present if callback != 0)`. `ServerCommand` additionally appends `frame (uint32 LE)` and `my_cmd (uint8 bool)`, and is a **broadcast echo of the request** (no success/failure code) sent to every joined client, not just the sender.
### Timetable command IDs and payload tuples
| Method | `cmd` | payload |
|---|---|---|
| `change_timetable()` | 174 (`ChangeTimetable`) | `VehicleID (varuint), VehicleOrderID (uint16), ModifyTimetableFlags (uint8), value (varuint), ModifyTimetableCtrlFlags (uint8)` |
| `set_vehicle_on_time()` | 176 (`SetVehicleOnTime`) | `VehicleID (varuint), apply_to_group (uint8 bool)` |
| `autofill_timetable()` | 177 (`AutofillTimetable`) | `VehicleID (varuint), bool (uint8), bool (uint8)` |
| `set_timetable_start()` | 180 (`SetTimetableStart`) | `VehicleID (varuint), bool (uint8), StateTicks (signed varuint)` |
`VehicleID` and other 4/8-byte fields use OpenTTD's custom varuint scheme (`write_varuint`/`read_varuint` in `protocol.py`) — a UTF-8-like prefix encoding, not LEB128; signed fields (`StateTicks`) use zigzag on top of it (`write_varuint_signed`/`read_varuint_signed`).
### Ownership requirement
A command is rejected (and the client kicked) unless it's issued by the company that owns the target vehicle — join that company via `join_company()` with a real company id (not 255/spectator) before calling any timetable method.
### Reading timetables — no query command exists
There is no getter `DoCommand` for orders/timetables anywhere in the protocol. `get_vehicle_timetable()` works by passively decoding `ServerCommand` broadcasts (including the sender's own) as they arrive — it only reflects **changes made after the client joined**. A vehicle's pre-existing timetable (set before this client connected) is invisible until something changes it again; seeing it upfront would require parsing the `ORDR`/`VEHS` chunks of the initial savegame transfer (`ServerMapData`), which this client does not implement.
## Stream Encryption (AEAD)
Once `ServerEnableEncryption` is received, all subsequent packets use **XChaCha20-Poly1305** (Authenticated Encryption with Associated Data).
### Encrypted Packet Format
On the wire, encrypted packets have the following structure:
1. **Length (2 bytes):** Big-endian uint16 of the *entire* remaining packet.
2. **MAC (16 bytes):** The Poly1305 authentication tag.
3. **Ciphertext (variable):** The encrypted payload.
### Decryption Logic
The `OpenTTDProtocol` layer uses an `IncrementalAuthenticatedEncryption` state from the Monocypher library. It maintains the nonce state internally. If a MAC check fails (indicating corruption or a wrong key), the client immediately closes the connection (`SocketClosed`).
## Keep-Alive (Simulation Synchronization)
OpenTTD is a lockstep simulation. The server sends `ServerFrame` packets periodically.
- **Client Requirement:** You must respond with a `ClientAck` containing the frame number and a one-time `token` provided in the frame packet.
- **Timeout:** If the server does not receive an ACK for several in-game days, it will disconnect the client with error code 17 (`TimeoutComputer`).