Simutrans Wiki Manual
The official on-line manual for Simutrans. Read and contribute.

Some notes on scheduling – feel free to borrow what you like

Started by Octavius, May 17, 2022, 10:51:13 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.


As I wrote about a month ago, I found an almost forgotten file with some thoughts about ways to handle scheduling in Simutrans-Extended, written a few years ago when I was last active with Simutrans. I'm ordering the ideas a bit and translating them to English, so I can post them here on the forum. I will also add some examples. I do not intend those working on the scheduling feature to implement everything the way I describe here, but this may provide some ideas and inspiration. Neither is this a complete solution and I've given very little thought to the financial aspects or passenger routing or ways to prevent players from stealing other player's vehicles. There are also some suggestions that aren't vital to scheduling, but could be nice to have and may have to be considered so they can be inserted later with relative ease.

First of all, three simplifications.

1: Abolish the idea of convoys without a line. Lines and transferring convoys from one line to another, or from the end of a line to the start of the same line, are so central to scheduling in this view, that supporting convoys without a line would require a lot of special handling – although not a prohibitive amount of special handling. And one can always make a line with just a single convoy.

2: Don't tie a line to a particular waytype or mode of transport. A line set up for London Underground tube stock isn't more or less compatible with steam trains than with electric trams, so why would this line be shown when assigning a steam train to a line, but not when doing so with a tram? Further, sometimes one wants to convert a line from bus to tram or train to tram with just a few modifications.

3: Abolish mirror schedules or return journeys. They work quite nicely for simple lines, but as soon as one sets departure times or loading criteria at intermediate stops, or when it isn't obvious on which platform a train should call on the return journey, it no longer works. One can always explicitly give the return journey after the outbound part or, as we shall see, make a separate line for the return journey. For simple lines, creating this return line could be automated.

Then I propose a change in the naming of lines. Make the name of the form /player/some/arbitrary/path. Instead of showing the lines sorted by name in a list, one can now show them in a collapsible tree, like a computer filesystem, which is easier to navigate if there are many lines. By making the name of the player the first part of the path, it becomes possible to reference other player's lines when recombining convoys, like at a border station, where wagons of one player are transferred to a locomotive of another.

Further, we need a natural way of getting an integer number of hours in a month, or designing timetables gets really nasty. I once "fixed" this by redefining the clock to have 64 seconds in a minute. I only had to change the function that converts the time to a string. This worked nicely with the default metres per tile, but remains a hack.

Now we introduce the concept of a service. A service is an implementation of a line at a particular reference time. When a service is started, it has no convoy attached, but after successful initialisation, it has exactly one convoy attached. Apart from a convoy, a service has a line, a reference time, a current position in its schedule and it keeps some statistics, like the current delay. This reference time is an absolute time: year, month, hour, minute, second. The player can set some additional booleans on a service: no loading, visit depot.

A line has a name, it keeps a bunch of statistics (I'm sure some people can think of useful statistics to keep) and it has a frequency and a schedule. If the frequency is zero, a new service on the line is started whenever a convoy is available for specifically this line; else, the frequency gives the number of services per month created for this line. With 6 hours per month and 2 services per month, the services will be created with reference times 1950-January-0:00:00, 1950-January-3:00:00, 1950-February-0:00:00 etc. Furthermore, there is a boolean that the player can use to disable a line. If a line is disabled, it doesn't automatically start a service. Furthermore, a service can be created when a line is triggered by a service.

Now the main part: the schedule.

A schedule is a sequence of instructions that a service follows, from initialisation to termination. Each instruction has a type and a number of parameters, depending on the type. Common parameters are a position, referring to a 3D position on the map like a waypoint or a stop, and a time, which is always relative to the reference time of the service. Instructions may also keep some statistics. Useful statistics may be actual arrival and departure times, average payload on departure or average speed. I'm sure people can think of other useful statistics.

I propose the following instruction types:

Parameters: creation_time (time); delay_limit (time); line (string); lines_allowed_to_trigger (string)
Lines_allowed_to_trigger limits the services that can trigger this initialiser to services on lines starting with this string. By setting it to /player/, with player the name of the player controlling this line, competitors cannot trigger services on this line, which is probably a good idea.
The creation time tells when a new service is created. For example, with 6 hours per month, 12 services per month and a creation time of 0:06:00, a new service is created at 0:06:00, 0:36:00, 1:06:00 etc., with reference times 0:00:00, 0:30:00, 1:00:00 etc. The newly created service will then call a convoy belonging to a line with a name starting with line (which may be an empty string) that's waiting for this line, this instruction. When a convoy responds, the convoy is attached to this service and the service proceeds to the next instruction. If no convoy responds before the delay limit expires, the service jumps straight to the next initiate instruction in its schedule. If the last initiate instruction fails, the service is cancelled. If initialisation succeeds, all further initiate instructions are ignored.

The first instruction of a schedule must always be an initiate instruction. Old-style Simutrans schedules can be converted into new-style schedules by adding a default initiate instruction and a default terminate instruction (see below). The default initialiser has creation time 0:00:00, a delay limit equal to the period of the line and has line referring to itself. If the frequency is zero, the creation time and delay limit are ignored.

Parameters: position (position); scheduled_departure_time (time); wait_for_time (boolean); loading_criterion (percentage); allow_joining_services_to_load (boolean)
Possibly useful parameters: allow_early_departures_with_sufficient_load (boolean); disallow_loading (boolean); disallow_unloading (boolean); force_unloading (boolean)
The convoy moves from its current position to the indicated position, stops and exchanges payload. The scheduled departure time is an offset relative to the service's absolute reference time, making it an absolute time, and therefore the service can see if it's running early or late. The actual arrival and departure times, relative to the service's reference time, can be recorded and kept as a statistic for the line; this may allow the passengers to perform actual travel planning, seeking the way that brings them to their destination soonest, but that may be a bit heavy on the CPU. The difference between the scheduled departure time and the actual departure time is stored as the current delay of the service. In real life we use arrival times, not departure times, but we can't really schedule arrival times in Simutrans. The stop instruction may keep some additional statistics: average load, probability of cancellation, ...

If allow_joining_services_to_load is true and this service is coupled to a different service at this stop (see below), the joining service sees it as a stop too. This would be the typical case for passenger trains. On goods trains, this would normally be set to false. If the loading criterion hasn't been fulfilled at departure time, the service will be cancelled. The service's reference time is incremented to the reference time of the next service on the line, the same happens to all other convoys on the line with a higher reference time and the initialiser of the line skips one iteration. If the frequency is 0 and the loading criterion hasn't been fulfilled at departure time (the service has a reference time, so you could set a departure time), the service will run late. But I don't think that will be very useful.

If the service is already at the right station, but a different track, it may not be necessary to move.

Parameters: position (position); scheduled_departure_time (time); wait_for_time (boolean)
The convoy moves from its current position to the indicated position. If, on approach, the scheduled departure time has already expired or wait_for_time is false, the convoy doesn't stop, but proceeds to the next instruction. Otherwise, the convoy will stop and wait for time. The time when the convoy passes the waypoint or departs from the waypoint is recorded and the current delay is updated.

By making this a separate instruction, it's possible to set a waypoint in a station without making it a stop. By setting wait_for_time true, it's possible to instruct convoys to wait for a short period at a particular spot away from a station, for example at a passing loop.

Parameters: position (position); scheduled_departure_time (time); wait_for_time (boolean); loading_criterion (percentage)
Possibly useful parameters: allow_early_departures_with_sufficient_load (boolean); disallow_loading (boolean); disallow_unloading (boolean); force_unloading (boolean)
This one doesn't exist in Simutrans right now, but could be nice to have. It's somewhere in between a stop and a waypoint. The idea is that on a specially equipped station tile, a specially equipped vehicle can load and/or unload goods without stopping the convoy, although there may be a speed limit. If the convoy has to wait for departure time or load, it will wait just before the tile where the exchange is supposed to happen.

Flying exchanges were once common with mail and would give a new dimension to travelling post offices. Furthermore, flying exchanges are still common with dry bulk goods. A train passes at low speed under a loading chute to load goods, and over an unloading pit or through a rotary car dumper to unload. This allows loading and unloading long bulk trains without building a straight and level platform long enough to hold the train. Note that the speed during loading and unloading may have to be changed from the speed in real life, to keep loading time the same.

Flying exchanges could be generalised to using water troughs as range stops. I read about range stops on the forum. A water trough could act as a range stop without stopping.

Parameters: position (position)
Possibly useful parameters: skip if no maintenance required (boolean)
The convoy goes to a depot and remains there until it has been serviced. If the player has set visit depot on the service, it will remain there until the player sets it to a different instruction or destroys the service. If the service terminates (see below) in a depot, it will remain there until called by a different service.

Parameters: joining_line (string); delay_limit (time); couple_to_rear (boolean)
This service calls exactly one terminating service on the joining_line. The joining service has a string indicating the line it has to join, which must match the first part of the name of this line, and it must be waiting for specifically this instruction or no instruction in particular. The joining convoy then moves to the calling convoy and couples to it from the front or rear, as requested. If the called convoy has no propulsion, this is reversed: the calling convoy moves to the called convoy. If no service responds before the delay_limit expires, skip to the next instruction. This may happen when the called service has been cancelled.

There's still the problem of what to do with the waiting service when the calling service has been cancelled. I hadn't worked that one out completely.

Parameters: maximum_mass (number); maximum_length (number); couple_to_rear (boolean)
Possibly useful parameters: allow other player's services (boolean); allow private cars (boolean)
Services waiting for a general call of this line, this instruction will respond and attempt to couple. This first applies to services of the same player already waiting, then services of other players already waiting. If the calling service is still waiting at a stop, the call will remain open until departure time, with services being accepted on a first come, first serve base. It may some day be possible to couple private cars as well. The coupling convoys remember the service to which they belong and will return to that service when uncoupling.

This instruction can be used by a locomotive that just hooks up any wagons going to a particular yard, but may also be used by RoRo ferries and car shuttles. In the latter case, the calling service may be of different waytype than the called service, but must have the right waytype built in. This is quite a big jump from where Simutrans is now, but I think that RoRo ferries are such an important part of transport that they deserve a place in Simutrans.

It may be useful to block other player's services or private cars from responding to this call.

Parameters: start_line (string); start_instruction (instruction); destination_line (string); destination_instruction (instruction)
This instruction is the counterpart of the general call. The service waits at its current position until a service on the specified start_line issues a general call at the start_instruction, or proceeds right away if the call is already open. Then it couples to the convoy of that service. It will remain coupled to that service, which may itself couple (or split, but this may be a bit hard to handle), until it reaches the destination_line and destination_instruction, which must be a stop, where the service will uncouple and continue on its original schedule.

The lines give the first part of the name of the line, the instructions the exact instruction or, if -1, any instruction will do. For uncoupling that will be the next stop. I doubt that not setting an exact line or instruction will be very useful.

Coupling can happen magically, with neither the calling nor the waiting convoy moving, if both are at the same station. This way, a tractor can leave its trailer in port and go on with its business, the trailer can later join a ship when it docks. This is no more magical than allowing a loco to run around a train on a single track station.

There's one somewhat open question on this general call system. Are vehicles coupled using a general call able to load and unload while coupled? Wayfinding for passengers and goods gets much more complicated if loading and unloading is allowed in this state. Consider passengers transferring from one bus to another while both are loaded on the same ferry, while it's generally unpredictable whether these buses will be on the same ferry at the same time. On the other hand, sometimes standard gauge railway wagons are loaded on narrow gauge wagons and hauled to a quarry, where they can be loaded with stone. It used to be quite common on the German narrow gauge lines.

Parameters: selection (enumerator); number_of_units (integer); uncouple_from_front (boolean); line (string)
The selection can be one of: all loaded units; all empty units; all propulsion units; specific number of units; maybe others.
This uncouples part of the convoy, which is then made available for the first initialiser of any line with a name starting with the given string. A unit would be the smallest unit that can be handled outside a depot. For most early trains or goods trains, that would be one wagon, but for modern multiple unit trains, it would be one trainset. If the split occurs in a depot, the smallest unit could be a single vehicle, but maybe that's too confusing.

Parameters: test (string)
Possibly useful parameter: timeout (time)
(I'm cheating a bit: this instruction and the following two weren't in my notes; I just made them up.)
The service remains in place until the test returns true. Basically the same as the test for CONDITIONAL EXECUTE (see below), but some of the conditions there make no sense here. It may be useful to set a timeout, to prevent a service from getting blocked forever if the condition is never true.

Parameters: signal (integer); lines (string); earliest (time); latest (time)
This sends a signal to all services on a line whose name starts with the lines string and whose reference time is at least the reference time of the sending service plus earliest and at most the reference time of the sending service plus latest. Every service remembers all the signals it has received since creation of this service, along with the line of the sender of that signal.

A service can detect the signals it received using the WAIT UNTIL TRUE and CONDITIONAL EXECUTE instructions. In these, one can check for a specific signal send from a line with a name starting with a specific string. This string can be used to limit scope of the signals, for example to avoid responding to signals send by a competitor.

Parameters: line (string); start_instruction (instruction)
Possibly useful parameter: reference_time (time), relative to the triggering convoy's reference time
This immediately creates a service on the indicated line (you could trigger multiple services on multiple lines at once, if you find some use for that), with the current instruction set to the indicated start_instruction, which must be an initialiser. This even works to create services on disabled lines. One way of using this is to call a replacement convoy on a line before the old convoy heads off to a depot for maintenance. Another possibility with this instruction is to have a line that automatically adjusts the number of convoys depending on demand.

Parameters: test (string); assume_true (boolean)
This performs a logical test, returning true or false. The test may depend on things like: current delay; amount of payload to be (un)loaded between here and instruction x; remaining distance until maintenance is required; mass of the convoy; maximum mass per tile; maximum axle load; number of convoys present on the line; top speed; number of units in this convoy; signal x has been received from a line with a name starting with y; many more options may be useful. Only listening for signals sent by a line whose name starts with /player/ makes the service deaf to signals send by the competitor, which is probably a good idea.

If the test evaluates to true, the next instruction is executed. Else, the next instruction is skipped. The assume_true parameter is required for routing of the payload. It tells the wayfinding algorithm whether it should assume the next instruction will be executed or not.

Parameters: target_instruction (instruction)
Jump forward in the schedule to the target_instruction, skipping an arbitrary number of instructions. Combined with the conditional execute, this can be used to build arbitrary if-else blocks.

Parameters: line (string); instruction (instruction); separate_propulsion_unit (boolean); line_for_propulsion_unit (string)
Possibly useful parameter: a delay limit and some alternative action, to prevent a convoy from blocking the line if it doesn't get called.
The last instruction of a schedule must always be a terminate instruction and the one before cannot be a conditional skip.
This instruction terminates a service. It will wait for a service on a line with name starting with line calling it at instruction, or at any instruction if instruction=-1. If separate_propulsion_unit is true, then after transferring the convoy to a new line, the propulsion unit is uncoupled and transferred to a different line. This can be useful to combine two locomotive hauled trains and continue with just a single locomotive.

Old style Simutrans schedules can be converted into new style by adding a default initialiser (explained above) and a default terminator. The default terminate instruction would use itself as the line and the first instruction as instruction, with separate_propulsion_unit false.

This is an open list of possible instructions. It may be necessary to add some more.

When creating a new convoy in a depot, it can be assigned to a line and initiate instruction. It will remain in the depot until that initiate instruction is executed. The initiate instruction may specify a line, but this new convoy will match any line of the player creating the convoy.

Recombining instructions following a stop are executed as soon as the service arrives at the stop, unless the service is to wait for load. The called service may be able to load or unload at this stop. Recombining instructions can also be given at a waypoint. Convoys will normally stop there, but maybe some players like coupling and uncoupling on the move. It's actually done in real life, although it's uncommon and violates the basic principle of railway safety: never get less than braking distance away from another train.


As some may not immediately see how all this could be used, I'll give some examples.

A simple case: Amersfoort–Ede local service, no depot visits included. False booleans and ignored parameters have been skipped for brevity.
Line /CXX/Amf-Ed/westbound
Frequency=12 (every 30 minutes)
 0 INITIATE time=0:20; delay_limit=0:30; line=/CXX/; lines_allowed_to_trigger=/CXX/
 1 STOP position=Ede-Wageningen,  track 1; time=0:23; wait for time=true
 2 STOP position=Ede Centrum,     track 1; time=0:27; wait for time=true
 3 STOP position=Lunteren,        track 2; time=0:34; wait for time=true
 4 STOP position=Barneveld Zuid,  track 1; time=0:39; wait for time=true
 5 STOP position=Barneveld,       track 1; time=0:44; wait for time=true
 6 STOP position=Barneveld Noord, track 1; time=0:47; wait for time=true
 7 STOP position=Hoevelaken,      track 1; time=0:55; wait for time=true
 8 STOP position=Amersfoort,      track 4; time=1:01; wait for time=false
 9 TERMINATE line=/CXX/Amf-Ed/eastbound; instruction=0

Line /CXX/Amf-Ed/eastbound
Frequency=12 (every 30 minutes)
 0 INITIATE time=0:05; delay_limit=0:30; line=/CXX/; lines_allowed_to_trigger=/CXX/
 1 STOP position=Amersfoort,      track 4; time=0:09; wait for time=true
 2 STOP position=Hoevelaken,      track 2; time=0:15; wait for time=true
 3 STOP position=Barneveld Noord, track 1; time=0:22; wait for time=true
 4 STOP position=Barneveld,       track 1; time=0:26; wait for time=true
 5 STOP position=Barneveld Zuid,  track 1; time=0:28; wait for time=true
 6 STOP position=Lunteren,        track 1; time=0:35; wait for time=true
 7 STOP position=Ede Centrum,     track 1; time=0:42; wait for time=true
 8 STOP position=Ede-Wageningen,  track 1; time=0:46; wait for time=false
 9 TERMINATE line=/CXX/Amf-Ed/westbound; instruction=0
There are two lines, which transfer convoys to each other. With three convoys assigned to both lines combined, we can run a 30 minute service between the two towns. Note how trains in opposite directions pass each other at Lunteren and somewhere between Hoevelaken and Barneveld Noord (which is double track). The first stop instruction doesn't cause any movement, as the train is already there. In the last stop instruction, wait for time is false, as there's no need to wait before transferring the convoy to the return service. The scheduled departure time at the terminus station is ignored (and is actually the expected arrival time).

I copied this example from today's real life timetable on Dutch railways.

This doesn't appear any harder than scheduling in Simutrans-Extended currently is.

Combining trains and depot visits

Making things a bit harder, I include a fast train and a local train on the same branch route, which are combined when reaching the main part of the network. I also include a depot visit at the Amsterdam end of the line. This is similar (although not exactly the same, as we have no rush-hour dependency in Simutrans) to how the Vlissingen–Amsterdam line was operated a few years ago in the Netherlands.
Line: /NS/locopools/Amsterdam-Watergraafsmeer/double-length-double-decker
Frequency: 0
 0 INITIATE: line=/NS/; lines_allowed_to_trigger=/NS/
 1 VISIT DEPOT position=Amsterdam Watergraafsmeer
 2 TERMINATE line=/NS/ instruction=-1

Line: /NS/Vs-Asd/southbound
Frequency: 12 (every 30 minutes)
 0 INITIATE time=0:20; delay limit=0:10; line=/NS/locopools/Amsterdam-Watergraafsmeer/double-length-double-decker; lines_allowed_to_trigger=/NS/
 1 STOP position=Amsterdam Centraal,    track 2; time=0:35; wait for time=true
 2 STOP position=Amsterdam Sloterdijk,  track 7; time=0:40; wait for time=true
 3 STOP position=Haarlem,               track 6; time=0:51; wait for time=true
 4 STOP position=Heemstede-Aerdenhout,  track 2; time=0:56; wait for time=true
 5 STOP position=Leiden Centraal,       track 8; time=1:15; wait for time=true
 6 STOP position=Den Haag Laan van NOI, track 5; time=1:24; wait for time=true
 7 STOP position=Den Haag HS,           track 4; time=1:29; wait for time=true
 8 STOP position=Delft,                 track 2; time=1:35; wait for time=true
 9 STOP position=Schiedam Centrum,      track 3; time=1:43; wait for time=true
10 STOP position=Rotterdam Centraal,    track 6; time=1:51; wait for time=true
11 STOP position=Rotterdam Blaak,       track 3; time=1:54; wait for time=true
12 STOP position=Dordrecht,             track 5; time=2:07; wait for time=true
13 STOP position=Roosendaal,            track 4; time=2:36; wait for time=true
14 SPLIT selection=specific number of units; number of units=1; uncouple from front=true; line=/NS/Vs-Rsd/westbound
15 STOP position=Bergen op Zoom,        track 2; time=2:45; wait for time=true
16 STOP position=Rilland-Bath,          track 1; time=2:55; wait for time=true
17 STOP position=Krabbendijke,          track 2; time=3:00; wait for time=true
18 STOP position=Kruiningen-Yerseke,    track 2; time=3:05; wait for time=true
19 STOP position=Kapelle-Biezelinge,    track 1; time=3:11; wait for time=true
20 STOP position=Goes,                  track 1; time=3:16; wait for time=true
21 STOP position=Arnemuiden,            track 2; time=3:26; wait for time=true
22 STOP position=Middelburg,            track 1; time=3:31; wait for time=true
23 STOP position=Vlissingen Souburg,    track 1; time=3:36; wait for time=true
24 STOP position=Vlissingen,            track 3; time=3:39; wait for time=false
25 TERMINATE line=/NS/Vs-Asd/northbound; instruction=0

Line: /NS/Vs-Rsd/westbound
Frequency: 12 (every 30 minutes)
 0 INITIATE time=0:00; delay limit=0:30; line=/NS/; lines_allowed_to_trigger=/NS/
 1 STOP position=Roosendaal,            track 4; time=0:03; wait for time=true
 2 STOP position=Bergen op Zoom,        track 2; time=0:12; wait for time=true
 3 STOP position=Goes,                  track 1; time=0:32; wait for time=true
 4 STOP position=Middelburg,            track 1; time=0:44; wait for time=true
 5 STOP position=Vlissingen,            track 1; time=0:51; wait for time=false
 6 TERMINATE line=/NS/Vs-Rsd/eastbound; instruction=0

Line: /NS/Vs-Rsd/eastbound
Frequency: 12 (every 30 minutes)
 0 INITIATE time=0:00; delay limit=0:30; line=/NS/; lines_allowed_to_trigger=/NS/
 1 STOP position=Vlissingen,            track 1; time=0:10; wait for time=true
 2 STOP position=Middelburg,            track 2; time=0:16; wait for time=true
 3 STOP position=Goes,                  track 2; time=0:28; wait for time=true
 4 STOP position=Bergen op Zoom,        track 3; time=0:47; wait for time=true
 5 WAYPOINT position=just before Roosendaal;
 6 TERMINATE line=/NS/Vs-Asd/northbound; instruction=12

Line: /NS/Vs-Asd/northbound
Frequency: 12 (every 30 minutes)
 0 INITIATE time=0:15; delay limit=0:30; line=/NS/; lines_allowed_to_trigger=/NS/
 1 STOP position=Vlissingen,            track 3; time=0:21; wait for time=true
 2 STOP position=Vlissingen Souburg,    track 2; time=0:24; wait for time=true
 3 STOP position=Middelburg,            track 2; time=0:29; wait for time=true
 4 STOP position=Arnemuiden,            track 1; time=0:32; wait for time=true
 5 STOP position=Goes,                  track 2; time=0:44; wait for time=true
 6 STOP position=Kapelle-Biezelinge,    track 2; time=0:49; wait for time=true
 7 STOP position=Kruiningen-Yerseke,    track 1; time=0:54; wait for time=true
 8 STOP position=Krabbendijke,          track 1; time=1:00; wait for time=true
 9 STOP position=Rilland-Bath,          track 2; time=1:04; wait for time=true
10 STOP position=Bergen op Zoom,        track 3; time=1:14; wait for time=true
11 STOP position=Roosendaal,            track 3; time=1:31; wait for time=true; allow joining services to load=true
12 SPECIFIC CALL joining_line=/NS/Vs-Rsd/eastbound; delay limit=0:15; couple to rear=true
13 STOP position=Dordrecht,             track 1; time=1:54; wait for time=true
14 STOP position=Rotterdam Blaak,       track 2; time=2:05; wait for time=true
15 STOP position=Rotterdam Centraal,    track 9; time=2:12; wait for time=true
16 STOP position=Schiedam Centrum,      track 5; time=2:16; wait for time=true
17 STOP position=Delft,                 track 1; time=2:24; wait for time=true
18 STOP position=Den Haag HS,           track 6; time=2:33; wait for time=true
19 STOP position=Den Haag Laan van NOI, track 6; time=2:36; wait for time=true
20 STOP position=Leiden Centraal,       track 5; time=2:50; wait for time=true
21 STOP position=Heemstede-Aerdenhout,  track 1; time=3:04; wait for time=true
22 STOP position=Haarlem,               track 3; time=3:10; wait for time=true
23 STOP position=Amsterdam Sloterdijk,  track 8; time=3:19; wait for time=true
24 STOP position=Amsterdam Centraal,    track 2; time=3:25; wait for time=true
25 TERMINATE line=/NS/locopools/Amsterdam-Watergraafsmeer/double-length-double-decker; target instruction=0
At 0:20, /NS/Asd-Vs/southbound calls a train from a train pool at Amsterdam Watergraafsmeer depot. This train consists of two multiple units coupled together. The train runs empty from the depot to Amsterdam Centraal, taking about 10 minutes, and waits another 5 minutes for its departure time. Then it runs as an intercity to Roosendaal, arriving around 2:30. Immediately after arrival, it executes the split instruction, which uncouples the front EMU and transfers it to /NS/Rsd-Vs/westbound. This front unit departs Roosendaal at 2:33 and runs as a fast train to Vlissingen, arriving at 3:21. The rear unit continues as slow service to Vlissingen, departing Roosendaal at 2:36 and arriving at Vlissingen at 3:39. At 3:40 the original front unit leaves Vlissingen on the return journey, heading for a waypoint just before Roosendaal. Another slow service has already departed Vlissingen at 3:21, arriving at Roosendaal around 4:24 and immediately calling the fast service behind it. This fast service is then somewhere between Bergen op Zoom and the waypoint just before Roosendaal, so when arriving at the waypoint, it will immediately respond to the call and proceed to couple with the waiting slow service, arriving around 4:28. After the combined convoy has finished loading at 4:31, it proceeds to Amsterdam Centraal, arriving at 6:25. The convoy unloads and is transferred back to the train pool, sending it back to the depot, where it may be serviced and is ready for any service requiring double length doubledecker trains.

Pilot service on the Gotthard Line

You may need a pilot locomotive to pull heavy goods train over a mountain pass. This pilot service will help any player who needs a hand.
Line: /HeavyHaulage/XX/southbound
Frequency: ...
# Some lines of heavy goods trains originating somewhere north of the Alps
20 WAYPOINT position=Arth-Goldau;       scheduled_departure_time=5:25; wait_for_time=true
21 WAYPOINT position=Erstfeld, track x; scheduled_departure_time=6:25; wait_for_time=true
22 SPECIFIC CALL joining line=/pilotservices/locopools/Erstfeld; delay_limit=2:00; couple_to_rear=false
23 WAYPOINT position=Biasca, track y; scheduled_departure_time=7:55; wait_for_time=true
24 SPLIT selection=specific number of units; number_of_units=1; uncouple_from_front=true; line=/pilotservices/locopools/Biasca
# Proceed to some destination south of the Alps

Line: /pilotservices/locopools/Erstfeld
Frequency: 0
 0 INITIATE line=/
 1 VISIT DEPOT position=Erstfeld
 2 TERMINATE line=/ instruction=-1

# And similar for the locopool at Biasca and the northbound services.

Reroute a train when excessively delayed

I once experienced this myself on an excessively delayed train to Antwerp, when the cross city tunnel wasn't available yet. According to the normal schedule, intercity trains from Amsterdam would come from the north, pass east of the city centre, then make a right U-turn heading for Antwerpen-Centraal. At Antwerpen-Centraal, the train would reverse, heading south towards Brussels. On this day however, the train made a slight left turn instead of the U-turn and called at Antwerpen-Berchem. There, I could change to an alternative train to Antwerpen-Centraal. The waypoint east of Antwerpen city centre is meant to measure the current delay.
Line: /NS/intl/Benelux/southbound
Frequency: 6 (hourly)
 1 STOP position=Amsterdam Centraal; ...
 6 STOP position=Roosendaal; ...
 7 WAYPOINT position=east of Antwerpen city centre; time=3:35; wait_for_time=false
 8 CONDITIONAL EXECUTE current delay > 15 minutes; assume_true=false
 9 JUMP target instruction=12
10 STOP position=Antwerpen-Centraal; time=3:55; wait_for_time=true
11 JUMP target instruction=13
12 STOP position=Antwerpen-Berchem; time=4:00; wait_for_time=false
13 STOP position=Mechelen; ...

Early termination of delayed services

It happens quite often that a delayed service is cancelled, so that the return service isn't delayed too. Passengers can use other trains to the same destination, leading to a small additional delay.
Line: /NS/Nm-Hdr/southbound
Frequency: 12 (every 30 minutes)
 0 INITIATE time=0:00; delay limit=0:15; line=/NS/; lines_allowed_to_trigger=/NS/
 1 STOP position=Den Helder, track 2; time=0:04; wait_for_time=true
# 14 intermediate stops skipped
16 STOP position=Ede-Wageningen, track 4; time=2:19; wait_for_time=true
17 CONDITIONAL EXECUTE current delay > 10 minutes; assume_true=false
18 JUMP target instruction=22
19 STOP position=Arnhem Centraal, track 8; time=2:35; wait_for_time=true
20 STOP position=Nijmegen; track 3; time=2:47; wait_for_time=false
21 TERMINATE line=/NS/Nm-Shl/westbound; instruction=0;
22 STOP position=Arnhem Centraal, track 11; wait_for_time=false
23 TERMINATE line=/NS/Nm-Shl/westbound; instruction=2;

Line: /NS/Nm-Shl/westbound
Frequency: 12 (every 30 minutes)
 0 INITIATE time=0:22; delay_limit=15:00; line=/NS/; lines_allowed_to_trigger=/NS/
 1 STOP position=Nijmegen, track 3; time=0:28; wait_for_time=true
 2 INITIATE time=0:37; delay_limit=20:00; line=/NS/; lines_allowed_to_trigger=/NS/
 3 STOP position=Arnhem Centraal, track 11; time=0:45; wait_for_time=true
# 4 intermediate stations skipped
 8 STOP Schiphol Airport, track 4; time=2:00; wait_for_time=false
 9 VISIT DEPOT position=Hoofddorp
In this case trains actually alternate between two lines. They run from Nijmegen to Den Helder and back, then from Nijmegen to Schiphol Airport (actually Hoofddorp depot) and back. If delay at Ede-Wageningen on the way to Nijmegen is excessive, as detected by conditional execute 17, the service terminates at Arnhem and the convoy is transferred to the return service on instruction 2. Normally, the service continues to Nijmegen, terminates there and transfers the convoy to the return service at instruction 0. If the service has terminated early, the initialiser of the return service at instruction 0 will fail to find a convoy before the delay limit expires and will try again at instruction 2. This will now succeed, making Arnhem Centraal the initial station of the return service.

A RoRo ferry

Ameland is a small island off the north coast of the Netherlands. The economy depends mostly on tourism, but there are some dairy farms as well. In the past, milk was transported to the mainland by pipeline, but nowadays it's done with a lorry on a RoRo ferry. This ferry also moves lorries with livestock or goods to supply shops, mail vans and passengers.
Line: /Co-operative Milk/collectors/Fryslân/Ameland
Frequency: 0
 0 INITIATE line=/Co-operative Milk/; lines_allowed_to_trigger=/Co-operative Milk/
 1 WAYPOINT position=Holwert ferry pier; wait for time=false
 2 WAIT FOR GENERAL CALL start line=/SimuShipping/Ameland; start instruction=2; destination line=/SimuShipping/Ameland; destination instruction=3
 3 WAYPOINT position=some carpark on Ameland where we don't block traffic
 4 WAIT UNTIL TRUE payload waiting for this line between here and instruction 8 (inclusive) larger than 0
 5 CONDITIONAL EXECUTE if load waiting at next stop > 0; assume true
 6 STOP position=Ameland Dairy 1
 7 CONDITIONAL EXECUTE if load waiting at next stop > 0; assume true
 8 STOP position=Ameland Dairy 2
 9 WAYPOINT position=Ameland ferry pier; wait for time=false
10 WAIT FOR GENERAL CALL start line=/SimuShipping/Ameland; start instruction=4; destination line=/SimuShipping/Ameland; destination instruction=5
11 STOP position=Dokkum dairy processor
12 TERMINATE line=/Co-operative Milk/collectors/Fryslân/Ameland; instruction=0

Line: /NOF/bus/Holwert
Frequency=6 (hourly)
 0 INITIATE time=0:15; delay_limit=0:10; line=/NOF/buspools/Leeuwarden/long-distance, lines_allowed_to_trigger=/NOF/
 1 STOP position=Leeuwarden;         time=0:24; wait for time=true
 2 STOP position=Koarnjum;           time=0:35; wait for time=false
 3 STOP position=Ferwert;            time=0:48; wait for time=false
 4 STOP position=Holwert;            time=0:56; wait for time=false
 5 STOP position=Holwert ferry pier; time=1:20; wait for time=true
 6 STOP position=Holwert;            time=1:24; wait for time=false
 7 STOP position=Ferwert;            time=1:32; wait for time=false
 8 STOP position=Koarnjum;           time=1:45; wait for time=false
 9 STOP position=Leeuwarden;         time=1:56; wait for time=false
10 TERMINATE line=/NOF/buspools/Leeuwarden/long-distance; instruction=0

Line: /NOF/buspools/Leeuwarden/long-distance
 0 INITIATE line=/NOF/; lines_allowed_to_trigger=/NOF/
 1 VISIT DEPOT position=Leeuwarden depot
 2 TERMINATE line=/NOF/; instruction=-1

Line: /NOF/bus/Ameland
Frequency=6 (hourly)
 0 INITIATE time=0:15; delay_limit=0:10; line=/NOF/; lines_allowed_to_trigger=/NOF/
 1 JUMP target instruction=5
 2 INITIATE time=0:25; delay_limit=1:00; line=/NOF/; lines_allowed_to_trigger=/NOF/
 3 WAYPOINT position=Holwert ferry pier; wait_for_time=false
 4 WAIT FOR GENERAL CALL start_line=/SimuShipping/Ameland; start_instruction=2; destination_line=/SimuShipping/Ameland; destination_instruction=3
 5 STOP position=Ameland ferry pier; time=0:25; wait_for_time=true
 6 STOP position=Nes;                time=0:28; wait_for_time=false
 7 STOP position=Ballum;             time=0:37; wait_for_time=false
 8 STOP position=Hollum;             time=0:47; wait_for_time=true
 9 STOP position=Ballum;             time=0:53; wait_for_time=false
10 STOP position=Nes;                time=1:02; wait_for_time=false
11 STOP position=Ameland ferry pier; wait_for_time=false
12 CONDITIONAL EXECUTE if distance to next maintenance < 100 km; assume false
13 JUMP target instruction=15
14 TERMINATE line=/NOF/bus/Ameland; instruction=0
15 WAIT FOR GENERAL CALL start_line=/SimuShipping/Ameland; start_instruction=4; destination_line=/SimuShipping/Ameland; destination_instruction=5
16 VISIT DEPOT position=Leeuwarden depot
17 TERMINATE line=/NOF/bus/Ameland; instruction=2

Line: /SimuShipping/Ameland
Frequency=6 (hourly)
 0 INITIATE time=0:10; delay_limit=0:30; line=/SimuShipping/; lines_allowed_to_trigger=/SimuShipping/
 1 STOP position=Holwert ferry pier; time=0:20; wait_for_time=true
 2 GENERAL CALL maximum_mass=400t; maximum_length=350m
 3 STOP position=Ameland ferry pier; time=1:20; wait_for_time=true
 4 GENERAL CALL maximum_mass=400t; maximum_length=350m
 5 STOP position=Holwert ferry pier; wait_for_time=false
 6 TERMINATE line=/SimuShipping/Ameland; instruction=0
The RoRo ferry just goes forth and back between Holwert and Ameland every 2 hours. There are two ships on the line, given an hourly service.

The milk lorry runs from the dairy processor in Dokkum to the ferry pier at Holwert, waits for the ferry and boards it, heading for the island. There it waits until either the first or the second dairy farm has milk waiting to be picked up. The lorry collects the milk and returns to the ferry pier. It boards the ferry again and delivers milk to to dairy processor.

For the bus lines I included depot visits. The first bus line connects the town and railway station of Leeuwarden to Holwert ferry pier, waiting at the ferry pier each time the ferry docks. The second bus line only goes back and forth on the island. The ferry has passenger accommodation, so the passengers don't have to stay on the bus to cross the sea. Only when the bus on the island needs maintenance, it boards the ferry empty, runs to Leeuwarden depot for its maintenance, then returns to the island. If we make the schedule a bit more complex by using the TRIGGER SERVICE instruction, we can actually call a replacement from the depot before the old bus heads back to the depot, preventing interruption of the service.

The nice thing about this arrangement is that we can move all kinds of goods to and from the island, and even have a bus service on the island, without needing a road depot there or specialised boats (or holds on one large boat) for different cargo types. A road depot on such a small island would be too expensive. We can even make a nice profit moving private cars to and from the island.

So, feel free to be inspired by this, if it is of any use.