simulate items on splitters and fix bugs where buildings could not pull from splitters

This commit is contained in:
2026-04-23 21:54:59 +02:00
parent 78f746d352
commit ea30d2ab7b
3 changed files with 323 additions and 58 deletions

View File

@@ -66,6 +66,7 @@ void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
st.outputA = outputA; st.outputA = outputA;
st.outputB = outputB; st.outputB = outputB;
st.nextOutputIsA = true; st.nextOutputIsA = true;
st.backDir = Rotation::North; // irrelevant until back is set
m_splitters[key(tile)] = st; m_splitters[key(tile)] = st;
} }
@@ -104,17 +105,14 @@ bool BeltSystem::tryPutItem(QPoint tile, Item item)
std::optional<Item> BeltSystem::tryTakeItem(Port port) std::optional<Item> BeltSystem::tryTakeItem(Port port)
{ {
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(port.tile)); const std::map<std::pair<int, int>, BeltTile>::iterator beltIt = m_belts.find(key(port.tile));
if (it == m_belts.end()) if (beltIt != m_belts.end())
{
if (beltIt->second.direction != port.direction)
{ {
return std::nullopt; return std::nullopt;
} }
if (it->second.direction != port.direction) BeltTile& bt = beltIt->second;
{
return std::nullopt;
}
BeltTile& bt = it->second;
if (bt.front && bt.front->progress >= 1.0) if (bt.front && bt.front->progress >= 1.0)
{ {
const Item taken = bt.front->item; const Item taken = bt.front->item;
@@ -125,20 +123,39 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
return std::nullopt; return std::nullopt;
} }
std::optional<ItemType> BeltSystem::peekItem(Port port) const const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
m_splitters.find(key(port.tile));
if (splIt != m_splitters.end())
{ {
const std::map<std::pair<int, int>, BeltTile>::const_iterator it = SplitterTile& st = splIt->second;
m_belts.find(key(port.tile)); if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0)
if (it == m_belts.end())
{ {
return std::nullopt; const Item taken = st.frontA->item;
st.frontA = std::nullopt;
return taken;
} }
if (it->second.direction != port.direction) if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0)
{ {
const Item taken = st.frontB->item;
st.frontB = std::nullopt;
return taken;
}
}
return std::nullopt; return std::nullopt;
} }
const BeltTile& bt = it->second; std::optional<ItemType> BeltSystem::peekItem(Port port) const
{
const std::map<std::pair<int, int>, BeltTile>::const_iterator beltIt =
m_belts.find(key(port.tile));
if (beltIt != m_belts.end())
{
if (beltIt->second.direction != port.direction)
{
return std::nullopt;
}
const BeltTile& bt = beltIt->second;
if (bt.front && bt.front->progress >= 1.0) if (bt.front && bt.front->progress >= 1.0)
{ {
return bt.front->item.type; return bt.front->item.type;
@@ -146,6 +163,24 @@ std::optional<ItemType> BeltSystem::peekItem(Port port) const
return std::nullopt; return std::nullopt;
} }
const std::map<std::pair<int, int>, SplitterTile>::const_iterator splIt =
m_splitters.find(key(port.tile));
if (splIt != m_splitters.end())
{
const SplitterTile& st = splIt->second;
if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0)
{
return st.frontA->item.type;
}
if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0)
{
return st.frontB->item.type;
}
}
return std::nullopt;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Maintenance // Maintenance
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -164,7 +199,9 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile)); const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
if (sIt != m_splitters.end()) if (sIt != m_splitters.end())
{ {
sIt->second.heldItem = std::nullopt; sIt->second.back = std::nullopt;
sIt->second.frontA = std::nullopt;
sIt->second.frontB = std::nullopt;
} }
} }
} }
@@ -216,10 +253,44 @@ void BeltSystem::advanceProgress()
} }
} }
} }
for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
it != m_splitters.end(); ++it)
{
SplitterTile& st = it->second;
if (st.back)
{
st.back->progress += m_progressPerTick;
if (st.back->progress > 0.5)
{
st.back->progress = 0.5;
}
}
if (st.frontA)
{
st.frontA->progress += m_progressPerTick;
if (st.frontA->progress > 1.0)
{
st.frontA->progress = 1.0;
}
}
if (st.frontB)
{
st.frontB->progress += m_progressPerTick;
if (st.frontB->progress > 1.0)
{
st.frontB->progress = 1.0;
}
}
}
} }
void BeltSystem::moveItemsToNextTile() void BeltSystem::moveItemsToNextTile()
{ {
// Belt items advancing into the next tile.
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin(); for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
it != m_belts.end(); ++it) it != m_belts.end(); ++it)
{ {
@@ -246,18 +317,47 @@ void BeltSystem::moveItemsToNextTile()
} }
else if (nextSplitter != m_splitters.end()) else if (nextSplitter != m_splitters.end())
{ {
if (!nextSplitter->second.heldItem) if (!nextSplitter->second.back)
{ {
nextSplitter->second.heldItem = bt.front->item; nextSplitter->second.back = BeltItemSlot{bt.front->item, 0.0};
nextSplitter->second.backDir = bt.direction;
bt.front = bt.back; bt.front = bt.back;
bt.back = std::nullopt; bt.back = std::nullopt;
} }
// else: splitter busy — item stays blocked at progress 1.0. // else: splitter back occupied — item stays blocked at progress 1.0.
} }
// else: no tile registered (e.g. open space, or building input port). // else: no tile registered (e.g. open space, or building input port).
// Items leaving into unregistered tiles are not consumed here — the // Items leaving into unregistered tiles are not consumed here — the
// building pull step uses tryTakeItem for that. // building pull step uses tryTakeItem for that.
} }
// Splitter front slots advancing into downstream belt tiles.
for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
it != m_splitters.end(); ++it)
{
SplitterTile& st = it->second;
const QPoint here = QPoint(it->first.first, it->first.second);
if (st.frontA && st.frontA->progress >= 1.0)
{
const QPoint dest = adjacentTile(here, st.outputA);
if (tryPlaceOnBelt(dest, st.frontA->item))
{
st.frontA = std::nullopt;
}
// else: downstream belt full or absent — item stays at progress 1.0
// for building pickup via tryTakeItem.
}
if (st.frontB && st.frontB->progress >= 1.0)
{
const QPoint dest = adjacentTile(here, st.outputB);
if (tryPlaceOnBelt(dest, st.frontB->item))
{
st.frontB = std::nullopt;
}
}
}
} }
void BeltSystem::routeSplitterItems() void BeltSystem::routeSplitterItems()
@@ -266,12 +366,12 @@ void BeltSystem::routeSplitterItems()
it != m_splitters.end(); ++it) it != m_splitters.end(); ++it)
{ {
SplitterTile& st = it->second; SplitterTile& st = it->second;
if (!st.heldItem) if (!st.back || st.back->progress < 0.5)
{ {
continue; continue;
} }
const Item& item = *st.heldItem; const Item& item = st.back->item;
const bool matchesA = st.filterA.empty() || const bool matchesA = st.filterA.empty() ||
std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end(); std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end();
@@ -280,42 +380,52 @@ void BeltSystem::routeSplitterItems()
if (matchesA && !matchesB) if (matchesA && !matchesB)
{ {
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputA); if (!st.frontA)
if (tryPlaceOnBelt(dest, item))
{ {
st.heldItem = std::nullopt; st.frontA = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
} }
} }
else if (matchesB && !matchesA) else if (matchesB && !matchesA)
{ {
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputB); if (!st.frontB)
if (tryPlaceOnBelt(dest, item))
{ {
st.heldItem = std::nullopt; st.frontB = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
} }
} }
else if (matchesA && matchesB) else if (matchesA && matchesB)
{ {
// Alternation: try preferred output first, fall back to other. // Alternation: try preferred output first, fall back to other if preferred full.
const Rotation preferred = st.nextOutputIsA ? st.outputA : st.outputB; const bool preferA = st.nextOutputIsA;
const Rotation fallback = st.nextOutputIsA ? st.outputB : st.outputA;
const QPoint prefDest = adjacentTile(QPoint(it->first.first, it->first.second), preferred); if (preferA && !st.frontA)
const QPoint fbDest = adjacentTile(QPoint(it->first.first, it->first.second), fallback);
if (tryPlaceOnBelt(prefDest, item))
{ {
st.heldItem = std::nullopt; st.frontA = BeltItemSlot{item, 0.0};
st.nextOutputIsA = !st.nextOutputIsA; st.back = std::nullopt;
st.nextOutputIsA = false;
} }
else if (tryPlaceOnBelt(fbDest, item)) else if (!preferA && !st.frontB)
{ {
st.heldItem = std::nullopt; st.frontB = BeltItemSlot{item, 0.0};
// nextOutputIsA stays: preferred was blocked, so we still owe it next. st.back = std::nullopt;
st.nextOutputIsA = true;
} }
// else both blocked — item stays. else if (preferA && !st.frontB)
{
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
st.frontB = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
} }
// else (!matchesA && !matchesB): stall — item stays in splitter. else if (!preferA && !st.frontA)
{
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
st.frontA = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
}
// else both fronts occupied — back stays.
}
// else (!matchesA && !matchesB): stall — back stays.
} }
} }
@@ -381,4 +491,39 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
visit(vi); visit(vi);
} }
} }
for (const std::pair<const std::pair<int, int>, SplitterTile>& entry : m_splitters)
{
const QPoint tile(entry.first.first, entry.first.second);
if (!viewportTiles.contains(tile))
{
continue;
}
const SplitterTile& st = entry.second;
if (st.back)
{
VisualItem vi;
vi.type = st.back->item.type;
vi.worldPos = slotWorldPos(tile, st.backDir, st.back->progress);
visit(vi);
}
if (st.frontA)
{
VisualItem vi;
vi.type = st.frontA->item.type;
vi.worldPos = slotWorldPos(tile, st.outputA, st.frontA->progress);
visit(vi);
}
if (st.frontB)
{
VisualItem vi;
vi.type = st.frontB->item.type;
vi.worldPos = slotWorldPos(tile, st.outputB, st.frontB->progress);
visit(vi);
}
}
} }

View File

@@ -111,7 +111,10 @@ private:
std::vector<ItemType> filterA; // empty = accept all std::vector<ItemType> filterA; // empty = accept all
std::vector<ItemType> filterB; std::vector<ItemType> filterB;
bool nextOutputIsA; // alternation state bool nextOutputIsA; // alternation state
std::optional<Item> heldItem; // item buffered waiting to exit std::optional<BeltItemSlot> back; // progress [0, 0.5]; entering from input belt
Rotation backDir; // direction of the feeding belt (for animation)
std::optional<BeltItemSlot> frontA; // progress [0, 1]; routed to outputA
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
}; };
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz

View File

@@ -296,8 +296,9 @@ TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]")
TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]") TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]")
{ {
// Layout: tileIn -> splitter -> tileA (West output) // Layout: tileIn -> splitter -> tileA (North output)
// -> tileB (East output) // -> tileB (South output)
// Pipeline per item: tileIn(1) -> back(2) -> front(3) -> output belt(4)
BeltSystem bs(kFastBeltSpeed); BeltSystem bs(kFastBeltSpeed);
const QPoint tileIn(0, 0); const QPoint tileIn(0, 0);
@@ -311,12 +312,14 @@ TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]
bs.placeBelt(tileB, Rotation::South); bs.placeBelt(tileB, Rotation::South);
bs.tryPutItem(tileIn, makeItem("item1")); bs.tryPutItem(tileIn, makeItem("item1"));
bs.tick(); // item moves: tileIn -> splitter held bs.tick(); // item1: tileIn -> splitter back (progress 0)
bs.tryPutItem(tileIn, makeItem("item2")); bs.tryPutItem(tileIn, makeItem("item2"));
bs.tick(); // item1 routes to outputA (North=tileA); item2 moves to splitter 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 routes to outputB (South=tileB) bs.tick(); // item2 back -> 0.5 -> frontB; item1 at tileA output edge
bs.tick(); // item2 frontB -> 1.0 -> tileB
bs.tick(); // item2 at tileB output edge
const bool inA = bs.tryTakeItem(Port{tileA, Rotation::North}).has_value(); const bool inA = bs.tryTakeItem(Port{tileA, Rotation::North}).has_value();
const bool inB = bs.tryTakeItem(Port{tileB, Rotation::South}).has_value(); const bool inB = bs.tryTakeItem(Port{tileB, Rotation::South}).has_value();
@@ -345,12 +348,126 @@ TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt
bs.placeBelt(tileB, Rotation::South); bs.placeBelt(tileB, Rotation::South);
// Filter: outputA = iron_ore only; outputB = accept all. // Filter: outputA = iron_ore only; outputB = accept all.
// iron_ore matches both filters → alternation; preferred = outputA (nextOutputIsA=true).
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {}); bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {});
bs.tryPutItem(tileIn, makeItem("iron_ore")); bs.tryPutItem(tileIn, makeItem("iron_ore"));
bs.tick(); // tileIn -> splitter held bs.tick(); // tileIn -> splitter back
bs.tick(); // routed to outputA (filter match) bs.tick(); // back -> frontA (filter + alternation → preferred outputA)
bs.tick(); // frontA -> tileA
bs.tick(); // item at tileA output edge
REQUIRE(bs.tryTakeItem(Port{tileA, Rotation::North}).has_value()); REQUIRE(bs.tryTakeItem(Port{tileA, Rotation::North}).has_value());
REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::South}).has_value()); REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::South}).has_value());
} }
// ---------------------------------------------------------------------------
// Splitter — direct building input (no output belts)
// ---------------------------------------------------------------------------
TEST_CASE("BeltSystem: splitter back slot is capped at 0.5 and waits before routing", "[belt]")
{
BeltSystem bs(kFastBeltSpeed);
const QPoint tileIn(0, 0);
const QPoint tileSpl(1, 0);
bs.placeBelt(tileIn, Rotation::East);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
bs.tryPutItem(tileIn, makeItem("iron_ore"));
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.
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
bs.tick(); // back advances to 0.5, routes to frontA at progress 0
bs.tick(); // frontA advances to 1.0, available for building pickup
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
}
TEST_CASE("BeltSystem: splitter delivers item directly to building input via tryTakeItem", "[belt]")
{
// Bug 1: splitter could not insert into a building input with no belt in between.
BeltSystem bs(kFastBeltSpeed);
const QPoint tileIn(0, 0);
const QPoint tileSpl(1, 0);
bs.placeBelt(tileIn, Rotation::East);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
// No output belts — both outputs lead directly to building inputs.
bs.tryPutItem(tileIn, makeItem("iron_ore"));
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
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
const std::optional<Item> taken = bs.tryTakeItem(Port{tileSpl, Rotation::North});
REQUIRE(taken.has_value());
REQUIRE(taken->type.id == "iron_ore");
}
TEST_CASE("BeltSystem: splitter accepts new items after building pulls from front slot", "[belt]")
{
// Bug 2: when outputs had no belts, splitter never cleared its held state
// so no new items could enter.
BeltSystem bs(kFastBeltSpeed);
const QPoint tileIn(0, 0);
const QPoint tileSpl(1, 0);
bs.placeBelt(tileIn, Rotation::East);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
bs.tryPutItem(tileIn, makeItem("item1"));
bs.tick();
bs.tick();
bs.tick(); // item1 now in frontA at 1.0
// Building pulls item1 — clears frontA; nextOutputIsA toggled to false.
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
// Feed item2; preferred is now South.
bs.tryPutItem(tileIn, makeItem("item2"));
bs.tick();
bs.tick();
bs.tick(); // item2 now in frontB at 1.0
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
}
TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (building inputs)", "[belt]")
{
BeltSystem bs(kFastBeltSpeed);
const QPoint tileIn(0, 0);
const QPoint tileSpl(1, 0);
bs.placeBelt(tileIn, Rotation::East);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
// item1 → frontA (preferred, nextOutputIsA=true)
bs.tryPutItem(tileIn, makeItem("item1"));
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.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.tick();
bs.tick();
bs.tick();
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
}