Summary
Tested on StoragePlus v1.2.1.0 with Core Keeper v1.2.1.0.
When two or more ItemDroppers share the same filter in a storage network, currently the dropper that comes first in the ECS query order wins every tick and the others stay idle. This is mostly a missing feature rather than a bug — but while implementing fair-share I also ran into a real race-condition bug in InventoryChangeBuffer usage that's worth fixing on its own.
I have working patches and am happy to send a PR if any of this looks worth merging. Filing as one issue because the four pieces layer naturally.
Repro
Setup: 3 ItemDroppers connected to a network containing a chest with copper ore. All 3 droppers set to filter copper ore.
Current behaviour: one dropper persistently drops items, the others stay idle (until the network is reconfigured).
Proposed fair-share design (3 pieces)
1. Per-dropper counter with peer set
Track each dropper's drop count and the set of peers in its fair-share group. Before TryQueueDropRequest, gate the dropper on counter parity with its peers. Bump the counter on a successful drop.
2. Scope groups per network (not global)
If you group droppers globally by (filterObject, filterVariation), two unrelated bases with same-filter droppers compete in one shared counter — whichever base drops more starves the other. Including StorageNetworkSnapshot.NetworkHash in the group key keeps each network independent.
3. With tied counts, gate on minOther, not maxOther
This one took a while to find. With 3 droppers all at count=10, every one of them passes a myCount <= maxOther gate (since maxOther = 10 and they're tied). They all proceed into TryQueueDropRequest, but only the first to reserve a (source_inventory, slot) actually drops (see bug below); the losers don't bump their counters, stay tied, and the same thing happens next tick — permanent idle for the trailing dropper.
Gating on minOther instead means tied state allows only the most-behind dropper through, breaking the cycle.
Race-condition bug (independent of fair-share)
StorageIntakeNetworkSystem (SimulationSystemGroup) and ItemDropperRequestSystem (InventorySystemGroup, later) can both queue an InventoryChangeBuffer entry against the same (source_inventory, source_slot) within a single tick. When InventoryUpdateSystem plays the buffer back, only the first entry succeeds — the rest silently no-op.
The dropper's "I queued a drop" log still fires correctly, but the items on the ground reflect only whichever side won the race. So even after fair-share is in place, in-game testing can look like fair-share is broken, which sent me down a long debug path.
Fix: a shared per-tick reservation set on StorageNetworkWorldCache. Intake clears it at the start of OnUpdate. Both Intake's DrainConnectorOutputs and ItemDropper's TryQueueDropRequest call reservedSourceSlots.Add(slotKey) after queueing a change, and Contains(slotKey) → continue before iterating the slot. slotKey = ((long)inventory.Index << 32) | (uint)slotIndex.
Implementation summary
The patches touch:
ItemDropperRequestSystem.cs — counter dicts + PassesFairShareGate + NotifyDropSucceeded + ComputeGroupKey (covers pieces 1–3, plus the dropper side of the reservation fix)
StorageNetworkWorldCache.cs — adds public HashSet<long> ReservedSourceSlotsThisTick + ClearTickReservations()
StorageIntakeNetworkSystem.cs — ClearTickReservations() at the start of OnUpdate, Add(slotKey) after each queued transfer (covers the Intake side of the reservation fix)
DOTS source-generator caveat: Pug's runtime uses generated __OnUpdate_* methods in Generated/*__System_*.g.cs, so the actual diff has to apply both to source and to the matching .g.cs files. About ~200 lines of meaningful change, doubled for that reason — mentioning so it's not a surprise if you look at a PR.
If a PR sounds useful just say the word. If you'd rather take this as a reference and rework it yourself, that works too.
Summary
Tested on StoragePlus v1.2.1.0 with Core Keeper v1.2.1.0.
When two or more
ItemDroppers share the same filter in a storage network, currently the dropper that comes first in the ECS query order wins every tick and the others stay idle. This is mostly a missing feature rather than a bug — but while implementing fair-share I also ran into a real race-condition bug inInventoryChangeBufferusage that's worth fixing on its own.I have working patches and am happy to send a PR if any of this looks worth merging. Filing as one issue because the four pieces layer naturally.
Repro
Setup: 3 ItemDroppers connected to a network containing a chest with copper ore. All 3 droppers set to filter copper ore.
Current behaviour: one dropper persistently drops items, the others stay idle (until the network is reconfigured).
Proposed fair-share design (3 pieces)
1. Per-dropper counter with peer set
Track each dropper's drop count and the set of peers in its fair-share group. Before
TryQueueDropRequest, gate the dropper on counter parity with its peers. Bump the counter on a successful drop.2. Scope groups per network (not global)
If you group droppers globally by
(filterObject, filterVariation), two unrelated bases with same-filter droppers compete in one shared counter — whichever base drops more starves the other. IncludingStorageNetworkSnapshot.NetworkHashin the group key keeps each network independent.3. With tied counts, gate on
minOther, notmaxOtherThis one took a while to find. With 3 droppers all at
count=10, every one of them passes amyCount <= maxOthergate (sincemaxOther = 10and they're tied). They all proceed intoTryQueueDropRequest, but only the first to reserve a (source_inventory, slot) actually drops (see bug below); the losers don't bump their counters, stay tied, and the same thing happens next tick — permanent idle for the trailing dropper.Gating on
minOtherinstead means tied state allows only the most-behind dropper through, breaking the cycle.Race-condition bug (independent of fair-share)
StorageIntakeNetworkSystem(SimulationSystemGroup) andItemDropperRequestSystem(InventorySystemGroup, later) can both queue anInventoryChangeBufferentry against the same(source_inventory, source_slot)within a single tick. WhenInventoryUpdateSystemplays the buffer back, only the first entry succeeds — the rest silently no-op.The dropper's "I queued a drop" log still fires correctly, but the items on the ground reflect only whichever side won the race. So even after fair-share is in place, in-game testing can look like fair-share is broken, which sent me down a long debug path.
Fix: a shared per-tick reservation set on
StorageNetworkWorldCache. Intake clears it at the start ofOnUpdate. Both Intake'sDrainConnectorOutputsand ItemDropper'sTryQueueDropRequestcallreservedSourceSlots.Add(slotKey)after queueing a change, andContains(slotKey) → continuebefore iterating the slot.slotKey = ((long)inventory.Index << 32) | (uint)slotIndex.Implementation summary
The patches touch:
ItemDropperRequestSystem.cs— counter dicts +PassesFairShareGate+NotifyDropSucceeded+ComputeGroupKey(covers pieces 1–3, plus the dropper side of the reservation fix)StorageNetworkWorldCache.cs— addspublic HashSet<long> ReservedSourceSlotsThisTick+ClearTickReservations()StorageIntakeNetworkSystem.cs—ClearTickReservations()at the start ofOnUpdate,Add(slotKey)after each queued transfer (covers the Intake side of the reservation fix)DOTS source-generator caveat: Pug's runtime uses generated
__OnUpdate_*methods inGenerated/*__System_*.g.cs, so the actual diff has to apply both to source and to the matching.g.csfiles. About ~200 lines of meaningful change, doubled for that reason — mentioning so it's not a surprise if you look at a PR.If a PR sounds useful just say the word. If you'd rather take this as a reference and rework it yourself, that works too.