fix missed code paths for artificially reduced splitter throughput

This commit is contained in:
2026-06-14 14:35:43 +02:00
parent 0a1b58442c
commit 8451f5a281
2 changed files with 111 additions and 13 deletions

View File

@@ -720,11 +720,18 @@ void BeltSystem::routeSplitterItems()
bool routed = false; bool routed = false;
// A front slot holds only one item, so an item entering at progress 0.0
// would have to traverse the whole tile before the next could enter,
// throttling that output below belt speed and leaving large gaps. Entering
// near the output edge lets the slot clear roughly every quarter tile, so
// the output stays packed (fixes the half-blocked / single-output gap bug).
constexpr double frontEntryProgress = 0.75;
if (matchesA && !matchesB) if (matchesA && !matchesB)
{ {
if (!st.frontA) if (!st.frontA)
{ {
st.frontA = BeltItemSlot{item, 0.0}; st.frontA = BeltItemSlot{item, frontEntryProgress};
routed = true; routed = true;
} }
} }
@@ -732,7 +739,7 @@ void BeltSystem::routeSplitterItems()
{ {
if (!st.frontB) if (!st.frontB)
{ {
st.frontB = BeltItemSlot{item, 0.0}; st.frontB = BeltItemSlot{item, frontEntryProgress};
routed = true; routed = true;
} }
} }
@@ -743,26 +750,26 @@ void BeltSystem::routeSplitterItems()
if (preferA && !st.frontA) if (preferA && !st.frontA)
{ {
st.frontA = BeltItemSlot{item, 0.0}; st.frontA = BeltItemSlot{item, frontEntryProgress};
st.nextOutputIsA = false; st.nextOutputIsA = false;
routed = true; routed = true;
} }
else if (!preferA && !st.frontB) else if (!preferA && !st.frontB)
{ {
st.frontB = BeltItemSlot{item, 0.0}; st.frontB = BeltItemSlot{item, frontEntryProgress};
st.nextOutputIsA = true; st.nextOutputIsA = true;
routed = true; routed = true;
} }
else if (preferA && !st.frontB) else if (preferA && !st.frontB)
{ {
// Preferred (A) is full — fall back to B; nextOutputIsA stays. // Preferred (A) is full — fall back to B; nextOutputIsA stays.
st.frontB = BeltItemSlot{item, 0.75}; st.frontB = BeltItemSlot{item, frontEntryProgress};
routed = true; routed = true;
} }
else if (!preferA && !st.frontA) else if (!preferA && !st.frontA)
{ {
// Preferred (B) is full — fall back to A; nextOutputIsA stays. // Preferred (B) is full — fall back to A; nextOutputIsA stays.
st.frontA = BeltItemSlot{item, 0.75}; st.frontA = BeltItemSlot{item, frontEntryProgress};
routed = true; routed = true;
} }
// else both fronts occupied — back stays. // else both fronts occupied — back stays.

View File

@@ -593,17 +593,18 @@ TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75
// (North has no downstream tile, so it can never move out). // (North has no downstream tile, so it can never move out).
bs.tryPutItem(tileSpl, makeItem("blockA")); bs.tryPutItem(tileSpl, makeItem("blockA"));
bs.tick(); // back: 0.25 bs.tick(); // back: 0.25
bs.tick(); // back: 0.5 -> frontA at 0.0 (preferred A), nextOutputIsA = false bs.tick(); // back: 0.5 -> frontA at 0.75 (preferred A), nextOutputIsA = false
bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontA: 0.25 -> 0.5 -> 0.75 -> 1.0 (stuck) bs.tick(); bs.tick(); // frontA: 0.75 -> 1.0 (stuck, no North downstream)
// Item routed to B as the *preferred* output enters at progress 0.0. // Cycle one item through B as the *preferred* output (also enters at 0.75) to
// flip nextOutputIsA back to true and free frontB for the fallback case below.
bs.tryPutItem(tileSpl, makeItem("toB_pref")); bs.tryPutItem(tileSpl, makeItem("toB_pref"));
bs.tick(); // back: 0.25 bs.tick(); // back: 0.25
bs.tick(); // back: 0.5 -> frontB at 0.0 (preferred B), nextOutputIsA = true bs.tick(); // back: 0.5 -> frontB at 0.75 (preferred B), nextOutputIsA = true
REQUIRE(southProgressOf("toB_pref") == Approx(0.0)); REQUIRE(southProgressOf("toB_pref") == Approx(0.75));
// Let it traverse and hand off to the downstream belt, freeing frontB. // One tick reaches the edge and hands off to tileB; the rest just clear frontB.
bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontB: 0.25 -> 0.5 -> 0.75 -> 1.0 -> tileB bs.tick(); bs.tick(); // frontB: 0.75 -> 1.0 -> tileB, then empty
// Next item prefers A again (nextOutputIsA == true), but A is still blocked, // Next item prefers A again (nextOutputIsA == true), but A is still blocked,
// so it falls back to B — and must enter near the edge at progress 0.75. // so it falls back to B — and must enter near the edge at progress 0.75.
@@ -613,6 +614,96 @@ TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75
REQUIRE(southProgressOf("toB_fallback") == Approx(0.75)); REQUIRE(southProgressOf("toB_fallback") == Approx(0.75));
} }
TEST_CASE("BeltSystem: splitter with an exclusive filter enters its only output at progress 0.75", "[belt]")
{
// An item that matches only one filter has a single eligible output. Like the
// blocked-fallback case, it must enter near the edge (progress 0.75) so the
// one-item-wide front does not throttle that output and open large gaps.
const double quarterSpeed = 0.25 * static_cast<double>(kTickRateHz);
BeltSystem bs(quarterSpeed);
const QPoint tileSpl(1, 0);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"copper_ore"}});
// Inverts slotWorldPos to recover a named item's progress along the given output.
auto progressOf = [&bs, tileSpl](const std::string& id, Rotation dir) -> std::optional<double>
{
std::optional<double> progress;
bs.forEachVisualItem(QRect(-5, -5, 20, 20), [&](VisualItem vi)
{
if (vi.type.id != id)
{
return;
}
switch (dir)
{
case Rotation::North: progress = (tileSpl.y() + 1.0) - vi.worldPos.y(); break;
case Rotation::South: progress = vi.worldPos.y() - tileSpl.y(); break;
case Rotation::East: progress = vi.worldPos.x() - tileSpl.x(); break;
case Rotation::West: progress = (tileSpl.x() + 1.0) - vi.worldPos.x(); break;
}
});
return progress;
};
// iron_ore matches filterA only -> sole eligible output A.
bs.tryPutItem(tileSpl, makeItem("iron_ore"));
bs.tick(); // back: 0.25
bs.tick(); // back: 0.5 -> routes to frontA at 0.75
REQUIRE(progressOf("iron_ore", Rotation::North) == Approx(0.75));
// copper_ore matches filterB only -> sole eligible output B.
bs.tryPutItem(tileSpl, makeItem("copper_ore"));
bs.tick(); // back: 0.25
bs.tick(); // back: 0.5 -> routes to frontB at 0.75
REQUIRE(progressOf("copper_ore", Rotation::South) == Approx(0.75));
}
TEST_CASE("BeltSystem: splitter alternation enters the preferred output at progress 0.75", "[belt]")
{
// With both outputs eligible and free, the preferred output uses the same
// near-edge entry as the diverted paths, so an evenly-split splitter keeps
// each side packed instead of throttling it to one in-flight item per tile.
const double quarterSpeed = 0.25 * static_cast<double>(kTickRateHz);
BeltSystem bs(quarterSpeed);
const QPoint tileSpl(1, 0);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); // no filters: both match
auto progressOf = [&bs, tileSpl](const std::string& id, Rotation dir) -> std::optional<double>
{
std::optional<double> progress;
bs.forEachVisualItem(QRect(-5, -5, 20, 20), [&](VisualItem vi)
{
if (vi.type.id != id)
{
return;
}
switch (dir)
{
case Rotation::North: progress = (tileSpl.y() + 1.0) - vi.worldPos.y(); break;
case Rotation::South: progress = vi.worldPos.y() - tileSpl.y(); break;
case Rotation::East: progress = vi.worldPos.x() - tileSpl.x(); break;
case Rotation::West: progress = (tileSpl.x() + 1.0) - vi.worldPos.x(); break;
}
});
return progress;
};
// First item: preferred A (nextOutputIsA starts true) -> frontA at 0.75.
bs.tryPutItem(tileSpl, makeItem("first"));
bs.tick(); // back: 0.25
bs.tick(); // back: 0.5 -> routes to preferred frontA at 0.75, nextOutputIsA = false
REQUIRE(progressOf("first", Rotation::North) == Approx(0.75));
// Second item: preference flipped, B is free -> frontB at 0.75.
bs.tryPutItem(tileSpl, makeItem("second"));
bs.tick(); // back: 0.25 (first sticks at North 1.0, no downstream)
bs.tick(); // back: 0.5 -> routes to preferred frontB at 0.75
REQUIRE(progressOf("second", Rotation::South) == Approx(0.75));
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Splitter — direct building input (no output belts) // Splitter — direct building input (no output belts)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------