Added real timetable support
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 33s
Continuous Integration / tests-and-coverage (pull_request) Successful in 26s

This commit is contained in:
2026-07-23 20:59:28 +02:00
parent 954663e80c
commit 3b54a722d6
10 changed files with 561 additions and 21 deletions

View File

@@ -26,13 +26,16 @@ RUN cmake .. \
&& make -j$(nproc) install
# Runtime stage
FROM debian:bookworm-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818
# Must track the builder's Debian release: the builder (debian:13/trixie) links
# against glibc 2.38+, so an older runtime (e.g. bookworm, glibc 2.36) cannot run
# the resulting binary. Package names use the trixie t64 spelling.
FROM debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd
RUN apt-get update && apt-get install -y \
libcurl3-gnutls \
libcurl3t64-gnutls \
liblzma5 \
liblzo2-2 \
libpng16-16 \
libpng16-16t64 \
libzstd1 \
zlib1g \
ca-certificates \

View File

@@ -25,13 +25,18 @@ This setup builds OpenTTD with the JGR Patch Pack (JGRPP) from source and runs i
Save games are stored in `config/save/`.
## JGRPP Source
The source code is cloned from the `jgrpp` branch of `https://github.com/JGRennison/OpenTTD-patches`.
The source code is cloned from the `jgrpp` branch of `https://github.com/JGRennison/OpenTTD-patches`
(currently at tag `jgrpp-0.71.1`) and carries local patches from `patches/` — see
[patches/README.md](patches/README.md). After a fresh clone of the source, apply them with
`git -C OpenTTD-patches am ../patches/*.patch` before building.
To update the server to a newer JGRPP version:
1. Update the `OpenTTD-patches` directory:
```bash
cd OpenTTD-patches && git pull && cd ..
```
2. Rebuild the image:
2. Reapply (rebase if needed) the local patches from `patches/`.
3. Rebuild the image:
```bash
docker-compose up -d --build
```

View File

@@ -0,0 +1,257 @@
From 73f6770eb3e0ec5be8da9e74fa0874fb6ba2d36d Mon Sep 17 00:00:00 2001
From: kovagoadi <kovagoadi@gmail.com>
Date: Sun, 19 Jul 2026 00:33:23 +0200
Subject: [PATCH] Add GameScript API timetable getters to ScriptOrder
Expose read-only timetable data to AI/GS scripts: per-order wait/travel
times, timetabled/fixed flags, leave type and max speed, plus per-vehicle
lateness, timetable start tick, current order time and total duration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
src/script/api/script_order.cpp | 104 ++++++++++++++++++++++++++++
src/script/api/script_order.hpp | 116 ++++++++++++++++++++++++++++++++
2 files changed, 220 insertions(+)
diff --git a/src/script/api/script_order.cpp b/src/script/api/script_order.cpp
index 865df623f9..ee18f7d588 100644
--- a/src/script/api/script_order.cpp
+++ b/src/script/api/script_order.cpp
@@ -718,3 +718,107 @@ static void _DoCommandReturnSetOrderFlags(class ScriptInstance &instance)
return ScriptMap::DistanceManhattan(origin_tile, dest_tile);
}
}
+
+/* static */ SQInteger ScriptOrder::GetTimetableWaitTime(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return -1;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return -1;
+ return order->GetWaitTime();
+}
+
+/* static */ SQInteger ScriptOrder::GetTimetableTravelTime(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return -1;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return -1;
+ return order->GetTravelTime();
+}
+
+/* static */ bool ScriptOrder::IsWaitTimetabled(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return false;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return false;
+ return order->IsWaitTimetabled();
+}
+
+/* static */ bool ScriptOrder::IsTravelTimetabled(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return false;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return false;
+ return order->IsTravelTimetabled();
+}
+
+/* static */ bool ScriptOrder::IsWaitFixed(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return false;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return false;
+ return order->IsWaitFixed();
+}
+
+/* static */ bool ScriptOrder::IsTravelFixed(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return false;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return false;
+ return order->IsTravelFixed();
+}
+
+/* static */ SQInteger ScriptOrder::GetLeaveType(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return -1;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return -1;
+ return order->GetLeaveType();
+}
+
+/* static */ SQInteger ScriptOrder::GetTimetableMaxSpeed(VehicleID vehicle_id, OrderPosition order_position)
+{
+ if (!IsValidVehicleOrder(vehicle_id, order_position)) return -1;
+
+ const Order *order = ::ResolveOrder(vehicle_id, order_position);
+ if (order == nullptr) return -1;
+ return order->GetMaxSpeed();
+}
+
+/* static */ SQInteger ScriptOrder::GetTimetableLateness(VehicleID vehicle_id)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return 0;
+
+ return ::Vehicle::Get(vehicle_id)->lateness_counter;
+}
+
+/* static */ SQInteger ScriptOrder::GetTimetableStartTick(VehicleID vehicle_id)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return -1;
+
+ return ::Vehicle::Get(vehicle_id)->timetable_start.base();
+}
+
+/* static */ SQInteger ScriptOrder::GetCurrentOrderTime(VehicleID vehicle_id)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return -1;
+
+ return ::Vehicle::Get(vehicle_id)->current_order_time;
+}
+
+/* static */ SQInteger ScriptOrder::GetTimetableTotalDuration(VehicleID vehicle_id)
+{
+ if (!ScriptVehicle::IsPrimaryVehicle(vehicle_id)) return -1;
+
+ const Vehicle *v = ::Vehicle::Get(vehicle_id);
+ if (v->orders == nullptr) return -1;
+ Ticks duration = v->orders->GetTimetableTotalDuration();
+ if (duration == INVALID_TICKS) return -1;
+ return duration;
+}
diff --git a/src/script/api/script_order.hpp b/src/script/api/script_order.hpp
index 81dc06cd7d..6c96b91b3a 100644
--- a/src/script/api/script_order.hpp
+++ b/src/script/api/script_order.hpp
@@ -604,6 +604,122 @@ public:
* @see ScriptEngine::GetMaximumOrderDistance and ScriptVehicle::GetMaximumOrderDistance
*/
static SQInteger GetOrderDistance(ScriptVehicle::VehicleType vehicle_type, TileIndex origin_tile, TileIndex dest_tile);
+
+ /**
+ * Gets the timetabled wait time of the given order for the given vehicle.
+ * @param vehicle_id The vehicle to get the timetable wait time for.
+ * @param order_position The order to get the timetable wait time for.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return The wait time of the order in ticks, or -1 when the order is invalid.
+ * @note The raw stored wait time is returned even if the wait time is not
+ * timetabled; use IsWaitTimetabled to check whether it is explicitly set.
+ */
+ static SQInteger GetTimetableWaitTime(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Gets the timetabled travel time of the given order for the given vehicle.
+ * @param vehicle_id The vehicle to get the timetable travel time for.
+ * @param order_position The order to get the timetable travel time for.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return The travel time of the order in ticks, or -1 when the order is invalid.
+ * @note The raw stored travel time is returned even if the travel time is not
+ * timetabled; use IsTravelTimetabled to check whether it is explicitly set.
+ */
+ static SQInteger GetTimetableTravelTime(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Checks whether the wait time of the given order is timetabled (explicitly set).
+ * @param vehicle_id The vehicle to check the order for.
+ * @param order_position The order to check.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return True if and only if the wait time is timetabled.
+ */
+ static bool IsWaitTimetabled(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Checks whether the travel time of the given order is timetabled (explicitly set).
+ * @param vehicle_id The vehicle to check the order for.
+ * @param order_position The order to check.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return True if and only if the travel time is timetabled.
+ */
+ static bool IsTravelTimetabled(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Checks whether the wait time of the given order is fixed (locked against autofill).
+ * @param vehicle_id The vehicle to check the order for.
+ * @param order_position The order to check.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return True if and only if the wait time is fixed.
+ */
+ static bool IsWaitFixed(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Checks whether the travel time of the given order is fixed (locked against autofill).
+ * @param vehicle_id The vehicle to check the order for.
+ * @param order_position The order to check.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return True if and only if the travel time is fixed.
+ */
+ static bool IsTravelFixed(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Gets the leave type of the given order for the given vehicle.
+ * @param vehicle_id The vehicle to get the leave type for.
+ * @param order_position The order to get the leave type for.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return The leave type of the order (0 = leave when timetabled, 1 = leave as
+ * soon as possible, 2 = leave early if any cargo fully loaded, 3 = leave early
+ * if all cargo fully loaded), or -1 when the order is invalid.
+ */
+ static SQInteger GetLeaveType(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Gets the timetabled maximum speed of the given order for the given vehicle.
+ * @param vehicle_id The vehicle to get the timetable max speed for.
+ * @param order_position The order to get the timetable max speed for.
+ * @pre IsValidVehicleOrder(vehicle_id, order_position).
+ * @return The maximum speed of the order (65535 when no speed cap is set),
+ * or -1 when the order is invalid.
+ */
+ static SQInteger GetTimetableMaxSpeed(VehicleID vehicle_id, OrderPosition order_position);
+
+ /**
+ * Gets the timetable lateness of the given vehicle.
+ * @param vehicle_id The vehicle to get the lateness for.
+ * @pre ScriptVehicle::IsPrimaryVehicle(vehicle_id).
+ * @return How many ticks the vehicle is late; negative values mean the vehicle
+ * is running early. Returns 0 when the vehicle is invalid, which is
+ * indistinguishable from an on-time vehicle; check the vehicle validity first.
+ */
+ static SQInteger GetTimetableLateness(VehicleID vehicle_id);
+
+ /**
+ * Gets the state tick at which the timetable of the given vehicle starts.
+ * @param vehicle_id The vehicle to get the timetable start tick for.
+ * @pre ScriptVehicle::IsPrimaryVehicle(vehicle_id).
+ * @return The absolute state tick the timetable starts at (0 when the
+ * timetable has not been started), or -1 when the vehicle is invalid.
+ */
+ static SQInteger GetTimetableStartTick(VehicleID vehicle_id);
+
+ /**
+ * Gets the number of ticks the given vehicle has spent on its current order.
+ * @param vehicle_id The vehicle to get the current order time for.
+ * @pre ScriptVehicle::IsPrimaryVehicle(vehicle_id).
+ * @return The number of ticks spent on the current order, or -1 when the
+ * vehicle is invalid.
+ */
+ static SQInteger GetCurrentOrderTime(VehicleID vehicle_id);
+
+ /**
+ * Gets the total duration of the timetable of the given vehicle.
+ * @param vehicle_id The vehicle to get the timetable duration for.
+ * @pre ScriptVehicle::IsPrimaryVehicle(vehicle_id).
+ * @return The total timetable duration in ticks, or -1 when the vehicle is
+ * invalid, has no orders, or the timetable is not complete.
+ */
+ static SQInteger GetTimetableTotalDuration(VehicleID vehicle_id);
};
DECLARE_ENUM_AS_BIT_SET(ScriptOrder::ScriptOrderFlags)
--
2.54.0

28
docker/patches/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Local JGRPP patches
The `docker/OpenTTD-patches/` directory is an **untracked** clone of
[JGRennison/OpenTTD-patches](https://github.com/JGRennison/OpenTTD-patches) checked out at tag
`jgrpp-0.71.1`. The patches in this directory are the local modifications this project needs on
top of that tag; they are the durable source of truth (the clone itself is not committed).
Current patches:
- `0001-Add-GameScript-API-timetable-getters-to-ScriptOrder.patch` — adds read-only timetable
getters (`GetTimetableWaitTime`, `GetTimetableTravelTime`, `IsWaitTimetabled`,
`IsTravelTimetabled`, `IsWaitFixed`, `IsTravelFixed`, `GetLeaveType`, `GetTimetableMaxSpeed`,
`GetTimetableLateness`, `GetTimetableStartTick`, `GetCurrentOrderTime`,
`GetTimetableTotalDuration`) to the `GSOrder` GameScript class. Required by the AdminBridge
GameScript's `get_timetable` command and the Python client's
`OpenTTDAdminClient.get_timetable()`.
## Applying after a fresh clone
```bash
git clone --branch jgrpp-0.71.1 https://github.com/JGRennison/OpenTTD-patches docker/OpenTTD-patches
git -C docker/OpenTTD-patches am ../patches/*.patch
```
Then build the image as usual (`docker-compose up -d --build` from `docker/`).
If the clone is updated past `jgrpp-0.71.1`, `git am` may conflict — the patches were generated
against that tag and need rebasing in that case.