Bag of trains tutorial
Posted: Wed Dec 04, 2024 6:23 pm
This is a beginner tutorial for getting a "bag of trains" setup going in vanilla. It is meant to provide a short and easy path to a working and robust implementation, not necessarily the fanciest or best implementation. People who've been playing Factorio for a long time could piece together how to do "bag of trains" from reading the FFF, but not everyone has the time or mental energy. Moreover, while the circuit details aren't hard, they weren't explicitly spelled out in the FFF articles. It is easy to get started, but also easy for a beginner to make mistakes.
Enough jabber, on to the implementation.
Notes: All trains are assumed top be 1-4 trains, meaning 1 locomotive and 4 cargo or fluid carriages.
The core idea is this: Do as little with circuits as possible and let the Factorio game engine do as much as possible. That's it.
To achieve that, we are going to let Factorio decide things like which train to dispatch and where it goes. We aren't going to think about calculating how many trains go to a stop or which train to send. First up, you need two kinds of trains for a basic setup, fluid trains and cargo trains. We are going to look at fluid trains, since cargo follows the same rules but with chests instead of storage tanks. The reason you need distinct types of trains is that fluid transportation requires different types of carriages and loading mechanisms - it's just easier to use 2 types of train.
Every fluid loading station has the same name: fluid load. Every "requester" station has a name of the form "<item symbol> unload". You need to use the item symbol in the name for the interrupts to work correctly. We'll get to that.
A short detour on how interrupts work: When a train is ready to depart its current stop, it will check for interrupts and set a temporary station for whichever it finds. That is the moment interrupts happen - not while the train is out and about or even during a stop, but only at the end of a stop.
Fluid unloading
Here is what your fluid unloading stop looks like:
We need 4 combinators in total and they are hooked up in a very particular way. The left-most combinator calculates the quantity of fluid needed to fill all the tanks, stored in the variable N for "need".
The next combinator calculates how many fuild trains are required to deliver the needed amount. That gets output to the variable T for "trains." Factorio, sensibly, uses C style division, so for example 5 divided by 3 is 1 since 5 = 3 * 1 + 2. Integer division returns the "quotient", or the number of times one number "goes into" the other. In the screenshot, our T variable is 0 since 11 / 12 = 0 * 12 + 11. The value of T is used to set the train limit on the stop. Why? If we don't set limits, then any train dispatched to a station of this type will just stop at the closest station of that type. We have to "close off" stations with 0 need so that trains intended for more distant stations don't go to a station that doesn't need it.
Now here is where things start to get un-intuitive if this is your first time doing this. The next combinator reads the value C from the train stop, which is the number of trains that are currently stopped at the station plus the number that are on the way. We have to subtract C from T to prevent excess trains from getting dispatched to this stop. We store the value T - C in a variable named after the requested fluid. This will eventually go onto the circuit network (probably transmitted by radar) to request trains carrying the desired liquid. In this example, we're requesting molten iron.
The final decider combinator is just a check to ensure that the requested number of trainloads of the desired fluid is a positive number. Under normal operation this won't go negative, but if it does for any reason (such as playing around with the circuits or train interrupts) you don't want a station transmitting a negative need, as this will quite literally cancel out positive need from other stations.
Finally, these are the signals read from and sent to the stop, just for clarity.
And that's it for the requester stations. Whew!
Fluid loading
What do fluid loading stations look like? Well if you have only one loading station for a given fluid, they're trivially easy. You read the fluid count from the circuit network and set the train limit to be that number. The most basic single-supplier stop looks like this:
The limit on the station is set like discussed:
What if you have multiple stops that provide that fluid? Well if the circuit network has the signal "2 molten iron" then every stop would set its limit to 2 trains. That will result in 2 trains being deployed to every loading station, which will obviously cause oversupply. So for multiple loading points, we use a round-robin system to assign train limits. Here is what that looks like for a molten copper station with two loading points.
The constant combinator provides the signal C which is set to the number of stops of this type. In this example, we have two loading stations.
The first combinator adds 1 (which is C - 1) to the value of the molten copper signal which is, you'll recall, the number of trains of molten copper being requested. In the screenshots that value is 0, since no copper is currently being requested.
Finally, we divide that number by C, the number of loading stops. If 1 train is being requested, then 1 + (C - 1) = C and so C / C is 1. This stop would then have a train limit of exactly 1.
For our second station, there is only one combinator.
This time we don't add anything and just divide by C = 2. Why? Think: If the molten copper equals 1, then the first station has limit 1 and the second station has limit 1 / 2 = 0. If molten copper equals 2, then the first station sets the limit to (2 + 1) / 2 = 3 / 2 = 1 (remember, this is integer division) and the second station sets its limit to 2 / 2 = 1. So the two stations each have limit 1, which is correct. Imagine you have molten copper equal to 3. Then station 1 has limit equal to (3 + 1) / 2 = 2, and station 2 has limit 3 / 2 = 1, which again is correct. You can see that this is indeed working in "round robin" fashion. As the request goes up, we increase the limit on each station one at a time.
If you're still following along, I'm impressed.
Fluid logistics train
Now let's take a look at the fluid logistics train itself. The "Temporary" station is not one that we set. It's caused by an interrupt. So this train has only one permanent stop on its schedule! That is the "fluid load" station (and remember that all of our loading stations have the same name, no matter if they provide molten copper or natural gas). All the other logic comes from the interrupts.
There are those confusing interrupts! Magically, we only need 3. The first is pretty easy to understand. That causes the train to go get fuel when it runs out. The use of "all" versus "any" locomotives fully fueled makes no difference since we're using 1 locomotive per train in this example. Note that we have the "allow interrupting other interrupts" checkbox ticked, and this is the only interrupt that we do that for. This means that it takes priority over other interrupts. After all, if the train is low on fuel, refueling has to be the #1 priority.
This one is also pretty easy to understand. If no loading destinations are open and the train has no cargo, it should go to the waiting station for a few seconds before checking again for an open loading station.
Here is where all the magic happens. Remember that we named all of our requester stops the same way: <item> unload. The reason is that this interrupt uses a wildcard. The condition says that if an item is detected in the train, the next thing it will do is look for a stop with the name <item> unload, go there, and wait until all <item>s have been unloaded. This is an interrupt, so the train checks for the presence of an item after finishing at the current stop.
Here is how a typical fluid logistics train will spend its day. You might build it at the refueling station. It will load up with fuel, then try to visit the only stop on its schedule: the fluid load stop. If none are available (meaning either the train limits are all 0 or they're all full up with trains) then the wait interrupt triggers and the train goes to the fluid wait stop. It hangs out until it gets its turn at the stop, then waits until a fluid loading station becomes available. The train goes to "fluid load" and is filled up with something - any fluid, potentially. Let's say it receives natural gas. Once filled, it looks at the interrupts to see where to go. It goes to "<natural gas> unload" until empty, then the whole thing starts over.
That's more than enough verbiage for now. If you have questions, corrections, or suggestions, please post a reply. I will try to patch this post if changes need to be made, hoping to keep it correct.
Changelog:
12/4/24 - fixed broken image links, added section titles, added picture of requester stop, minor text edits.
12/5/24 - added a note about "interrupting other interrupts".
Enough jabber, on to the implementation.
Notes: All trains are assumed top be 1-4 trains, meaning 1 locomotive and 4 cargo or fluid carriages.
The core idea is this: Do as little with circuits as possible and let the Factorio game engine do as much as possible. That's it.
To achieve that, we are going to let Factorio decide things like which train to dispatch and where it goes. We aren't going to think about calculating how many trains go to a stop or which train to send. First up, you need two kinds of trains for a basic setup, fluid trains and cargo trains. We are going to look at fluid trains, since cargo follows the same rules but with chests instead of storage tanks. The reason you need distinct types of trains is that fluid transportation requires different types of carriages and loading mechanisms - it's just easier to use 2 types of train.
Every fluid loading station has the same name: fluid load. Every "requester" station has a name of the form "<item symbol> unload". You need to use the item symbol in the name for the interrupts to work correctly. We'll get to that.
A short detour on how interrupts work: When a train is ready to depart its current stop, it will check for interrupts and set a temporary station for whichever it finds. That is the moment interrupts happen - not while the train is out and about or even during a stop, but only at the end of a stop.
Fluid unloading
Here is what your fluid unloading stop looks like:
We need 4 combinators in total and they are hooked up in a very particular way. The left-most combinator calculates the quantity of fluid needed to fill all the tanks, stored in the variable N for "need".
The next combinator calculates how many fuild trains are required to deliver the needed amount. That gets output to the variable T for "trains." Factorio, sensibly, uses C style division, so for example 5 divided by 3 is 1 since 5 = 3 * 1 + 2. Integer division returns the "quotient", or the number of times one number "goes into" the other. In the screenshot, our T variable is 0 since 11 / 12 = 0 * 12 + 11. The value of T is used to set the train limit on the stop. Why? If we don't set limits, then any train dispatched to a station of this type will just stop at the closest station of that type. We have to "close off" stations with 0 need so that trains intended for more distant stations don't go to a station that doesn't need it.
Now here is where things start to get un-intuitive if this is your first time doing this. The next combinator reads the value C from the train stop, which is the number of trains that are currently stopped at the station plus the number that are on the way. We have to subtract C from T to prevent excess trains from getting dispatched to this stop. We store the value T - C in a variable named after the requested fluid. This will eventually go onto the circuit network (probably transmitted by radar) to request trains carrying the desired liquid. In this example, we're requesting molten iron.
The final decider combinator is just a check to ensure that the requested number of trainloads of the desired fluid is a positive number. Under normal operation this won't go negative, but if it does for any reason (such as playing around with the circuits or train interrupts) you don't want a station transmitting a negative need, as this will quite literally cancel out positive need from other stations.
Finally, these are the signals read from and sent to the stop, just for clarity.
And that's it for the requester stations. Whew!
Fluid loading
What do fluid loading stations look like? Well if you have only one loading station for a given fluid, they're trivially easy. You read the fluid count from the circuit network and set the train limit to be that number. The most basic single-supplier stop looks like this:
The limit on the station is set like discussed:
What if you have multiple stops that provide that fluid? Well if the circuit network has the signal "2 molten iron" then every stop would set its limit to 2 trains. That will result in 2 trains being deployed to every loading station, which will obviously cause oversupply. So for multiple loading points, we use a round-robin system to assign train limits. Here is what that looks like for a molten copper station with two loading points.
The constant combinator provides the signal C which is set to the number of stops of this type. In this example, we have two loading stations.
The first combinator adds 1 (which is C - 1) to the value of the molten copper signal which is, you'll recall, the number of trains of molten copper being requested. In the screenshots that value is 0, since no copper is currently being requested.
Finally, we divide that number by C, the number of loading stops. If 1 train is being requested, then 1 + (C - 1) = C and so C / C is 1. This stop would then have a train limit of exactly 1.
For our second station, there is only one combinator.
This time we don't add anything and just divide by C = 2. Why? Think: If the molten copper equals 1, then the first station has limit 1 and the second station has limit 1 / 2 = 0. If molten copper equals 2, then the first station sets the limit to (2 + 1) / 2 = 3 / 2 = 1 (remember, this is integer division) and the second station sets its limit to 2 / 2 = 1. So the two stations each have limit 1, which is correct. Imagine you have molten copper equal to 3. Then station 1 has limit equal to (3 + 1) / 2 = 2, and station 2 has limit 3 / 2 = 1, which again is correct. You can see that this is indeed working in "round robin" fashion. As the request goes up, we increase the limit on each station one at a time.
If you're still following along, I'm impressed.
Fluid logistics train
Now let's take a look at the fluid logistics train itself. The "Temporary" station is not one that we set. It's caused by an interrupt. So this train has only one permanent stop on its schedule! That is the "fluid load" station (and remember that all of our loading stations have the same name, no matter if they provide molten copper or natural gas). All the other logic comes from the interrupts.
There are those confusing interrupts! Magically, we only need 3. The first is pretty easy to understand. That causes the train to go get fuel when it runs out. The use of "all" versus "any" locomotives fully fueled makes no difference since we're using 1 locomotive per train in this example. Note that we have the "allow interrupting other interrupts" checkbox ticked, and this is the only interrupt that we do that for. This means that it takes priority over other interrupts. After all, if the train is low on fuel, refueling has to be the #1 priority.
This one is also pretty easy to understand. If no loading destinations are open and the train has no cargo, it should go to the waiting station for a few seconds before checking again for an open loading station.
Here is where all the magic happens. Remember that we named all of our requester stops the same way: <item> unload. The reason is that this interrupt uses a wildcard. The condition says that if an item is detected in the train, the next thing it will do is look for a stop with the name <item> unload, go there, and wait until all <item>s have been unloaded. This is an interrupt, so the train checks for the presence of an item after finishing at the current stop.
Here is how a typical fluid logistics train will spend its day. You might build it at the refueling station. It will load up with fuel, then try to visit the only stop on its schedule: the fluid load stop. If none are available (meaning either the train limits are all 0 or they're all full up with trains) then the wait interrupt triggers and the train goes to the fluid wait stop. It hangs out until it gets its turn at the stop, then waits until a fluid loading station becomes available. The train goes to "fluid load" and is filled up with something - any fluid, potentially. Let's say it receives natural gas. Once filled, it looks at the interrupts to see where to go. It goes to "<natural gas> unload" until empty, then the whole thing starts over.
That's more than enough verbiage for now. If you have questions, corrections, or suggestions, please post a reply. I will try to patch this post if changes need to be made, hoping to keep it correct.
Changelog:
12/4/24 - fixed broken image links, added section titles, added picture of requester stop, minor text edits.
12/5/24 - added a note about "interrupting other interrupts".