Timetables define the AI service pattern on a map:
Timetables are usually created in your map's Map.lua inside:
This page explains the full timetable workflow using the TestMap example.
Related: Create Map.lua
Before creating timetables, ensure:
Timetables reference station objects, so stations must exist before you build schedules.
A typical `loadTimetables()` builds:
At the end, the map registers them via:
controlCenter:setTimetableList(self.timetables, self.dispatchingStrategies, self.depots);
Timetables define:
A timetable template is created with:
Timetable:new("U1", 0)
| Parameter | Description |
|---|---|
| `“U1”` | Line name used for UI and routing logic. |
| `0` | Variant / index value (map-specific usage). |
Templates are usually created per direction:
Train compositions define which vehicle sets may be used when this timetable spawns AI trains.
Each composition is referenced by its `contentName`, exactly as it is registered in the ContentManager (e.g. in `Vehicle.lua` or `Composition.lua`).
Example:
contentName = "Berlin_HK_2x"
The name must match exactly. If a composition is not registered or the name is incorrect, no train will spawn for it.
Compositions are added directly to the timetable template:
:addTrainComposition("Berlin_HK_2x")
A weight can optionally be provided:
:addTrainComposition("Berlin_HK_1x", 0.5)
If no weight is specified, a default weight of `1.0` is assumed.
Weights control how likely a composition is selected relative to other compositions within the same timetable.
They only affect AI spawning behavior for this timetable.
| Weight | Meaning |
|---|---|
| 1.0 | Standard usage. The composition is commonly selected. |
| 0.5 | Reduced probability. The composition is used less frequently. |
| 0.0 | The composition is excluded from AI spawning for this timetable. |
A weight of `0.0` prevents the composition from being selected when AI trains are spawned by this timetable.
The composition itself remains a valid registered vehicle and may still:
This behavior is intentional and allows a composition to be:
Stops define the actual route of a timetable. Each stop is a table passed into `addStop({ … })`.
Example:
:addStop({ station = self.stations.TS, platform = 2, departure = 0, speedLimit = 70, routeSettingMaxETA = 0.5, })
| Field | Type | Description |
|---|---|---|
| station | Station | Reference to a station defined in `loadStations()`. |
| platform | number | Platform number the train uses at this station. Must exist in BP_StationDefinition. |
| departure | number | Minutes after service start/spawn when the train departs this stop. |
| speedLimit | number | Speed limit applied after departing this stop (signal logic dependent). |
| routeSettingMaxETA | number (optional) | How many minutes before departure the route (Fahrstraße) should be requested/set. |
| altPlatform | table<string> (optional) | Alternative platforms that may be used if the primary platform is unavailable. |
Example:
altPlatform = { "2", }
⚠️ Use string values (`“1”`, `“2”`) because platform identifiers are typically handled as strings in routing/dispatch contexts.
This allows AI to select another platform if:
Example:
routeSettingMaxETA = 0.5
Meaning:
This can help avoid early route locking and improves traffic handling at busy stations.
A timetable template does not spawn trains by itself. It must be cloned into real timetable entries and inserted into `self.timetables`.
For day-based schedules, use `DayMask`:
local DM = DayMask;
| DayMask | Meaning |
|---|---|
| DM.Weekdays | Monday to Friday |
| DM.Weekends | Saturday and Sunday |
| DM.Sat | Saturday only |
| DM.Sun | Sunday only |
| DM.Always | Every day |
The typical workflow is:
This creates a repeating service between two times:
TableUtil.insertList( self.timetables, self.TestLine_Dir1:clone(daytime(04, 30), DM.Weekdays):repeatUntil(daytime(23, 30), 10) ); TableUtil.insertList( self.timetables, self.TestLine_Dir2:clone(daytime(04, 35), DM.Weekdays):repeatUntil(daytime(23, 35), 10) );
| Call | Description |
|---|---|
| clone(daytime(HH, MM), DayMask) | Creates the first entry at a given time (filtered by the day mask). |
| repeatUntil(daytime(HH, MM), interval) | Repeats every X minutes until the end time. |
| TableUtil.insertList(list, result) | Inserts all generated entries into `self.timetables`. |
Result:
If you want to schedule individual trips (first/last train, gaps, special runs), insert a single cloned entry:
table.insert(self.timetables, self.TestLine_Dir1:clone(daytime(12, 07), DM.Weekdays)); table.insert(self.timetables, self.TestLine_Dir2:clone(daytime(12, 12), DM.Weekdays));
This creates exactly one departure at the given time.
Use this approach when you need full control over:
The following patterns are commonly used when building more advanced schedules. They are shown here using the TestMap stations (`TS`, `TSD`, `TSA`, `DP`).
Sometimes a service should start later or terminate earlier than the full route. This is useful for:
Example: terminate at `TSD` (short turn / depot related movement):
local TS_to_TSD = self.TestLine_Dir1:clone(0, nil, true):terminateAtStation("TSD", true); table.insert(self.timetables, TS_to_TSD:clone(daytime(05, 10), DM.Weekdays));
Example: start at `TSD` (depot insertion into service):
local TSD_to_TSA = self.TestLine_Dir1:clone(0, nil, true):startAtStation("TSD", true); table.insert(self.timetables, TSD_to_TSA:clone(daytime(05, 20), DM.Weekdays));
If you want to use the same template but spawn on a different platform, you can override platform numbers after cloning.
Example: force first stop to use platform 1 instead of 2 at TS:
local TS_Platform1 = self.TestLine_Dir1:clone(0, nil, true); TS_Platform1:getFirstStop().platform = 1; table.insert(self.timetables, TS_Platform1:clone(daytime(06, 00), DM.Weekdays));
This is useful if:
A service run is a trip that should not be treated as a normal passenger service.
Example: a depot-related move to TS marked as service run:
local DP_to_TS_SR = self.TestLine_Dir2:clone(0, nil, true) :startAtStation("DP", true) :terminateAtStation("TS", true) :setIsServiceRun(true); table.insert(self.timetables, DP_to_TS_SR:clone(daytime(04, 10), DM.Weekdays));
If you modify stop properties (platforms, PIS text, flags), it can be helpful to ensure the stop list is unique:
local Variant = self.TestLine_Dir1:clone(0, nil, true); Variant:forceUniqueStopList();
This prevents accidental shared stop references when creating multiple variants.
This section defines:
Depots define where AI trains are allowed to park when they are not in service. They are also used by the ControlCenter for dispatching and (later) career mode.
In the TestMap, the depot station is:
And it provides these depot tracks:
`self.depots` is a table that groups depot tracks into named blocks (groups). Each group contains a list of `Depot_DepotSpace` entries.
| Field | Meaning |
|---|---|
| station | Station reference (must exist in `self.stations`) |
| platform | Track / platform ID as defined in the BP_StationDefinition |
| direction | Which direction trains should park/spawn facing (1 or 2) |
| noParkingTimetable | If true: no dedicated parking timetable should be generated for this track (useful to keep a track free) |
---@type table<string, Depot_DepotSpace[]> self.depots = { -- Main depot area (long tracks) ["DP_51_54"] = { { station = self.stations.DP, platform = "51", direction = 2, noParkingTimetable = false }, { station = self.stations.DP, platform = "52", direction = 2, noParkingTimetable = false }, { station = self.stations.DP, platform = "53", direction = 2, noParkingTimetable = false }, { station = self.stations.DP, platform = "54", direction = 2, noParkingTimetable = false }, }, -- Short depot / test track (useful to keep free or for special moves) ["DP_60"] = { { station = self.stations.DP, platform = "60", direction = 2, noParkingTimetable = true }, }, };
Notes:
Dispatching Strategies define how trains are handled outside of normal passenger service. They are used to:
Dispatching is handled by the ControlCenter and works in addition to normal timetables.
Dispatching strategies are defined as a table, grouped by station:
---@type table<Station, ControlCenter_DispatchingStrategy[]> self.dispatchingStrategies = { [self.stations.WA] = { -- strategies for this station }, }
Each station can have multiple strategies. They are evaluated top to bottom, so order matters.
Dispatching strategies are evaluated when:
If no strategy matches, the train will remain idle.
Each dispatching strategy can define the following fields:
| Field | Description |
|---|---|
| sourceStation | Station where the train currently is. Use `nil` for depot spawns. |
| targetStation | Station the train should serve next. Use `nil` for depot despawn. |
| sourcePlatforms | Allowed platforms the train may come from. |
| targetPlatforms | Allowed platforms the train may go to. |
| depotName | Name of the depot (as defined in the depots table). |
| minLayover | Minimum minutes the train must wait before reuse. |
| keepLine | Try to keep the train on the same line. |
| replaceFirstPlatform | Replace the first stop platform if needed. |
| replaceLastPlatform | Replace the last stop platform if needed. |
| overrideFirstPlatform | Force a specific first platform. |
| overrideLastPlatform | Force a specific last platform. |
| timetable | Optional hidden timetable used for movements. |
Not all fields are required for every strategy.
This is the most common case: A train arrives at a station and turns around to serve the opposite direction.
Example (Test Map):
{ sourceStation = self.stations.Kbo, targetStation = self.stations.Kbo, sourcePlatforms = { "1" }, targetPlatforms = { "2" }, minLayover = 3, }
What happens:
No depot is involved.
Some stations require a shunting move to turn a train.
In this case, a hidden timetable is attached:
{ sourceStation = self.stations.Go, targetStation = self.stations.Go, sourcePlatforms = { "1" }, targetPlatforms = { "2" }, minLayover = 3, timetable = Timetable:new("", 0) :setIsServiceRun(true) :addStop({ station = self.stations.Go, platform = "6", departure = 2, turnAround = true, }) :addStop({ station = self.stations.Go, platform = "2", departure = 3, }), }
This allows:
When no train is available, dispatching can fetch a train from a depot.
Example:
{ sourceStation = nil, targetStation = self.stations.WA, targetPlatforms = { "1", "2" }, depotName = "WA_06_09", overrideFirstPlatform = "3", timetable = Timetable:new("", 0) :setIsServiceRun(true) :addStop({ station = self.stations.WA, platform = "7", departure = -5, }) :addStop({ station = self.stations.WA, platform = "3", departure = -3, }), }
Key points:
After service ends, trains can be removed from traffic.
Example:
{ sourceStation = self.stations.WA, sourcePlatforms = { "1", "2" }, targetStation = nil, depotName = "WA_11_18", timetable = Timetable:new("", 0) :setIsServiceRun(true) :addStop({ station = self.stations.WA, platform = "3", departure = 2, }) :addStop({ station = self.stations.WA, platform = "11", departure = 6, }), }
Here:
Timetables inside dispatching strategies:
They allow precise control over:
Dispatching strategies are evaluated in order.
Recommended structure per station:
1. normal turnarounds 2. depot spawn strategies 3. depot despawn strategies 4. fallback strategies
This avoids unnecessary depot movements and keeps traffic stable.
If dispatching behaves unexpectedly, always check the order first.
Career Mode is optional. If you don’t want career mode features on your map, you can leave `loadCareerMode()` empty.
If you *do* want career mode, this function defines:
`self.cmTakeoverStations` is a list of stations where career mode allows a takeover.
For the TestMap, we keep it simple:
-- Initializes data for career mode (optional) function TestMap:loadCareerMode() -- Stations where the player can take over in career mode self.cmTakeoverStations = { self.stations.TS, -- TestStation (main terminus) self.stations.TSA, -- TestStation Anfang (other terminus) self.stations.DP, -- Depot (optional takeover) }; end
Notes:
Route closures allow you to define sections that may be blocked in career mode. This is mainly for scenario systems and future gameplay logic.
Each closure uses:
Example for the TestMap (optional):
function TestMap:loadCareerMode() self.cmTakeoverStations = { self.stations.TS, self.stations.TSA, self.stations.DP, }; -- Optional: example closure between TS and TSA on weekends self.cmRouteClosures = { { stationSource = self.stations.TS, stationTarget = self.stations.TSA, tempClosure = DayMask.Weekends, }, }; end
If you don’t need closures, simply omit `self.cmRouteClosures`.
`self.cmGroups` defines which train compositions can appear in career mode.
Each group represents a probability set for vehicle selection.
You can define multiple groups. Each group has:
| Field | Meaning |
|---|---|
| frequency | Probability factor in the range 0.0 – 1.0 |
| compositions | List of composition `contentName` strings |
Important:
function TestMap:loadCareerMode() self.cmTakeoverStations = { self.stations.TS, self.stations.TSA, self.stations.DP, }; -- Career mode vehicle selection pool self.cmGroups = { -- Main vehicle pool (always available) { frequency = 1.0, compositions = { "Berlin_HK_2x", "Berlin_A3L92_4x", }, }, -- Optional / rare vehicles { frequency = 0.4, compositions = { "Berlin_HK_1x", "Berlin_A3L92_3x", }, }, }; end
Result:
Career mode also needs timetable templates that can be used for route finding.
For the TestMap, we reference our two templates:
function TestMap:loadCareerMode() self.cmTakeoverStations = { self.stations.TS, self.stations.TSA, self.stations.DP, }; self.cmGroups = { { frequency = 1, compositions = { "Berlin_HK_2x", "Berlin_A3L92_4x", }, }, }; -- Templates that can be used for pathfinding self.pathfindingTemplates = { self.TestLine_Dir1, self.TestLine_Dir2, }; end
If you forget this, career mode may not be able to plan valid routes on your map.
The final step is registering runtime data with the ControlCenter:
controlCenter:setStationList(self.stations); controlCenter:setTimetableList(self.timetables, self.dispatchingStrategies, self.depots);
| Call | Description |
|---|---|
| setStationList | Registers all stations for routing, UI and spawning logic. |
| setTimetableList | Registers AI services plus dispatching and depot logic. |
If this function is missing or incomplete: