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 32410 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 32410 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 32410 times