This is a tiny story about my trains related side project for a feature that was requested quite often (
80501,
81682).
In large rail networks, there is quite often need to deliver a single item type to multiple places (let it be iron plates). Standard approach to solve this would be to have multiple target
train stops with different names and dedicated trains. This approach avoids the case where too many trains would go to a single target jamming all the intersections since the station was not designed for that many trains.
Another approach is to have target
train stops which belong to the same station. In this case, multiple trains will have the same schedule. To avoid all the trains from going to a single
train stop and making other targets to starve,
train stop can be disabled by circuit network when the request is already fulfilled. This approach has however one huge downside: trains horde. When a single
train stop is enabled, a lot of trains may rush to it trying to deliver their items, first
train is unloaded and rest of the trains after traveling half of the rail network have to go back since
train stop is back disabled. And this is only a 1-to-N (or N-to-1) case. For the N-to-M delivery network (like multiple mining outposts delivering ore to multiple smelters) it was nearly impossible or would require crazy circuit networks setups that would selectively disable some
train stops and would send limited amount of trains into rail network.
Suggested idea to limit amount of trains that could go to a single
train stop was kind of simple and i could see how it would solving all of those cases. For this i decided to first make a prototype which would be as simple to implement as possible, but first i need a way to hook this mechanic into existing trains logic.
When a
train wants to go to a station (which is just a name which can be assigned to multiple
train stops), there are couple of things that need to happen:
- List of possible target rails is collected.
Train stops have a rail to which they are connected to and a related inbound direction. In case of temporary
train stops, they are simply a rail pointer which counts as 2 possible targets (one for each direction
train could get to it).
- Current position of
train is passed with the list of targets to a trains pathfinder, which finds a path to one of the provided
train stops.
- Trains pathfinder returns a path which describes all the intermediate rails
train has to visit to get to the destination.
Simplest and most obvious way to hook was to prevent given
train stop from being added to the possible targets list when a
train stop is full. With this in mind, first prototype came into life: trains limit was set by writing to an exposed trains_limit variable using console commands, and every time
train was requested to find a path, i would go through list of all the trains, look to which
train stops they are going to, convert it into amount of trains going for each
train stop, and if given candidate
train stop would happen to be full, it would not be added to the list of possible targets. That way pathfinder would not be able to find a path and consequently
train would not be able to go to a full
train stop.
With this prototype i was able to play with this feature (some details are in
80501). Quite quickly i noticed some issues with this approach. Save file i was using for testing had stacker with waypoint
train stops in it.

- high_ore_usage_output_stacker.png (1.64 MiB) Viewed 32941 times
When a
train is moving, it has a braking point which is ahead of the
train and is used to decide when to start breaking. Without it, if
train would not be able to reserve a signal, it would stop immediately at the signal making bad experience. The same braking point is used with waypoint
train stops: when a braking point reaches a waypoint, path is immediately expanded to point to the next target. Officialy (when looking into the locomotive)
train is still heading for the waypoint, but internally it already has a path that goes beyond and ends on a
train stop from next schedule record. When a
train reaches a waypoint, official current goal is updated.
The issue was that the
train when going to the waypoint, would keep that
train stop from being overrun (when a
train has a path to
train stop, no more trains can go here, and when a
train repaths, it temporarily clears its target which makes that
train stop not full so it is able to path to the same
train stop), but when the braking point passed that waypoint, the waypoint would no longer be considered as full. Other
train far inside of the network would find a path and make the waypoint to be full. For now this is all fine, but when the first
train's braking point reached a signal it was unable to reserve, repath was performed. Doing a repath when existing path has waypoints clears the waypoints and forces
train to find a path to the waypoint again - and since it is already full because of other
train, this
train is now stuck in a NO_PATH state.
To fix this issue, when counting trains going for each
train stop, i had to also count the waypoints into the usage counters. This guarantees that in case of a repath with full waypoint, clearing path will make the waypoint counter to be 1 less than full and
train will be able to find a path to it.
At this point i also decided to add the circuit network input signal. This was a much better way to provide a limit value and would allow making more complex contraptions, like on ore outpost, setting limit of trains to a value related to the amount of ore in chests which could be loaded without the
train waiting for more resources.
Another issue that was noticed with the prototype, was the backpressure: When all
train stops from a given station were full, some trains when doing a repath would have no valid target making them go into the NO_PATH state. In general this approach was nice since it made trains at the ore outpost to wait before there was a free spot in iron smelter. The flaw was that when a
train stop was no longer full, no repath event was generated. There was no good way to solve this in the current prototype since it would require recounting
train stop usage counters every time any
train would repath.
At this point the prototype was frozen for couple of months, from time to time i was looking into it to decide if it could be improved.
Some day (around 2 months before 1.0 release) i found a nice solution: i need a reliable reference counter. When a
train gets a path to a
train stop, in
train stop the counter is simply incremented, and when the reference is cleared (because repath made
train go to a different
train stop) i could quickly check if new value is below current limit (keep in mind that limit was able to change) and if it was,
train stop was marked and at the end of current tick, all trains in NO_PATH state heading for a station that has
train stop marked would be forced to repath.

- fff361-train-stop.png (84.66 KiB) Viewed 32941 times
This reference counter logic was nicely wrapped and added in 3 places: in a
train (when a
train is stopped at
train stop, it keeps it reserved), in a
train's path (when a
train is heading to a
train stop it keeps it reserved) and in path waypoints. Counter itself was directly available and i decided to expose it as a signal. At this point it was also possible to implement the hard trains limit logic: when a limit was decreased i could mark the
train stop as to be updated and when it was found there are too many trains, all the trains heading for that
train stop were collected, sorted by the distance remaining and the farthest trains would be forced to repath so the limit is enforced.
This approach was working quite nicely for me, however kovarex looked at it and decided it is a bad solution: when a
train would be forced to go away, it could happen all other
train stops are also full so the
train could stop in the middle of the rail network complaining about the NO_PATH and blocking rails. It was far better if a
train already has a reservation, it should be able to reach that
train stop. For this i had to change the hook into trains logic: before the repath happens, old path is deleted. At this point reservation for the old target (or first waypoint) is stashed, and when possible targets list is created,
train stop even when being full will still be returned as valid target if the
train has a stashed reservation for it. This guarantees that
train will not stop in the middle of the rail network because of the limit decreased. At the same time, this logic made the hard limit logic to fail, since trains over the limit were always able to find a path to a full
train stop. Hard limit logic is now deleted, but if it would be highly requested by the mod authors, i could add it back.
Another complaint was the ability to specify the limit only using the circuit network: it was convenient and simple for me to implement it that way, but it was not user-friendly. In most cases the limit would be used as constant. For this i looked at the inserter's stack size override gui code and made similar thing in the
train stop. Unfortunately there was no good place in the
train stop gui due to the sliders to set a
train stop color. When the
train stop gui was reworked a little, there was a place for the simple trains limit text box that was not using the circuit control GUI.
There was one last thing to decide and solve. When all
train stops are full, target list would be empty and
train would enter a NO_PATH state. One possible solution was to handle this case the same way as station with all
train stops disabled:
train would skip such station. Personally i did not like that approach since it would not be possible to make a schedule for a
train that has to visit all the intermediate stations (
train that collects multiple different ingredients and then goes to final "crafting" station). Well, it would be possible using fake unreachable
train stop, but then it would need to not have any limits and trains could overflow all the rails close to it, or the limit would be also set here and the problem would again appear when even this
train stop was full. For this i decided the NO_PATH state is way better as it made the back pressure work nicely:
train will not leave previous
train stop it currently stands until it finds a valid target and is able to make a reservation. The NO_PATH message is however quite misleading. Technically it is a NO_PATH but i want to avoid players being confused when a
train says there is no path while all the rails and signals are properly placed. To fix this, when collecting targets when any
train stop was rejected because it was full, and in the end it happend the
train has no valid targets, the
train is told to show the "Destination full" message instead.

- fff361-destination-full.gif (1.64 MiB) Viewed 32941 times