diff --git a/src/lib/sim/BeltSystem.cpp b/src/lib/sim/BeltSystem.cpp index 9f3b6b0..66fd149 100644 --- a/src/lib/sim/BeltSystem.cpp +++ b/src/lib/sim/BeltSystem.cpp @@ -756,13 +756,13 @@ void BeltSystem::routeSplitterItems() else if (preferA && !st.frontB) { // Preferred (A) is full — fall back to B; nextOutputIsA stays. - st.frontB = BeltItemSlot{item, 0.0}; + st.frontB = BeltItemSlot{item, 0.75}; routed = true; } else if (!preferA && !st.frontA) { // Preferred (B) is full — fall back to A; nextOutputIsA stays. - st.frontA = BeltItemSlot{item, 0.0}; + st.frontA = BeltItemSlot{item, 0.75}; routed = true; } // else both fronts occupied — back stays. @@ -963,3 +963,4 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles, } } + diff --git a/src/test/BeltSystemTest.cpp b/src/test/BeltSystemTest.cpp index 0b425b3..58c662c 100644 --- a/src/test/BeltSystemTest.cpp +++ b/src/test/BeltSystemTest.cpp @@ -1,5 +1,7 @@ #include "catch.hpp" +#include +#include #include #include @@ -553,6 +555,64 @@ TEST_CASE("BeltSystem: splitter falls back to other output when preferred is blo REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value()); } +TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75", "[belt]") +{ + // When the preferred output is blocked, the diverted item is dropped onto the + // open output near its edge (progress 0.75) instead of at progress 0.0. This + // closes the large gap that would otherwise appear between items leaving the + // open side of a half-blocked splitter. + // + // Progress/tick = 0.25 so the 0.0-vs-0.75 entry position is observable: a + // normally-routed item starts at 0.0, a fallback item starts at 0.75. + const double quarterSpeed = 0.25 * static_cast(kTickRateHz); + BeltSystem bs(quarterSpeed); + + const QPoint tileSpl(1, 0); + const QPoint tileB(1, 1); // South output belt; North output has no belt (blocked). + + bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); + bs.placeBelt(tileB, Rotation::South); + + // Reads a named item's progress along the South output via the rendering contract. + // slotWorldPos maps a South-bound slot on tileSpl (y = 0) to worldPos.y == progress. + // Matching by id avoids the blocked North item, which also renders at worldPos.y 0. + auto southProgressOf = [&bs](const std::string& id) -> std::optional + { + std::optional progress; + bs.forEachVisualItem(QRect(-5, -5, 20, 20), [&](VisualItem vi) + { + if (vi.type.id == id) + { + progress = vi.worldPos.y(); + } + }); + return progress; + }; + + // Permanently block output A: route one item to frontA where it sticks at 1.0 + // (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) + + // Item routed to B as the *preferred* output enters at progress 0.0. + 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)); + + // 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 + + // 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. + bs.tryPutItem(tileSpl, makeItem("toB_fallback")); + bs.tick(); // back: 0.25 + bs.tick(); // back: 0.5 -> fallback routes to frontB at 0.75 + REQUIRE(southProgressOf("toB_fallback") == Approx(0.75)); +} + // --------------------------------------------------------------------------- // Splitter — direct building input (no output belts) // ---------------------------------------------------------------------------