Add scheduled dispatch support (edit + authoritative view)
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 22s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s

Editing (game port, OpenTTDClient): a core of JGRPP's scheduled dispatch
DoCommands — set_scheduled_dispatch (enable/disable), add/remove schedule,
add/remove/clear slots, and set duration/start date. Adds the command IDs
to protocol.py.

Viewing (admin, OpenTTDAdminClient.get_dispatch): the GameScript API has no
dispatch support, so a new server patch (docker/patches/0002-*) adds
read-only GSOrder.GetScheduledDispatch* / IsScheduledDispatchEnabled
getters, an AdminBridge GameScript get_dispatch handler exposes them, and
get_dispatch() returns the live schedules and slots (mirrors get_timetable).

Note: set_dispatch_start_date values are normalised by the engine relative
to current game time, so they read back offset from the requested value.

Includes unit + e2e tests, a demo in main.py, and protocol/timetable docs.
The AdminBridge GameScript and the patched OpenTTD-patches clone live
outside this repo; the 0002 patch file is the durable source for the latter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 22:56:37 +02:00
parent 81a4d9333d
commit 2eea541158
11 changed files with 822 additions and 6 deletions

View File

@@ -48,10 +48,45 @@ def read_varuint_signed(data):
return value, rest
class GameCommand(IntEnum):
DeleteOrder = 51
InsertOrder = 52
ChangeTimetable = 174
SetVehicleOnTime = 176
AutofillTimetable = 177
SetTimetableStart = 180
# Scheduled dispatch (JGRPP)
SchDispatch = 205
SchDispatchAdd = 206
SchDispatchRemove = 207
SchDispatchSetDuration = 208
SchDispatchSetStartDate = 209
SchDispatchClear = 213
SchDispatchAddNewSchedule = 214
SchDispatchRemoveSchedule = 215
# Sentinel VehicleOrderID meaning "append to the end of the order list" for InsertOrder.
INVALID_VEH_ORDER_ID = 0xFFFF
class OrderType(IntEnum):
"""OrderType occupies bits 0-3 of an order's `type` byte (bits 6-7 hold OrderNonStopFlags)."""
GotoStation = 1
GotoDepot = 2
GotoWaypoint = 6
class OrderNonStopFlags(IntEnum):
"""Packed into bits 6-7 of an order's `type` byte."""
StopEverywhere = 0
NoStopAtIntermediate = 1
NoStopAtDestination = 2
NoStopAtAny = 3
class OrderStopLocation(IntEnum):
"""Packed into bits 4-5 of an order's `type` byte. Near-end/middle/through are train-only;
FarEnd is the only value the server accepts for every vehicle type, so it is the safe default."""
PlatformNearEnd = 0
PlatformMiddle = 1
PlatformFarEnd = 2
PlatformThrough = 3
class ModifyTimetableFlags(IntEnum):
WaitTime = 0