Added real timetable support
This commit is contained in:
@@ -31,8 +31,19 @@ The Admin Network has no native packet or `AdminUpdateType` for listing individu
|
||||
|
||||
**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.
|
||||
|
||||
When a `list_vehicles` request carries a `request_id` field, the AdminBridge GameScript echoes it back in the reply (backward compatible: absent otherwise).
|
||||
|
||||
### Timetable Query
|
||||
The stock GameScript API has no timetable getters, so this project patches the server (see `docker/patches/`) to add read-only getters to `GSOrder` (`GetTimetableWaitTime`, `GetTimetableTravelTime`, `IsWaitTimetabled`, `IsTravelTimetabled`, `IsWaitFixed`, `IsTravelFixed`, `GetLeaveType`, `GetTimetableMaxSpeed`, `GetTimetableLateness`, `GetTimetableStartTick`, `GetCurrentOrderTime`, `GetTimetableTotalDuration`). On top of that, `get_timetable()` sends a request over the same GameScript JSON channel as vehicle listing and awaits the correlated reply — an **authoritative snapshot** of the live game state, unlike the passive observer on the game port (see below).
|
||||
|
||||
- **Request:** `{"command": "get_timetable", "vehicle_id": N, "request_id": X}` — `request_id` is a client-side monotonic counter used to match the reply to the awaiting caller.
|
||||
- **Reply (success):** `{"command": "get_timetable", "vehicle_id": N, "request_id": X, "lateness": ..., "start_tick": ..., "current_order_time": ..., "total_duration": ..., "orders": [{"position", "wait_time", "travel_time", "wait_timetabled", "travel_timetabled", "wait_fixed", "travel_fixed", "leave_type", "max_speed"}, ...]}` (booleans encoded as 0/1).
|
||||
- **Reply (error):** same envelope with an `"error"` field instead of the data: `"invalid_vehicle"` (no such vehicle) or `"response_too_large"` (the reply exceeded the admin packet size limit, possible with very many orders). `get_timetable()` raises `ValueError` for these.
|
||||
|
||||
Replies carrying a `request_id` that matches a pending request resolve that request and are **not** delivered to the `on_gamescript` callback; all other `ServerGamescript` traffic reaches the callback unchanged. The same `update_frequency` subscription requirement applies (`get_timetable()` subscribes automatically on first use). Since GameScripts do not tick while the game is paused, a query against a paused server times out (`asyncio.TimeoutError`).
|
||||
|
||||
## 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).
|
||||
Unlike vehicle listing, timetables have no stock GameScript API surface (this project adds read-only getters via a server patch — see "Timetable Query" above; writing still has none). 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.
|
||||
@@ -50,8 +61,8 @@ Both `ClientCommand` and `ServerCommand` share this body: `company (uint8)`, `cm
|
||||
### 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.
|
||||
### Reading timetables — no query command exists on the game port
|
||||
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. For an authoritative read, use the Admin Network's `get_timetable()` instead (see "Timetable Query" above).
|
||||
|
||||
## Stream Encryption (AEAD)
|
||||
Once `ServerEnableEncryption` is received, all subsequent packets use **XChaCha20-Poly1305** (Authenticated Encryption with Associated Data).
|
||||
|
||||
@@ -1,20 +1,86 @@
|
||||
# Vehicle Timetables: Usage Guide
|
||||
|
||||
This guide covers `OpenTTDClient`'s timetable API: `change_timetable()`, `autofill_timetable()`,
|
||||
`set_timetable_start()`, `set_vehicle_on_time()`, and `get_vehicle_timetable()`. For wire-format
|
||||
internals (packet layout, varuint encoding, command IDs), see [PROTOCOL.md](PROTOCOL.md#vehicle-timetables-game-port-docommands).
|
||||
This guide covers the timetable API: writing via `OpenTTDClient`'s `change_timetable()`,
|
||||
`autofill_timetable()`, `set_timetable_start()`, `set_vehicle_on_time()`, and reading via
|
||||
`OpenTTDAdminClient.get_timetable()` (authoritative, recommended) or `OpenTTDClient`'s
|
||||
`get_vehicle_timetable()` (passive change observer). For wire-format internals (packet layout,
|
||||
varuint encoding, command IDs), see [PROTOCOL.md](PROTOCOL.md#vehicle-timetables-game-port-docommands).
|
||||
This guide is about *how to call these methods and what their parameters mean*, with worked examples.
|
||||
|
||||
## The two things you must know before using this API
|
||||
## Reading: `get_timetable()` — authoritative snapshot (recommended)
|
||||
|
||||
1. **You must join the vehicle's own company.** These are real game commands (`DoCommand`s), not
|
||||
admin-network calls. Join with `client.join_company(company_id=<id>, company_password=...)`
|
||||
using the id of the company that owns the vehicle — spectators (`company_id=255`, the default)
|
||||
are rejected. Sending a command for a vehicle you don't own also fails.
|
||||
2. **There is no "get timetable" query.** OpenTTD's protocol has no command that returns a
|
||||
vehicle's current timetable. `get_vehicle_timetable()` works by passively watching the
|
||||
An **awaitable** method on `OpenTTDAdminClient` (admin port, TCP 3977) that queries the real
|
||||
timetable state from the running game via the AdminBridge GameScript. Unlike the observer
|
||||
approach below, it works for timetables set **before** you connected and returns the game's
|
||||
**actual** state, not the last requested change. No `join_company` needed — it's read-only and
|
||||
sees every company's vehicles.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from openttd import OpenTTDAdminClient
|
||||
|
||||
async def main():
|
||||
admin = OpenTTDAdminClient("127.0.0.1", admin_name="TimetableReader")
|
||||
await admin.connect(admin_password="asd")
|
||||
await admin.joined.wait()
|
||||
|
||||
data = await admin.get_timetable(7)
|
||||
print(data)
|
||||
|
||||
await admin.quit()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Returns a `dict` shaped like:
|
||||
|
||||
```python
|
||||
{
|
||||
"command": "get_timetable",
|
||||
"vehicle_id": 7,
|
||||
"lateness": 0, # ticks late; negative = running early
|
||||
"start_tick": 1000000, # absolute StateTicks the timetable starts at; 0 = not started
|
||||
"current_order_time": 42, # ticks spent on the current order so far
|
||||
"total_duration": 5400, # full timetable round-trip in ticks; -1 = timetable incomplete
|
||||
"orders": [
|
||||
{
|
||||
"position": 0,
|
||||
"wait_time": 120, # ticks (raw stored value)
|
||||
"travel_time": 300, # ticks (raw stored value)
|
||||
"wait_timetabled": 1, # 1 = wait time explicitly set, 0 = not timetabled
|
||||
"travel_timetabled": 1,
|
||||
"wait_fixed": 0, # 1 = locked against autofill
|
||||
"travel_fixed": 0,
|
||||
"leave_type": 0, # 0 normal, 1 leave early, 2 early if any cargo full, 3 early if all full
|
||||
"max_speed": 65535, # order speed cap; 65535 = no cap
|
||||
},
|
||||
# ... one entry per order position
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Errors: raises `ValueError` when the GameScript reports one (`invalid_vehicle` for a nonexistent
|
||||
vehicle id; `response_too_large` if a very long order list overflows the admin packet limit),
|
||||
`asyncio.TimeoutError` when no reply arrives within `timeout` (default 5.0s — note GameScripts
|
||||
don't run while the game is **paused**, so a paused server always times out), and
|
||||
`ConnectionError` if the admin connection drops mid-query.
|
||||
|
||||
Requirements: the server must run the bundled AdminBridge GameScript **and** the patched JGRPP
|
||||
build with the `GSOrder` timetable getters (both included in this repo's `docker/` setup — see
|
||||
`docker/patches/README.md`). The Gamescript update-frequency subscription it needs is set up
|
||||
automatically on first call.
|
||||
|
||||
## Writing (and the legacy observer): things you must know
|
||||
|
||||
1. **You must join the vehicle's own company to write.** The `change_timetable()` family are real
|
||||
game commands (`DoCommand`s) on the game port, not admin-network calls. Join with
|
||||
`client.join_company(company_id=<id>, company_password=...)` using the id of the company that
|
||||
owns the vehicle — spectators (`company_id=255`, the default) are rejected. Sending a command
|
||||
for a vehicle you don't own also fails.
|
||||
2. **`get_vehicle_timetable()` is a passive observer, not a query.** It watches the
|
||||
`ServerCommand` broadcasts the server sends to every joined client whenever *anyone* changes a
|
||||
timetable. This means:
|
||||
timetable. Use it for live change monitoring on the game port; prefer `get_timetable()` above
|
||||
for reading actual state. Its limitations:
|
||||
- It only reflects changes made **after your client joined**. A vehicle's pre-existing
|
||||
timetable (set before you connected) is invisible until something changes it again.
|
||||
- It reflects what was **requested**, not a confirmed result — the wire protocol has no
|
||||
|
||||
Reference in New Issue
Block a user