This is a continuation of the first post found here.
Writing the first part has been a very rewarding experience. I’m re-learning how valuable teaching is for both the writer and reader.
Revisiting my old circuits to explain how they function has already revealed many areas for improvement. I made some of those improvements while publishing part 1 and I’ve got a few more to walk through in this post.
Adding a dedicated trash train was long overdue. Previously the seed train doubled as my trash train. Over-filling the seed module manifest would leave little to no room to return items to base.
During base expansion I create and destroy many of these logistic stations. Waiting for stations to deconstruct to move the perimeter wall or clean up my rail network cost me hours of wait time & round trips.
I came up with a partial fix which I included in the last blueprint book:
This would replace the seed train stop with dedicated trash trains. However, this worked best when I was destroying the outpost, in others it required manual fixups. There is no substitute for building this feature directly into the outpost:
I’m using a new trick here too: buffer chests to replace the requester and steel chests. These have a major advantage over the prior setup as they allow the player logistics, construction robots and regular logistics (assemblers, smelters) to pull from the trash chests while a train is en-route.
I frequently found that when refactoring outposts I would flush all my beacons, modules and assemblers through the trash circuit. When using using requester and steel chests I would be unable to use those items again until a resupply train arrives (the trash items would be loaded then unloaded immediately, still a PITA).
Using buffer chests in set-requests
mode is the best of both worlds. It
behaves like a requester chest, it will not pull from other buffer chests & it
makes itself available to the logistics network.
The trash inserters do not need to overlap with the module slot inserter alignments so I found there was room for another 4 unloaders (from 4 to 8).
I found a few ways to shrink the size of the indexer circuit by using a negative
mask rather than the *1.0M
, >1.0M
, -1.0M
trick in the previous post
removing 4 combinators.
Blueprint.txt
Install the Text Plates mod if needed.
There were some concessions made as well: the circuit will no longer pause when the train is fully loaded. This was most annoying when standing over it, building it. Most of the time I’m elsewhere.
Let’s explode the circuit and do a walk-through:
There are two changes. The indexer circuit will now output everything except the
cursor [*each] != [I]
. I then [*each] * -1.0M
to create a negative mask
that is joined with the WANT
constant combinator.
The MISSING
combinator takes three input signals:
{rail: 100, chest: 50, inserter: 10}
{rail: -1M, inserter: -1M}
when chest
is selected.{rail: -80, chest: -10}
Which sum to: {rail: -1M, chest: 40, inserter: -1M}
.
The MISSING
combinator filters negative values with [*each] > 0 ->
[*each]
. Now we have an isolated item signal of exactly what is missing
{chest: 40}
which is perfect to calculate the filter inserter hand size and
filter item (HAND
, ITEM
).
Index management is a pain. Setting new requests remotely requires a lot of fiddling to correctly upgrade the manifests and indexes for each loader.
The dream is plonking down an upgraded, self-contained module at the loader and unloader with the circuit making all adjustments needed at both ends.
There’s (at least) two missing pieces:
Last week I thought this was impossible – and perhaps it should be. Here is a compact, fast, exact, auto-indexing logistic loader circuit that will also reset the train loader cache, index & index builder on manifest changes.
blueprint.txt
Top-left is the request manifest (constant combinator).
Bottom-left is the exact cargo wagon contents.
Right is the index state, built to match the items in the request signal.
The steel chest is used as the persistent storage for the index.
The brainwave for this came after reading a Reddit comment on the first post. I can use a filter inserter to select and isolate a single item from a mixed item signal.
Naturally this will only work for item signals and you’ll need those items within the current logistic network while building the index.
Previously I thought this would only be possible by using this Lua script to generate a massive constant combinator array programmed with all in-game items. This would not be robust to non-vanilla items (without re-running the script in each context), it has a huge footprint (at least 12 combinators) and the iteration times over ~250 items to use 10 of them was very slow. The manual index was a huge improvement at the time.
The procedure is:
(1..18)
with [clock] % 19
.[*each] ->
C: 1
.[C]ount > 1
):
Let simulate it with a bit of Rust:
fn main() {
let mut db: std::collections::BTreeMap<char, i32> = ['A', 'B', 'C', 'D', 'E', 'F']
.iter()
.map(|&item| (item, 1))
.collect();
let mut clock: i32 = 1234;
loop {
for (k, v) in &db {
print!("{}{} ", k, v);
}
// convert clock signal into index position
let index = clock % 19;
// find items at this index position
let items: Vec<char> = db
.iter()
.filter_map(|(k, v)| if *v == index { Some(*k) } else { None })
.collect();
let count = items.len();
print!("- index: {}", index);
if count > 0 {
print!(", items: {}", items.iter().collect::<String>());
}
if count > 1 {
let first = items[0];
// insert one of the multiple items to move it into the next slot (higher value).
db.entry(first).and_modify(|e| *e += 1);
println!(", insert: {}", first);
} else {
// resume clock
clock += 1;
println!(", resume");
}
let mut values: Vec<&i32> = db.values().collect();
values.sort();
values.dedup();
if values.len() == db.len() && count == 0 {
println!("done!");
break;
}
}
}
A1 B1 C1 D1 E1 F1 - index: 18, resume
A1 B1 C1 D1 E1 F1 - index: 0, resume
A1 B1 C1 D1 E1 F1 - index: 1, items: ABCDEF, insert: A
A2 B1 C1 D1 E1 F1 - index: 1, items: BCDEF, insert: B
A2 B2 C1 D1 E1 F1 - index: 1, items: CDEF, insert: C
A2 B2 C2 D1 E1 F1 - index: 1, items: DEF, insert: D
A2 B2 C2 D2 E1 F1 - index: 1, items: EF, insert: E
A2 B2 C2 D2 E2 F1 - index: 1, items: F, resume
A2 B2 C2 D2 E2 F1 - index: 2, items: ABCDE, insert: A
A3 B2 C2 D2 E2 F1 - index: 2, items: BCDE, insert: B
A3 B3 C2 D2 E2 F1 - index: 2, items: CDE, insert: C
A3 B3 C3 D2 E2 F1 - index: 2, items: DE, insert: D
A3 B3 C3 D3 E2 F1 - index: 2, items: E, resume
A3 B3 C3 D3 E2 F1 - index: 3, items: ABCD, insert: A
A4 B3 C3 D3 E2 F1 - index: 3, items: BCD, insert: B
A4 B4 C3 D3 E2 F1 - index: 3, items: CD, insert: C
A4 B4 C4 D3 E2 F1 - index: 3, items: D, resume
A4 B4 C4 D3 E2 F1 - index: 4, items: ABC, insert: A
A5 B4 C4 D3 E2 F1 - index: 4, items: BC, insert: B
A5 B5 C4 D3 E2 F1 - index: 4, items: C, resume
A5 B5 C4 D3 E2 F1 - index: 5, items: AB, insert: A
A6 B5 C4 D3 E2 F1 - index: 5, items: B, resume
A6 B5 C4 D3 E2 F1 - index: 6, items: A, resume
A6 B5 C4 D3 E2 F1 - index: 7, resume
done!
We’ve converted our request signal A1 B1 C1 D1 E1 F1
into a unique index A6
B5 C4 D3 E2 F1
, woo!
Lets match this to the circuit:
The CLOCK SOURCE
, PAUSE?
, CLOCK
& TO INDEX
combinators are familiar
elements from prior circuits.
INDEX
is the persistent memory where the index is built and INSERTER
is how
we shift items up the index slots one by one (hand size: 1).
IF BUILD
checks we are in the index building mode, the other mode FLUSH
is
triggered when changing the constant combinator. When triggered, the FLUSH
mode will flush all caches/chests (train loading cache, index chest, requester
chest). Covered more later.
INDEX ITEMS
will select all items at the given index position with [*each] ==
[I] -> [*each]: 1
.
ITEM COUNT
will emit C: 1
for each item found at the current index position.
If C>1
we pause the clock (PAUSE TO FIX
) while we fix this index position.
IF MULTI ITEM
will send a signal to the filter inserter and requester chest if
C>1
.
Now, with what I’ve explained so far there is a big off-by-one error. The item
at index position 1 will never be inserted into the chest due to the base
request signal rail: 1, chest: 1, inserter: 1, ..
, it needs a way to
invalidate the index=1 position. I did this by re-using the clock source dot:
1
to occupy that slot and push every other item up a slot. The dot
is not an
item and can’t be inserted.
This is the second circuit mode which will flush all state after a manifest change and then allow an index rebuild to start from a known good state (zero).
The FLUSH
trigger is a small 3 combinator detection circuit CHANGE
DETECTION
. Both arithmetic combinators do [*each] ^ 0 -> [*each]
to normalize
the request signals. The output of the first is fed into the second then both
fed into the SET ON CHANGE
decider with the [*each] != 2 -> S: 1
condition.
In the steady state, each value in the SET ON CHANGE
input signal will be 2
(one from each arithmetic combinator). On changes, there will be a 1-tick memory
delay in the circuit in which one item will read a value of 1 triggering S: 1
and the SR-LATCH
to start the flush cycle.
The SR-LATCH
in the set state S > 0
will enable the 8 inserters that flush
all chests into the active provider chests. The RESET WHEN FLUSHED
condition
is true when all active provider chests are empty.
There are two transistors 1
& 2
which will cut off signals to the requester
chests when the FLUSH
mode is active.
The indexer circuit in the middle remains the same as the prior circuits.
And finally upgraded my logistic stations to share the same trash stop with the trash trains:
Included in the blueprint book below.
Would love you hear your feedback, corrections, contributions & fixes!
Email: mason.larobina@gmail.com or raise an issue on the GitHub repo.
For updates, star the repo or follow:
https://github.com/mason-larobina/factorio/commits/master.atom