Friday Facts #389 - Train control improvements

Posted by kovarex, Klonan on 2023-12-15

Hello,
trains are one of our most favorite parts of the game. We already talked about the ways we improved rails (FFF-377), so its time to talk about how we improved the way you can control the trains that ride on them.


Train Schedule Interrupts

The way you control the trains is very static at the moment. You have a list of stops to visit, and the train just goes through them one by one and thats it. The only dynamic part is the wait time at each of the stops.
There is no way to just go somewhere else dynamically based on what is going on.

The most obvious annoyance caused by this limitation is the problem with how to refuel trains. In 1.1, you need to make sure that every schedule has a stop which also refuels the locomotives. For example, the Iron drop-off station at the main base also has an inserter for loading fuel. But the more distinct routes you have, the more refueling stops you need.

It is kind of boring and repetitive, and even more annoying when you want to change the type of fuel. The alternative is to have a dedicated refueling station and just put it into every schedule, but it feels very wasteful.

The actual logic of what we would like the train to do is pretty simple:
"Do your stuff, and only when you are running low on fuel, go to the dedicated refueling station".

Since this feels very natural, we implemented a new feature in the game which allows exactly this kind of logic very easily, we call them Schedule interrupts.

The interrupts are very simple, you specify a list of conditions to trigger the interrupt, and a list of target stations + wait conditions that will be added to the schedule. Whenever the train wants to leave the current station, it checks all the interrupts one by one, evaluating the interrupt conditions. If the conditions are fulfilled, the interrupt is activated and the targets are pasted into the current schedule as temporary stops.

The available conditions are mostly the familiar set from the wait conditions (full/empty cargo, circuit condition, item/fluid count, etc.), with some special additions that only make sense for interrupts.

It was very convenient that we had already implemented the concept of temporary stations, which was used only for manually sending the train somewhere.
For those who don't know, the temporary station is something like a one-time order, once train leaves the temporary station, the entry is removed from the schedule.

The refueling example was the original motivation, but the way it works obviously has a much wider range of applications.

The generic trains

Since the interrupt can have a cargo condition, we can make an interrupt for each type of cargo, saying where it should be delivered.

For example: if you have Iron ore, go to Iron ore drop, if you have Copper ore, go to Copper ore drop, etc.

This means, that the train with these kinds of interrupts is now able to deal with whatever cargo you throw at it, so it doesn't really care if it should pick up iron or copper or whatever you support with the interrupts. At this point, there is no reason to distinguish different loading stations, and as long as you use the train stop limits, you can name all the loading stations the same, and just use the one schedule to manage all the things.

One of the big advantages of this system, is that all your trains are shared between all of the possible routes, so you don't have to think about "Copper trains are running low" or "I don't have enough circuit trains" etc. There is just one big bag of trains, and you either have enough or not.

The depot problem

This is all very nice, but it kind of created a new problem, and its the fluctuation of trains availability based on all the unloading stations being backed up or not. This can lead to an excess of trains in the system when some of the resources or production is running low, and we need a way to deal with it.

So we just added a special interrupt condition called "Destination full", which allows us to make an interrupt to send a train to a depot if all the item inputs are occupied, so it doesn't block the current station.

Some people noticed a row of depot stations in some of our screenshots, this is what they were for.

Interrupts are global

Initially interrupts were specific to each schedule, but we eventually realized, that it is a really good idea to be able to share the same interrupt between different schedules. We had the problem where once we wanted to upgrade our fuel from coal to rocket fuel for instance, we would have to go through each schedule and update the interrupt, which was not only a big hassle, but often resulted in some trains not being updated.

So we made it that interrupts are shared globally (identified by their name), and when you edit an interrupt it changes for all the trains with that interrupt. This made it much more convenient and less error prone.

Interrupt in interrupt

Normally, when an interrupt is activated, other interrupts won't be able to interfere until it is finished. But in some specific cases, this is too limiting, so we added a another special interrupt condition, called "In interrupt". This allows the interrupt to trigger while another interrupt is in progress, which clears the original interrupt and replaces it with the new interrupt targets.

There is some very specific case where this is a crucial thing to have, but it is on a planet we didn't reveal yet, so more on that later :) .


Reservation upgrade

All the more advanced interrupt schedule systems heavily depends on the train stop reservation limit. This is what prevents all the trains in your system trying to go to one stop.
But there was one little problem with the system, which could even cause traffic jams/deadlocks, and which was made much more important with the generic schedules.

The problem is that once the train decides to leave the station, it instantly clears the reservation of the train limit, while still physically blocking the stop. This lets another train start its journey toward the stop, while there might not be enough space to wait without blocking the mainline.

So we fixed it, so the train will only give up its reservation once it leaves the block with the train stop. I would be interested to know if other people encountered this problem in 1.1 as well.


Train groups

The lack of train groups and the inability to edit multiple schedules all at once was annoying, but wasn't that important before interrupts, because you didn't really need to update/tweak the existing schedules that much.

But with interrupts and more generic trains, you need to update the schedules/interrupts of existing trains all the time, so this obviously had to be done. The approach is similar to what we did with other grouping changes in the 2.0 update (logistic groups and interrupts).

So any changes to the schedule of one member of the group will update the whole group. Importantly, this ignores the temporary stops, as they are part of the state of the individual trains (so hijacking a train to chauffeur you somewhere won't break anything).

It only makes sense to show the groups in the train overview.

So now you can just place down a new train, set it to the group you want, and off it goes. If you have too many of one type, you can just reassign the train's group and turn it from an Iron train to a Copper train without hunting down an existing train to copy-paste from.

When you add a new interrupt (to handle a new item type), it will be added to all in the group and everything will 'just work' without any needless train/schedule bureaucracy.


Auto color based on destination

This is another thing teased and noticed in previous FFFs. We like to color our trains based on what they deliver, but with the generic system, any train can carry anything and static coloring just won't do the trick.

Initially we wanted to somehow add coloring to the schedule GUI, but it would be a hassle, where every station entry needs to have yet another UI element (in a GUI which is already not that simple). But we realized that a much more natural approach is available.

So we added a simple checkbox (on by default) to the locomotive color widget, to change its color based on the destination.


Conclusion

We have been asked to do something like 'logistic trains' many times. Schedule interrupts provide a more generic system, where logistic trains is just one of the things you can build from it. For example, you can have a system where you let the stop decide where to send the train using the circuit conditions and interrupts.

It is also more approachable, because you can use interrupts for just refueling or something simple, while still keeping the normal schedules.

We have been playing with this for quite some time, and it is one of the features we couldn't play without at this point.
To clarify, this is a core engine feature which will be available for everyone with the 2.0 base game update.


And more to come...

We didn't stop there, we have more improvements related to trains prepared for 2.0, but we will have to leave them for another day...

Let us know what you think at the usual places.