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

@@ -0,0 +1,195 @@
From 1e4bdcca84e956ece32d2d77dc8001bfd1d8e2f8 Mon Sep 17 00:00:00 2001
From: kovagoadi <kovagoadi@gmail.com>
Date: Fri, 24 Jul 2026 22:32:29 +0200
Subject: [PATCH] Add GameScript API scheduled dispatch getters to ScriptOrder
Expose read-only scheduled dispatch data to AI/GS scripts: per-vehicle
schedule count and enabled flag, per-schedule duration, start tick, max
delay and slot re-use, and per-slot offset and flags. Enables the
AdminBridge GameScript's get_dispatch command and the Python client's
OpenTTDAdminClient.get_dispatch().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
src/script/api/script_order.cpp | 81 +++++++++++++++++++++++++++++++++
src/script/api/script_order.hpp | 75 ++++++++++++++++++++++++++++++
2 files changed, 156 insertions(+)
diff --git a/src/script/api/script_order.cpp b/src/script/api/script_order.cpp
index ee18f7d588..3db4639f14 100644
--- a/src/script/api/script_order.cpp
+++ b/src/script/api/script_order.cpp
@@ -822,3 +822,84 @@ static void _DoCommandReturnSetOrderFlags(class ScriptInstance &instance)
if (duration == INVALID_TICKS) return -1;
return duration;
}
+
+/**
+ * Resolve a scheduled dispatch schedule for a vehicle, or nullptr if the vehicle/schedule is invalid.
+ */
+static const DispatchSchedule *ResolveDispatchSchedule(VehicleID vehicle_id, SQInteger schedule_index)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return nullptr;
+ const Vehicle *v = ::Vehicle::Get(vehicle_id);
+ if (v->orders == nullptr) return nullptr;
+ if (schedule_index < 0 || static_cast<uint>(schedule_index) >= v->orders->GetScheduledDispatchScheduleCount()) return nullptr;
+ return &v->orders->GetDispatchScheduleByIndex(static_cast<uint>(schedule_index));
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchScheduleCount(VehicleID vehicle_id)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return -1;
+
+ const Vehicle *v = ::Vehicle::Get(vehicle_id);
+ if (v->orders == nullptr) return 0;
+ return v->orders->GetScheduledDispatchScheduleCount();
+}
+
+/* static */ SQInteger ScriptOrder::IsScheduledDispatchEnabled(VehicleID vehicle_id)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return -1;
+
+ return ::Vehicle::Get(vehicle_id)->vehicle_flags.Test(VehicleFlag::ScheduledDispatch) ? 1 : 0;
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchDuration(VehicleID vehicle_id, SQInteger schedule_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ return ds->GetScheduledDispatchDuration();
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchStartTick(VehicleID vehicle_id, SQInteger schedule_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ return ds->GetScheduledDispatchStartTick().base();
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchDelay(VehicleID vehicle_id, SQInteger schedule_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ return ds->GetScheduledDispatchDelay();
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchReuseSlots(VehicleID vehicle_id, SQInteger schedule_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ return ds->GetScheduledDispatchReuseSlots() ? 1 : 0;
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchSlotCount(VehicleID vehicle_id, SQInteger schedule_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ return (SQInteger)ds->GetScheduledDispatch().size();
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchSlotOffset(VehicleID vehicle_id, SQInteger schedule_index, SQInteger slot_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ const std::vector<DispatchSlot> &slots = ds->GetScheduledDispatch();
+ if (slot_index < 0 || static_cast<size_t>(slot_index) >= slots.size()) return -1;
+ return slots[static_cast<size_t>(slot_index)].offset;
+}
+
+/* static */ SQInteger ScriptOrder::GetScheduledDispatchSlotFlags(VehicleID vehicle_id, SQInteger schedule_index, SQInteger slot_index)
+{
+ const DispatchSchedule *ds = ::ResolveDispatchSchedule(vehicle_id, schedule_index);
+ if (ds == nullptr) return -1;
+ const std::vector<DispatchSlot> &slots = ds->GetScheduledDispatch();
+ if (slot_index < 0 || static_cast<size_t>(slot_index) >= slots.size()) return -1;
+ return slots[static_cast<size_t>(slot_index)].flags;
+}
diff --git a/src/script/api/script_order.hpp b/src/script/api/script_order.hpp
index 6c96b91b3a..d0a7e41fa7 100644
--- a/src/script/api/script_order.hpp
+++ b/src/script/api/script_order.hpp
@@ -720,6 +720,81 @@ public:
* invalid, has no orders, or the timetable is not complete.
*/
static SQInteger GetTimetableTotalDuration(VehicleID vehicle_id);
+
+ /**
+ * Gets the number of scheduled dispatch schedules of the given vehicle.
+ * @param vehicle_id The vehicle to query.
+ * @pre ScriptVehicle::IsPrimaryVehicle(vehicle_id).
+ * @return The number of dispatch schedules (0 when the vehicle has no order list),
+ * or -1 when the vehicle is invalid.
+ */
+ static SQInteger GetScheduledDispatchScheduleCount(VehicleID vehicle_id);
+
+ /**
+ * Gets whether scheduled dispatch is enabled for the given vehicle.
+ * @param vehicle_id The vehicle to query.
+ * @pre ScriptVehicle::IsPrimaryVehicle(vehicle_id).
+ * @return 1 if enabled, 0 if disabled, or -1 when the vehicle is invalid.
+ */
+ static SQInteger IsScheduledDispatchEnabled(VehicleID vehicle_id);
+
+ /**
+ * Gets the duration in ticks of a dispatch schedule.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @return The schedule duration in ticks, or -1 when the vehicle or schedule is invalid.
+ */
+ static SQInteger GetScheduledDispatchDuration(VehicleID vehicle_id, SQInteger schedule_index);
+
+ /**
+ * Gets the start tick of a dispatch schedule.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @return The absolute start state tick, or -1 when the vehicle or schedule is invalid.
+ */
+ static SQInteger GetScheduledDispatchStartTick(VehicleID vehicle_id, SQInteger schedule_index);
+
+ /**
+ * Gets the maximum allowed delay of a dispatch schedule.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @return The maximum delay in ticks, or -1 when the vehicle or schedule is invalid.
+ */
+ static SQInteger GetScheduledDispatchDelay(VehicleID vehicle_id, SQInteger schedule_index);
+
+ /**
+ * Gets whether a dispatch schedule re-uses its dispatch slots.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @return 1 if slots are re-used, 0 if not, or -1 when the vehicle or schedule is invalid.
+ */
+ static SQInteger GetScheduledDispatchReuseSlots(VehicleID vehicle_id, SQInteger schedule_index);
+
+ /**
+ * Gets the number of departure slots in a dispatch schedule.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @return The number of slots, or -1 when the vehicle or schedule is invalid.
+ */
+ static SQInteger GetScheduledDispatchSlotCount(VehicleID vehicle_id, SQInteger schedule_index);
+
+ /**
+ * Gets the departure offset (in ticks, within the schedule duration) of a dispatch slot.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @param slot_index The slot index within the schedule.
+ * @return The slot offset, or -1 when the vehicle, schedule or slot is invalid.
+ */
+ static SQInteger GetScheduledDispatchSlotOffset(VehicleID vehicle_id, SQInteger schedule_index, SQInteger slot_index);
+
+ /**
+ * Gets the flag word of a dispatch slot.
+ * @param vehicle_id The vehicle to query.
+ * @param schedule_index The dispatch schedule index.
+ * @param slot_index The slot index within the schedule.
+ * @return The slot flags, or -1 when the vehicle, schedule or slot is invalid.
+ */
+ static SQInteger GetScheduledDispatchSlotFlags(VehicleID vehicle_id, SQInteger schedule_index, SQInteger slot_index);
};
DECLARE_ENUM_AS_BIT_SET(ScriptOrder::ScriptOrderFlags)
--
2.54.0