From 8451f5a2816abe9d834fdf7aaf5d83cfc80e2952 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Sun, 14 Jun 2026 14:35:43 +0200 Subject: [PATCH] fix missed code paths for artificially reduced splitter throughput --- src/lib/sim/BeltSystem.cpp | 19 ++++--- src/test/BeltSystemTest.cpp | 105 +++++++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/src/lib/sim/BeltSystem.cpp b/src/lib/sim/BeltSystem.cpp index 66fd149..de8e167 100644 --- a/src/lib/sim/BeltSystem.cpp +++ b/src/lib/sim/BeltSystem.cpp @@ -720,11 +720,18 @@ void BeltSystem::routeSplitterItems() 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 (!st.frontA) { - st.frontA = BeltItemSlot{item, 0.0}; + st.frontA = BeltItemSlot{item, frontEntryProgress}; routed = true; } } @@ -732,7 +739,7 @@ void BeltSystem::routeSplitterItems() { if (!st.frontB) { - st.frontB = BeltItemSlot{item, 0.0}; + st.frontB = BeltItemSlot{item, frontEntryProgress}; routed = true; } } @@ -743,26 +750,26 @@ void BeltSystem::routeSplitterItems() if (preferA && !st.frontA) { - st.frontA = BeltItemSlot{item, 0.0}; + st.frontA = BeltItemSlot{item, frontEntryProgress}; st.nextOutputIsA = false; routed = true; } else if (!preferA && !st.frontB) { - st.frontB = BeltItemSlot{item, 0.0}; + st.frontB = BeltItemSlot{item, frontEntryProgress}; st.nextOutputIsA = true; routed = true; } else if (preferA && !st.frontB) { // Preferred (A) is full — fall back to B; nextOutputIsA stays. - st.frontB = BeltItemSlot{item, 0.75}; + st.frontB = BeltItemSlot{item, frontEntryProgress}; routed = true; } else if (!preferA && !st.frontA) { // Preferred (B) is full — fall back to A; nextOutputIsA stays. - st.frontA = BeltItemSlot{item, 0.75}; + st.frontA = BeltItemSlot{item, frontEntryProgress}; routed = true; } // else both fronts occupied — back stays. diff --git a/src/test/BeltSystemTest.cpp b/src/test/BeltSystemTest.cpp index 58c662c..b298e06 100644 --- a/src/test/BeltSystemTest.cpp +++ b/src/test/BeltSystemTest.cpp @@ -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). bs.tryPutItem(tileSpl, makeItem("blockA")); bs.tick(); // back: 0.25 - bs.tick(); // back: 0.5 -> frontA at 0.0 (preferred A), nextOutputIsA = false - bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontA: 0.25 -> 0.5 -> 0.75 -> 1.0 (stuck) + bs.tick(); // back: 0.5 -> frontA at 0.75 (preferred A), nextOutputIsA = false + 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.tick(); // back: 0.25 - bs.tick(); // back: 0.5 -> frontB at 0.0 (preferred B), nextOutputIsA = true - REQUIRE(southProgressOf("toB_pref") == Approx(0.0)); + bs.tick(); // back: 0.5 -> frontB at 0.75 (preferred B), nextOutputIsA = true + REQUIRE(southProgressOf("toB_pref") == Approx(0.75)); - // Let it traverse and hand off to the downstream belt, freeing frontB. - bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontB: 0.25 -> 0.5 -> 0.75 -> 1.0 -> tileB + // One tick reaches the edge and hands off to tileB; the rest just clear frontB. + bs.tick(); bs.tick(); // frontB: 0.75 -> 1.0 -> tileB, then empty // 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. @@ -613,6 +614,96 @@ TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 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(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 + { + std::optional 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(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 + { + std::optional 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) // ---------------------------------------------------------------------------