From af8a2224c07a4614c8a550bd1375d2ecc473da1d Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Tue, 14 Jul 2026 20:15:39 +0200 Subject: [PATCH] fix issue where items were accepted by a belt from opposite travel direction --- docs/requirements.md | 9 +- src/lib/sim/BeltSystem.cpp | 69 +++++++++++ src/lib/sim/BeltSystem.h | 12 +- src/test/BeltSystemTest.cpp | 233 +++++++++++++++++++++++++----------- src/test/BuildingTest.cpp | 6 +- 5 files changed, 249 insertions(+), 80 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index d24c1de..0ad60a8 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -127,14 +127,14 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING. - REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration. - REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. It has an output buffer whose holding capacity is defined by the `output_buffer_capacity` field of the `salvage_bay` entry in `buildings.toml` (rather than by a production cycle, since the Salvage Bay has no recipe). A ship at the bay hands over one unit of scrap per tick while the buffer has free space; a full buffer blocks further drop-off until space frees up (consistent with the buffer-full semantics of REQ-MAT-OUTPUT-BUFFER). Held scrap is pushed onto connected output belts. -- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED). -- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules: +- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED). A belt accepts items only through a non-output edge (REQ-MAT-ACCEPT-DIR). +- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Incoming items are accepted only through the splitter's non-output edges (REQ-MAT-ACCEPT-DIR). Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules: - An item matching only one output's filter is routed to that output. - An item matching both outputs' filters is distributed by strict alternation between those outputs. - An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved. - If neither output has a filter, items are distributed by strict alternation. - In all alternation cases, if one output is blocked the item goes to the other output until it unblocks. -- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with R/Shift+R. Items arriving from an adjacent belt tile whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile. +- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with R/Shift+R. Items arriving from an adjacent belt tile on a non-output edge (i.e. not the mouth edge in the entry's facing direction — see REQ-MAT-ACCEPT-DIR) whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile. - REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with R/Shift+R. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance. - REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed or demolished. - A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry. @@ -148,7 +148,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels. - REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. -- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port tile regardless of that belt's direction. +- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port tile. The belt's direction is otherwise unconstrained (it may flow away from the building or perpendicular to it), except that a belt oriented with its own output edge facing back into the building refuses the item and the deposit fails (REQ-MAT-ACCEPT-DIR). +- REQ-MAT-ACCEPT-DIR: A transport tile (belt, splitter, tunnel entry, or tunnel exit) accepts an incoming item only through a non-output edge; an item that would enter through one of the tile's output edges is refused. For a belt or a tunnel entry/exit the sole output edge is the one in its facing direction; for a splitter either of its two output directions is an output edge. This applies both to items pushed from an adjacent transport tile and to items deposited by a building's output port (REQ-MAT-OUTPUT-PORT). - REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or schematic, all items in all input buffers are cleared. - REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or schematic, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated). - REQ-MAT-OUTPUT-BUFFER-REPROCESSING: Exception to REQ-MAT-OUTPUT-BUFFER — the Reprocessing Plant's output buffer holds at most one cycle's output. This prevents exploits where the player stalls the output belt to force the plant to reroll. diff --git a/src/lib/sim/BeltSystem.cpp b/src/lib/sim/BeltSystem.cpp index 34438bb..9d21a61 100644 --- a/src/lib/sim/BeltSystem.cpp +++ b/src/lib/sim/BeltSystem.cpp @@ -27,6 +27,56 @@ QPoint BeltSystem::adjacentTile(QPoint tile, Rotation dir) return tile; } +Rotation BeltSystem::oppositeRotation(Rotation dir) +{ + switch (dir) + { + case Rotation::North: return Rotation::South; + case Rotation::East: return Rotation::West; + case Rotation::South: return Rotation::North; + case Rotation::West: return Rotation::East; + } + return dir; +} + +bool BeltSystem::entersThroughOutputEdge(QPoint tile, Rotation travelDir) const +{ + // An item travelling in travelDir crosses into the tile through the edge + // opposite that direction. If that entry edge is one of the tile's output + // edges, the tile must refuse the item (REQ-MAT-ACCEPT-DIR). + const Rotation entryEdge = oppositeRotation(travelDir); + + const std::map, BeltTile>::const_iterator beltIt = + m_belts.find(key(tile)); + if (beltIt != m_belts.end()) + { + return entryEdge == beltIt->second.direction; + } + + const std::map, SplitterTile>::const_iterator splIt = + m_splitters.find(key(tile)); + if (splIt != m_splitters.end()) + { + return entryEdge == splIt->second.outputA || entryEdge == splIt->second.outputB; + } + + const std::map, TunnelEntryTile>::const_iterator teIt = + m_tunnelEntries.find(key(tile)); + if (teIt != m_tunnelEntries.end()) + { + return entryEdge == teIt->second.direction; + } + + const std::map, TunnelExitTile>::const_iterator txIt = + m_tunnelExits.find(key(tile)); + if (txIt != m_tunnelExits.end()) + { + return entryEdge == txIt->second.direction; + } + + return false; +} + QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress) { // Map progress [0, 1] along the belt direction to a fractional tile-unit position. @@ -202,6 +252,12 @@ void BeltSystem::reevaluateTunnelPairing() bool BeltSystem::tryPutItem(QPoint tile, Item item, Rotation fromDir) { + // Refuse items that would enter through the tile's output edge (REQ-MAT-ACCEPT-DIR). + if (entersThroughOutputEdge(tile, fromDir)) + { + return false; + } + const std::map, BeltTile>::iterator bIt = m_belts.find(key(tile)); if (bIt != m_belts.end()) { @@ -579,6 +635,13 @@ void BeltSystem::moveItemsToNextTile() const QPoint here = QPoint(it->first.first, it->first.second); const QPoint next = adjacentTile(here, bt.direction); + // Refuse to hand off into a downstream tile's output edge (REQ-MAT-ACCEPT-DIR); + // the item stays blocked at progress 1.0. + if (entersThroughOutputEdge(next, bt.direction)) + { + continue; + } + const std::map, BeltTile>::iterator nextBelt = m_belts.find(key(next)); const std::map, SplitterTile>::iterator nextSplitter = m_splitters.find(key(next)); @@ -804,6 +867,12 @@ bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item) bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir) { + // Refuse items that would enter through the tile's output edge (REQ-MAT-ACCEPT-DIR). + if (entersThroughOutputEdge(dest, fromDir)) + { + return false; + } + if (tryPlaceOnBelt(dest, item)) { return true; diff --git a/src/lib/sim/BeltSystem.h b/src/lib/sim/BeltSystem.h index 322a19c..6454e21 100644 --- a/src/lib/sim/BeltSystem.h +++ b/src/lib/sim/BeltSystem.h @@ -74,8 +74,10 @@ public: // port.direction = direction items flow on that tile // // tryPutItem: place item onto tile. - // Returns false if the tile is not a belt/splitter, or tile full. - // fromDir: travel direction of the item (used for splitter animation). + // Returns false if the tile is not a belt/splitter/tunnel entry, tile full, + // or the item would enter through the tile's output edge (REQ-MAT-ACCEPT-DIR). + // fromDir: travel direction of the item (used for splitter animation and for + // the output-edge check). bool tryPutItem(QPoint tile, Item item, Rotation fromDir = Rotation::West); // tryTakeItem: remove and return the leading item from port.tile. @@ -117,6 +119,12 @@ private: static std::pair key(QPoint tile); static QPoint adjacentTile(QPoint tile, Rotation dir); + static Rotation oppositeRotation(Rotation dir); + + // True if an item travelling in travelDir would enter the transport tile at + // `tile` through one of that tile's output edges (and must therefore be + // refused). Returns false if no transport tile occupies `tile`. + bool entersThroughOutputEdge(QPoint tile, Rotation travelDir) const; // Returns the world-space centre of a slot given tile origin and progress. static QPointF slotWorldPos(QPoint tile, Rotation dir, double progress); diff --git a/src/test/BeltSystemTest.cpp b/src/test/BeltSystemTest.cpp index b298e06..92f6185 100644 --- a/src/test/BeltSystemTest.cpp +++ b/src/test/BeltSystemTest.cpp @@ -44,14 +44,14 @@ TEST_CASE("BeltSystem: tryPutItem succeeds on registered belt", "[belt]") const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - REQUIRE(bs.tryPutItem(tile, makeItem("iron_ore"))); + REQUIRE(bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East)); } TEST_CASE("BeltSystem: tryPutItem fails on unregistered tile", "[belt]") { BeltSystem bs(kFastBeltSpeed); - REQUIRE_FALSE(bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"))); + REQUIRE_FALSE(bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East)); } TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]") @@ -61,7 +61,7 @@ TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]") bs.placeBelt(tile, Rotation::East); bs.removeTile(tile); - REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("iron_ore"))); + REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East)); } // --------------------------------------------------------------------------- @@ -74,10 +74,10 @@ TEST_CASE("BeltSystem: four items fit in one tile", "[belt]") const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - REQUIRE(bs.tryPutItem(tile, makeItem("a"))); - REQUIRE(bs.tryPutItem(tile, makeItem("b"))); - REQUIRE(bs.tryPutItem(tile, makeItem("c"))); - REQUIRE(bs.tryPutItem(tile, makeItem("d"))); + REQUIRE(bs.tryPutItem(tile, makeItem("a"), Rotation::East)); + REQUIRE(bs.tryPutItem(tile, makeItem("b"), Rotation::East)); + REQUIRE(bs.tryPutItem(tile, makeItem("c"), Rotation::East)); + REQUIRE(bs.tryPutItem(tile, makeItem("d"), Rotation::East)); } TEST_CASE("BeltSystem: fifth tryPutItem on full tile returns false", "[belt]") @@ -86,12 +86,12 @@ TEST_CASE("BeltSystem: fifth tryPutItem on full tile returns false", "[belt]") const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("a")); - bs.tryPutItem(tile, makeItem("b")); - bs.tryPutItem(tile, makeItem("c")); - bs.tryPutItem(tile, makeItem("d")); + bs.tryPutItem(tile, makeItem("a"), Rotation::East); + bs.tryPutItem(tile, makeItem("b"), Rotation::East); + bs.tryPutItem(tile, makeItem("c"), Rotation::East); + bs.tryPutItem(tile, makeItem("d"), Rotation::East); - REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("e"))); + REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("e"), Rotation::East)); } // --------------------------------------------------------------------------- @@ -103,7 +103,7 @@ TEST_CASE("BeltSystem: tryTakeItem returns placed item after reaching output edg BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East); bs.tick(); // advance to output edge const std::optional taken = bs.tryTakeItem(eastPort(tile)); @@ -117,7 +117,7 @@ TEST_CASE("BeltSystem: tryTakeItem requires item to reach output edge before yie BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East); // Item placed but not yet at output edge — must not be available. REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value()); @@ -133,8 +133,8 @@ TEST_CASE("BeltSystem: tryTakeItem with two items returns both after each reache BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("first")); - bs.tryPutItem(tile, makeItem("second")); + bs.tryPutItem(tile, makeItem("first"), Rotation::East); + bs.tryPutItem(tile, makeItem("second"), Rotation::East); // Front item reaches output edge after one tick. bs.tick(); @@ -169,7 +169,7 @@ TEST_CASE("BeltSystem: item transfers from tile A to tile B and becomes availabl bs.placeBelt(tileA, Rotation::East); bs.placeBelt(tileB, Rotation::East); - bs.tryPutItem(tileA, makeItem("iron_ore")); + bs.tryPutItem(tileA, makeItem("iron_ore"), Rotation::East); bs.tick(); // item reaches output edge of A, moves to B at progress 0 bs.tick(); // item reaches output edge of B @@ -185,7 +185,7 @@ TEST_CASE("BeltSystem: item stays at progress 1.0 when next tile is absent", "[b const QPoint tileA(0, 0); bs.placeBelt(tileA, Rotation::East); - bs.tryPutItem(tileA, makeItem("iron_ore")); + bs.tryPutItem(tileA, makeItem("iron_ore"), Rotation::East); bs.tick(); // Item should still be on tileA (no registered tile to the east). @@ -202,7 +202,7 @@ TEST_CASE("BeltSystem: item traverses 3-tile chain in 3 ticks (one per tile)", " bs.placeBelt(tileB, Rotation::East); bs.placeBelt(tileC, Rotation::East); - bs.tryPutItem(tileA, makeItem("iron_ore")); + bs.tryPutItem(tileA, makeItem("iron_ore"), Rotation::East); bs.tick(); // A output edge → moves to B at progress 0 bs.tick(); // B output edge → moves to C at progress 0 bs.tick(); // C output edge → available for pickup @@ -221,13 +221,13 @@ TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]") bs.placeBelt(tileB, Rotation::East); // Fill tileB to capacity. - bs.tryPutItem(tileB, makeItem("b1")); - bs.tryPutItem(tileB, makeItem("b2")); - bs.tryPutItem(tileB, makeItem("b3")); - bs.tryPutItem(tileB, makeItem("b4")); + bs.tryPutItem(tileB, makeItem("b1"), Rotation::East); + bs.tryPutItem(tileB, makeItem("b2"), Rotation::East); + bs.tryPutItem(tileB, makeItem("b3"), Rotation::East); + bs.tryPutItem(tileB, makeItem("b4"), Rotation::East); // Place item in tileA — should be blocked. - bs.tryPutItem(tileA, makeItem("a1")); + bs.tryPutItem(tileA, makeItem("a1"), Rotation::East); bs.tick(); // Item in tileA must still be there. @@ -246,13 +246,13 @@ TEST_CASE("BeltSystem: belt second slot is capped at progress 0.75", "[belt]") bs.placeBelt(tile, Rotation::East); // Advance front item to the output edge; it stays there (no next tile). - bs.tryPutItem(tile, makeItem("front_item")); + bs.tryPutItem(tile, makeItem("front_item"), Rotation::East); bs.tick(); // slot[0]: 0.4 bs.tick(); // slot[0]: 0.8 bs.tick(); // slot[0]: 1.0 (capped, stuck) // Place second item; slot[0] is at 1.0. - bs.tryPutItem(tile, makeItem("back_item")); + bs.tryPutItem(tile, makeItem("back_item"), Rotation::East); bs.tick(); // slot[1]: 0.4 bs.tick(); // slot[1] would reach 0.8 — capped at 0.75 @@ -272,8 +272,8 @@ TEST_CASE("BeltSystem: clearTiles removes all items from specified tiles", "[bel BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("iron_ore")); - bs.tryPutItem(tile, makeItem("copper_ore")); + bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East); + bs.tryPutItem(tile, makeItem("copper_ore"), Rotation::East); bs.clearTiles({tile}); @@ -289,7 +289,7 @@ TEST_CASE("BeltSystem: forEachVisualItem visits items inside viewport", "[belt]" BeltSystem bs(kFastBeltSpeed); const QPoint tile(5, 5); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East); int count = 0; bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; }); @@ -302,7 +302,7 @@ TEST_CASE("BeltSystem: forEachVisualItem skips items outside viewport", "[belt]" BeltSystem bs(kFastBeltSpeed); const QPoint tile(50, 50); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East); int count = 0; bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; }); @@ -315,7 +315,7 @@ TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]") BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(tile, makeItem("copper_ingot")); + bs.tryPutItem(tile, makeItem("copper_ingot"), Rotation::East); std::vector seen; bs.forEachVisualItem(QRect(-1, -1, 10, 10), [&seen](VisualItem vi) @@ -348,10 +348,10 @@ TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt] bs.placeBelt(tileA, Rotation::North); bs.placeBelt(tileB, Rotation::South); - bs.tryPutItem(tileIn, makeItem("item1")); + bs.tryPutItem(tileIn, makeItem("item1"), Rotation::East); bs.tick(); // item1: tileIn -> splitter back (progress 0) - bs.tryPutItem(tileIn, makeItem("item2")); + bs.tryPutItem(tileIn, makeItem("item2"), Rotation::East); bs.tick(); // item1 back -> 0.5 -> frontA; item2 advances but back is occupied bs.tick(); // item1 frontA -> 1.0 -> tileA; item2 enters splitter back bs.tick(); // item2 back -> 0.5 -> frontB; item1 at tileA output edge @@ -388,7 +388,7 @@ TEST_CASE("BeltSystem: splitter routes to preferred output when item matches bot bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {}); - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); // tileIn -> splitter back bs.tick(); // back -> frontA (both match, alternation, preferred A) bs.tick(); // frontA -> tileA @@ -411,7 +411,7 @@ TEST_CASE("BeltSystem: splitter routes item to output A when only filter A match bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"copper_ore"}}); - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); // tileIn -> splitter back bs.tick(); // back -> frontA (exclusive match to A) bs.tick(); // frontA reaches 1.0; no downstream belt, waits for building pickup @@ -433,7 +433,7 @@ TEST_CASE("BeltSystem: splitter routes item to output B when only filter B match bs.setSplitterFilters(tileSpl, {ItemType{"copper_ore"}}, {ItemType{"iron_ore"}}); - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); @@ -456,17 +456,17 @@ TEST_CASE("BeltSystem: splitter alternates A then B when item matches both expli bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"iron_ore"}}); // Item 1 → preferred A (nextOutputIsA=true initially). - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value()); // Item 2 → preferred B (nextOutputIsA toggled to false). - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value()); // Item 3 → preferred A again (nextOutputIsA toggled back to true). - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value()); REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value()); @@ -486,7 +486,7 @@ TEST_CASE("BeltSystem: splitter routes unmatched item to the unfiltered output", bs.setSplitterFilters(tileSpl, {ItemType{"copper_ore"}}, {}); - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value()); @@ -506,7 +506,7 @@ TEST_CASE("BeltSystem: splitter stalls when item matches neither filter", "[belt bs.setSplitterFilters(tileSpl, {ItemType{"copper_ore"}}, {ItemType{"iron_ingot"}}); - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); // tileIn -> splitter back bs.tick(); // back reaches 0.5; routing fires but stalls (no filter match) bs.tick(); // back stays at 0.5; stall persists @@ -530,17 +530,17 @@ TEST_CASE("BeltSystem: splitter falls back to other output when preferred is blo bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"iron_ore"}}); // Item 1 → preferred A (nextOutputIsA=true → false after routing). - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); // frontA = item1 at 1.0 // Item 2 → preferred B (nextOutputIsA=false → true after routing). Take item2 to free frontB. - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); // frontB = item2 at 1.0 REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value()); // frontA still holds item1; nextOutputIsA=true (prefer A). // Item 3: both match, preferred A is occupied → fallback to B without toggling nextOutputIsA. - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); // frontB = item3 at 1.0 REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value()); // item1 still in A @@ -549,7 +549,7 @@ TEST_CASE("BeltSystem: splitter falls back to other output when preferred is blo // nextOutputIsA was not toggled by the fallback: next item should still prefer A. REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value()); // free frontA REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value()); // free frontB - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value()); // item4 → A (preferA still true) REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value()); @@ -591,14 +591,14 @@ TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75 // 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.tryPutItem(tileSpl, makeItem("blockA"), Rotation::East); bs.tick(); // back: 0.25 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) // 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"), Rotation::East); bs.tick(); // back: 0.25 bs.tick(); // back: 0.5 -> frontB at 0.75 (preferred B), nextOutputIsA = true REQUIRE(southProgressOf("toB_pref") == Approx(0.75)); @@ -608,7 +608,7 @@ TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75 // 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.tryPutItem(tileSpl, makeItem("toB_fallback"), Rotation::East); bs.tick(); // back: 0.25 bs.tick(); // back: 0.5 -> fallback routes to frontB at 0.75 REQUIRE(southProgressOf("toB_fallback") == Approx(0.75)); @@ -648,13 +648,13 @@ TEST_CASE("BeltSystem: splitter with an exclusive filter enters its only output }; // iron_ore matches filterA only -> sole eligible output A. - bs.tryPutItem(tileSpl, makeItem("iron_ore")); + bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::East); 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.tryPutItem(tileSpl, makeItem("copper_ore"), Rotation::East); 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)); @@ -692,13 +692,13 @@ TEST_CASE("BeltSystem: splitter alternation enters the preferred output at progr }; // First item: preferred A (nextOutputIsA starts true) -> frontA at 0.75. - bs.tryPutItem(tileSpl, makeItem("first")); + bs.tryPutItem(tileSpl, makeItem("first"), Rotation::East); 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.tryPutItem(tileSpl, makeItem("second"), Rotation::East); 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)); @@ -718,7 +718,7 @@ TEST_CASE("BeltSystem: splitter back slot is capped at 0.5 and waits before rout bs.placeBelt(tileIn, Rotation::East); bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); // item enters splitter back at progress 0; routing not yet triggered // Back has not yet reached 0.5 — front slots empty, nothing available. @@ -743,7 +743,7 @@ TEST_CASE("BeltSystem: splitter delivers item directly to building input via try bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); // No output belts — both outputs lead directly to building inputs. - bs.tryPutItem(tileIn, makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore"), Rotation::East); bs.tick(); // tileIn -> splitter back bs.tick(); // back -> frontA at progress 0 bs.tick(); // frontA reaches 1.0; no downstream belt, item waits for building pickup @@ -766,7 +766,7 @@ TEST_CASE("BeltSystem: splitter accepts new items after building pulls from fron bs.placeBelt(tileIn, Rotation::East); bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); - bs.tryPutItem(tileIn, makeItem("item1")); + bs.tryPutItem(tileIn, makeItem("item1"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); // item1 now in frontA at 1.0 @@ -775,7 +775,7 @@ TEST_CASE("BeltSystem: splitter accepts new items after building pulls from fron REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value()); // Feed item2; preferred is now South. - bs.tryPutItem(tileIn, makeItem("item2")); + bs.tryPutItem(tileIn, makeItem("item2"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); // item2 now in frontB at 1.0 @@ -812,21 +812,21 @@ TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (bui bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); // item1 → frontA (preferred, nextOutputIsA=true) - bs.tryPutItem(tileIn, makeItem("item1")); + bs.tryPutItem(tileIn, makeItem("item1"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value()); // item2 → frontB (preferred, nextOutputIsA now false) - bs.tryPutItem(tileIn, makeItem("item2")); + bs.tryPutItem(tileIn, makeItem("item2"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value()); // item3 → frontA again (nextOutputIsA toggled back to true) - bs.tryPutItem(tileIn, makeItem("item3")); + bs.tryPutItem(tileIn, makeItem("item3"), Rotation::East); bs.tick(); bs.tick(); bs.tick(); @@ -847,7 +847,7 @@ TEST_CASE("BeltSystem: tunnel pairing — basic pair within max distance", "[bel bs.placeTunnelEntry(entry, Rotation::East, 10); bs.placeTunnelExit(exit, Rotation::East); - bs.tryPutItem(entry, makeItem("iron_ore")); + bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::East); // With kFastBeltSpeed, items cross one tile per tick. // entry tile: 1 tick to reach front progress 1.0 @@ -873,7 +873,7 @@ TEST_CASE("BeltSystem: tunnel pairing — wrong direction prevents pair", "[belt bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10); bs.placeTunnelExit(QPoint(3, 0), Rotation::North); - bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore")); + bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East); for (int i = 0; i < 20; ++i) { @@ -891,7 +891,7 @@ TEST_CASE("BeltSystem: tunnel pairing — beyond max distance prevents pair", "[ bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 2); bs.placeTunnelExit(QPoint(3, 0), Rotation::East); - bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore")); + bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East); for (int i = 0; i < 20; ++i) { @@ -912,7 +912,7 @@ TEST_CASE("BeltSystem: tunnel pairing — same-dir entry between blocks pairing" bs.placeTunnelExit(QPoint(4, 0), Rotation::East); // Put item on Entry2 — should reach exit. - bs.tryPutItem(QPoint(2, 0), makeItem("copper_ore")); + bs.tryPutItem(QPoint(2, 0), makeItem("copper_ore"), Rotation::East); for (int i = 0; i < 20; ++i) { bs.tick(); @@ -921,7 +921,7 @@ TEST_CASE("BeltSystem: tunnel pairing — same-dir entry between blocks pairing" bs.tryTakeItem(Port{QPoint(4, 0), Rotation::East}); // Put item on Entry1 — should NOT reach exit (Entry1 is unpaired). - bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore")); + bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East); for (int i = 0; i < 20; ++i) { bs.tick(); @@ -939,7 +939,7 @@ TEST_CASE("BeltSystem: tunnel pairing — cross-dir entry between is ignored", " bs.placeTunnelEntry(QPoint(2, 0), Rotation::North, 10); bs.placeTunnelExit(QPoint(4, 0), Rotation::East); - bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore")); + bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East); for (int i = 0; i < 20; ++i) { bs.tick(); @@ -958,7 +958,7 @@ TEST_CASE("BeltSystem: unpaired entry blocks items at front", "[belt]") bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10); // No exit placed — entry is unpaired. - bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore")); + bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East); for (int i = 0; i < 10; ++i) { bs.tick(); @@ -984,7 +984,7 @@ TEST_CASE("BeltSystem: demolish entry discards transit items", "[belt]") bs.placeTunnelEntry(entry, Rotation::East, 10); bs.placeTunnelExit(exit, Rotation::East); - bs.tryPutItem(entry, makeItem("iron_ore")); + bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::East); // Advance just enough for item to enter transit but not reach exit. bs.tick(); // item enters entry front @@ -1010,7 +1010,7 @@ TEST_CASE("BeltSystem: clearTiles discards tunnel transit items", "[belt]") bs.placeTunnelEntry(entry, Rotation::East, 10); bs.placeTunnelExit(exit, Rotation::East); - bs.tryPutItem(entry, makeItem("iron_ore")); + bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::East); bs.tick(); bs.tick(); @@ -1037,7 +1037,7 @@ TEST_CASE("BeltSystem: belt to entry to transit to exit to belt full chain", "[b bs.placeTunnelExit(exit, Rotation::East); bs.placeBelt(beltOut, Rotation::East); - bs.tryPutItem(beltIn, makeItem("iron_ore")); + bs.tryPutItem(beltIn, makeItem("iron_ore"), Rotation::East); for (int i = 0; i < 30; ++i) { @@ -1061,11 +1061,11 @@ TEST_CASE("BeltSystem: multiple items transit tunnel in order", "[belt]") bs.placeTunnelEntry(entry, Rotation::East, 10); bs.placeTunnelExit(exit, Rotation::East); - bs.tryPutItem(entry, makeItem("item1")); + bs.tryPutItem(entry, makeItem("item1"), Rotation::East); bs.tick(); bs.tick(); // item1 enters transit - bs.tryPutItem(entry, makeItem("item2")); + bs.tryPutItem(entry, makeItem("item2"), Rotation::East); for (int i = 0; i < 30; ++i) { @@ -1086,3 +1086,94 @@ TEST_CASE("BeltSystem: multiple items transit tunnel in order", "[belt]") REQUIRE(taken2.has_value()); REQUIRE(taken2->type.id == "item2"); } + +// --------------------------------------------------------------------------- +// Output-edge rejection (REQ-MAT-ACCEPT-DIR) +// --------------------------------------------------------------------------- + +TEST_CASE("BeltSystem: belt refuses an item arriving through its output edge", "[belt]") +{ + // Belt A flows East into belt B, but B flows West — so the hand-off would + // enter B through its own (West) output edge and must be refused. Without the + // guard the item would ping-pong between the two belts forever. + BeltSystem bs(kFastBeltSpeed); + const QPoint tileA(0, 0); + const QPoint tileB(1, 0); + bs.placeBelt(tileA, Rotation::East); + bs.placeBelt(tileB, Rotation::West); + + REQUIRE(bs.tryPutItem(tileA, makeItem("iron_ore"), Rotation::East)); + for (int i = 0; i < 5; ++i) + { + bs.tick(); + } + + // B never accepts the item through its output edge... + REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::West}).has_value()); + // ...and it stays blocked at A's output edge. + REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value()); +} + +TEST_CASE("BeltSystem: tryPutItem refuses a deposit onto a belt facing the source", "[belt]") +{ + // A belt whose output edge faces back toward the depositing building must + // refuse the item; feeding through a non-output edge still works. + BeltSystem bs(kFastBeltSpeed); + const QPoint tile(0, 0); + bs.placeBelt(tile, Rotation::West); + + // Item travelling East enters through the West (output) edge -> refused. + REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::East)); + // Item travelling West enters through the East (back) edge -> accepted. + REQUIRE(bs.tryPutItem(tile, makeItem("iron_ore"), Rotation::West)); +} + +TEST_CASE("BeltSystem: splitter refuses an item arriving through an output edge", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + const QPoint tileSpl(1, 0); + bs.placeSplitter(tileSpl, Rotation::North, Rotation::South); + + // Entering through the North output edge (item travelling South) -> refused. + REQUIRE_FALSE(bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::South)); + // Entering through the South output edge (item travelling North) -> refused. + REQUIRE_FALSE(bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::North)); + // Entering through a non-output (West) edge (item travelling East) -> accepted. + REQUIRE(bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::East)); +} + +TEST_CASE("BeltSystem: tunnel entry refuses an item arriving through its mouth edge", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + const QPoint entry(0, 0); + bs.placeTunnelEntry(entry, Rotation::East, 10); + + // Item travelling West enters through the East mouth (output) edge -> refused. + REQUIRE_FALSE(bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::West)); + // Item travelling East enters through the back (West) edge -> accepted. + REQUIRE(bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::East)); +} + +TEST_CASE("BeltSystem: tunnel exit refuses an item pushed into its output edge", "[belt]") +{ + // A splitter's East front sits next to a tunnel exit whose output faces West + // (back toward the splitter). The front cannot hand off into the exit's + // output edge, so the item stays on the splitter front. + BeltSystem bs(kFastBeltSpeed); + const QPoint tileSpl(1, 0); + const QPoint exitTile(2, 0); + bs.placeSplitter(tileSpl, Rotation::East, Rotation::South); + bs.placeTunnelExit(exitTile, Rotation::West); + + // Feed through the West edge (item travelling East); first item routes to the + // East front (nextOutputIsA starts true). + REQUIRE(bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::East)); + for (int i = 0; i < 5; ++i) + { + bs.tick(); + } + + // The item is stuck on the splitter's East front; the exit never received it. + REQUIRE(bs.peekItem(Port{tileSpl, Rotation::East}).has_value()); + REQUIRE_FALSE(bs.peekItem(Port{exitTile, Rotation::West}).has_value()); +} diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 7b2c5bc..37c37d1 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -214,13 +214,13 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0); // Belt is queued — not yet in BeltSystem. - REQUIRE_FALSE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"))); + REQUIRE_FALSE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); // Complete construction (1 s). Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); - REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"))); + REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); REQUIRE(bs.allBuildings().size() == 1); REQUIRE(bs.allBuildings()[0].type == BuildingType::Belt); REQUIRE(bs.allBuildings()[0].anchor == QPoint(5, 5)); @@ -810,7 +810,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta belts.placeBelt(QPoint(-1, 0), Rotation::East); for (int i = 0; i < 5; ++i) { - belts.tryPutItem(QPoint(-1, 0), makeItem("scrap")); + belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); belts.tick(); bs.tickBeltPull(); }