Skip to content

Feature request: fair-share for multiple ItemDroppers with the same filter (and a small race-condition bug found along the way) #2

Description

@uuuu790

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.csClearTickReservations() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions