#include "BeltSystem.h" #include #include "Tick.h" #include "tracing.h" // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- std::pair BeltSystem::key(QPoint tile) { return {tile.x(), tile.y()}; } QPoint BeltSystem::adjacentTile(QPoint tile, Rotation dir) { switch (dir) { case Rotation::North: return {tile.x(), tile.y() - 1}; case Rotation::East: return {tile.x() + 1, tile.y() }; case Rotation::South: return {tile.x(), tile.y() + 1}; case Rotation::West: return {tile.x() - 1, tile.y() }; } return tile; } QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress) { // Map progress [0, 1] along the belt direction to a fractional tile-unit position. // Progress 0 = entered from opposite side; 1 = at output edge. double baseX = tile.x() + 0.5; double baseY = tile.y() + 0.5; switch (dir) { case Rotation::North: return {baseX, baseY - (progress - 0.5)}; case Rotation::East: return {baseX + (progress - 0.5), baseY}; case Rotation::South: return {baseX, baseY + (progress - 0.5)}; case Rotation::West: return {baseX - (progress - 0.5), baseY}; } return {baseX, baseY}; } // --------------------------------------------------------------------------- // Construction / placement // --------------------------------------------------------------------------- BeltSystem::BeltSystem(double beltSpeed_tps) : m_progressPerTick_tpt(beltSpeed_tps * kTickDurationSeconds) { } void BeltSystem::placeBelt(QPoint tile, Rotation direction) { m_splitters.erase(key(tile)); BeltTile bt; bt.direction = direction; m_belts[key(tile)] = bt; } void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB) { m_belts.erase(key(tile)); SplitterTile st; st.outputA = outputA; st.outputB = outputB; st.nextOutputIsA = true; m_splitters[key(tile)] = st; } void BeltSystem::placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance) { m_belts.erase(key(tile)); m_splitters.erase(key(tile)); m_tunnelExits.erase(key(tile)); TunnelEntryTile te; te.direction = direction; te.maxDistance = maxDistance; m_tunnelEntries[key(tile)] = te; reevaluateTunnelPairing(); } void BeltSystem::placeTunnelExit(QPoint tile, Rotation direction) { m_belts.erase(key(tile)); m_splitters.erase(key(tile)); m_tunnelEntries.erase(key(tile)); TunnelExitTile tx; tx.direction = direction; m_tunnelExits[key(tile)] = tx; reevaluateTunnelPairing(); } void BeltSystem::removeTile(QPoint tile) { const bool wasTunnel = (m_tunnelEntries.erase(key(tile)) > 0) | (m_tunnelExits.erase(key(tile)) > 0); m_belts.erase(key(tile)); m_splitters.erase(key(tile)); if (wasTunnel) { reevaluateTunnelPairing(); } } void BeltSystem::setSplitterFilters(QPoint tile, const std::vector& filterA, const std::vector& filterB) { const std::map, SplitterTile>::iterator it = m_splitters.find(key(tile)); if (it == m_splitters.end()) { return; } it->second.filterA = filterA; it->second.filterB = filterB; } std::optional BeltSystem::getSplitterInfo(QPoint tile) const { const std::map, SplitterTile>::const_iterator it = m_splitters.find(key(tile)); if (it == m_splitters.end()) { return std::nullopt; } return SplitterInfo{ it->second.outputA, it->second.outputB, it->second.filterA, it->second.filterB }; } // --------------------------------------------------------------------------- // Tunnel pairing // --------------------------------------------------------------------------- void BeltSystem::reevaluateTunnelPairing() { std::vector oldLinks; std::swap(oldLinks, m_tunnelLinks); for (const std::pair, TunnelEntryTile>& entry : m_tunnelEntries) { const QPoint entryPos(entry.first.first, entry.first.second); const Rotation dir = entry.second.direction; const int maxDist = entry.second.maxDistance; for (int d = 1; d <= maxDist; ++d) { QPoint probe = entryPos; for (int step = 0; step < d; ++step) { probe = adjacentTile(probe, dir); } // Check if a same-direction tunnel entry is here (blocks pairing) const std::map, TunnelEntryTile>::const_iterator teIt = m_tunnelEntries.find(key(probe)); if (teIt != m_tunnelEntries.end() && teIt->second.direction == dir) { break; } // Check if a same-direction tunnel exit is here (forms pair) const std::map, TunnelExitTile>::const_iterator txIt = m_tunnelExits.find(key(probe)); if (txIt != m_tunnelExits.end()) { if (txIt->second.direction == dir) { TunnelLink link; link.entryTile = entryPos; link.exitTile = probe; link.length = static_cast(d); for (const TunnelLink& old : oldLinks) { if (old.entryTile == entryPos && old.exitTile == probe) { link.items = old.items; break; } } m_tunnelLinks.push_back(std::move(link)); break; } // Different direction exit — skip, keep searching } } } } // --------------------------------------------------------------------------- // Port interface // --------------------------------------------------------------------------- bool BeltSystem::tryPutItem(QPoint tile, Item item, Rotation fromDir) { const std::map, BeltTile>::iterator bIt = m_belts.find(key(tile)); if (bIt != m_belts.end()) { return tryPlaceOnBelt(tile, item); } const std::map, SplitterTile>::iterator splIt = m_splitters.find(key(tile)); if (splIt != m_splitters.end()) { if (splIt->second.back.size() < 2) { splIt->second.back.push_back(BeltItemSlot{item, 0.0}); splIt->second.backDir.push_back(fromDir); return true; } return false; } const std::map, TunnelEntryTile>::iterator teIt = m_tunnelEntries.find(key(tile)); if (teIt != m_tunnelEntries.end()) { if (teIt->second.itemSlots.size() < 4) { teIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0}); return true; } return false; } return false; } std::optional BeltSystem::tryTakeItem(Port port) { const std::map, BeltTile>::iterator beltIt = m_belts.find(key(port.tile)); if (beltIt != m_belts.end()) { if (beltIt->second.direction != port.direction) { return std::nullopt; } BeltTile& bt = beltIt->second; if (!bt.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0) { const Item taken = bt.itemSlots.front().item; bt.itemSlots.erase(bt.itemSlots.begin()); return taken; } return std::nullopt; } const std::map, SplitterTile>::iterator splIt = m_splitters.find(key(port.tile)); if (splIt != m_splitters.end()) { SplitterTile& st = splIt->second; if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0) { const Item taken = st.frontA->item; st.frontA = std::nullopt; return taken; } if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0) { const Item taken = st.frontB->item; st.frontB = std::nullopt; return taken; } } const std::map, TunnelExitTile>::iterator txIt = m_tunnelExits.find(key(port.tile)); if (txIt != m_tunnelExits.end()) { TunnelExitTile& tx = txIt->second; if (tx.direction == port.direction && !tx.itemSlots.empty() && tx.itemSlots.front().progress >= 1.0) { const Item taken = tx.itemSlots.front().item; tx.itemSlots.erase(tx.itemSlots.begin()); return taken; } } return std::nullopt; } std::optional BeltSystem::peekItem(Port port) const { const std::map, 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.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0) { return bt.itemSlots.front().item.type; } return std::nullopt; } const std::map, 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; } } const std::map, TunnelExitTile>::const_iterator txIt = m_tunnelExits.find(key(port.tile)); if (txIt != m_tunnelExits.end()) { const TunnelExitTile& tx = txIt->second; if (tx.direction == port.direction && !tx.itemSlots.empty() && tx.itemSlots.front().progress >= 1.0) { return tx.itemSlots.front().item.type; } } return std::nullopt; } // --------------------------------------------------------------------------- // Maintenance // --------------------------------------------------------------------------- void BeltSystem::clearTiles(const std::vector& tiles) { for (const QPoint& tile : tiles) { const std::map, BeltTile>::iterator bIt = m_belts.find(key(tile)); if (bIt != m_belts.end()) { bIt->second.itemSlots.clear(); } const std::map, SplitterTile>::iterator sIt = m_splitters.find(key(tile)); if (sIt != m_splitters.end()) { sIt->second.back.clear(); sIt->second.backDir.clear(); sIt->second.frontA = std::nullopt; sIt->second.frontB = std::nullopt; } const std::map, TunnelEntryTile>::iterator teIt = m_tunnelEntries.find(key(tile)); if (teIt != m_tunnelEntries.end()) { teIt->second.itemSlots.clear(); for (TunnelLink& link : m_tunnelLinks) { if (link.entryTile == tile) { link.items.clear(); } } } const std::map, TunnelExitTile>::iterator txIt = m_tunnelExits.find(key(tile)); if (txIt != m_tunnelExits.end()) { txIt->second.itemSlots.clear(); for (TunnelLink& link : m_tunnelLinks) { if (link.exitTile == tile) { link.items.clear(); } } } } } // --------------------------------------------------------------------------- // Tick // --------------------------------------------------------------------------- void BeltSystem::tick() { TRACE(); advanceProgress(); advanceTunnelProgress(); moveItemsToNextTile(); moveTunnelItems(); routeSplitterItems(); } void BeltSystem::advanceProgress() { for (std::map, BeltTile>::iterator it = m_belts.begin(); it != m_belts.end(); ++it) { BeltTile& bt = it->second; for (std::size_t i = 0; i < bt.itemSlots.size(); ++i) { bt.itemSlots[i].progress += m_progressPerTick_tpt; // Absolute cap: slot i cannot exceed 1.0 - i * 0.25. const double absoluteCap = 1.0 - i * 0.25; if (bt.itemSlots[i].progress > absoluteCap) { bt.itemSlots[i].progress = absoluteCap; } // Gap constraint: must stay 0.25 behind the slot ahead. if (i > 0) { const double gapCap = bt.itemSlots[i - 1].progress - 0.25; if (bt.itemSlots[i].progress > gapCap) { bt.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap); } } } } for (std::map, SplitterTile>::iterator it = m_splitters.begin(); it != m_splitters.end(); ++it) { SplitterTile& st = it->second; for (std::size_t i = 0; i < st.back.size(); ++i) { st.back[i].progress += m_progressPerTick_tpt; const double absoluteCap = 0.5 - i * 0.25; if (st.back[i].progress > absoluteCap) { st.back[i].progress = absoluteCap; } if (i > 0) { const double gapCap = st.back[i - 1].progress - 0.25; if (gapCap < 0.0) { st.back[i].progress = 0.0; } else if (st.back[i].progress > gapCap) { st.back[i].progress = gapCap; } } } if (st.frontA) { st.frontA->progress += m_progressPerTick_tpt; if (st.frontA->progress > 1.0) { st.frontA->progress = 1.0; } } if (st.frontB) { st.frontB->progress += m_progressPerTick_tpt; if (st.frontB->progress > 1.0) { st.frontB->progress = 1.0; } } } } void BeltSystem::advanceTunnelProgress() { for (std::map, TunnelEntryTile>::iterator it = m_tunnelEntries.begin(); it != m_tunnelEntries.end(); ++it) { TunnelEntryTile& te = it->second; for (std::size_t i = 0; i < te.itemSlots.size(); ++i) { te.itemSlots[i].progress += m_progressPerTick_tpt; const double absoluteCap = 1.0 - i * 0.25; if (te.itemSlots[i].progress > absoluteCap) { te.itemSlots[i].progress = absoluteCap; } if (i > 0) { const double gapCap = te.itemSlots[i - 1].progress - 0.25; if (te.itemSlots[i].progress > gapCap) { te.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap); } } } } for (std::map, TunnelExitTile>::iterator it = m_tunnelExits.begin(); it != m_tunnelExits.end(); ++it) { TunnelExitTile& tx = it->second; for (std::size_t i = 0; i < tx.itemSlots.size(); ++i) { tx.itemSlots[i].progress += m_progressPerTick_tpt; const double absoluteCap = 1.0 - i * 0.25; if (tx.itemSlots[i].progress > absoluteCap) { tx.itemSlots[i].progress = absoluteCap; } if (i > 0) { const double gapCap = tx.itemSlots[i - 1].progress - 0.25; if (tx.itemSlots[i].progress > gapCap) { tx.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap); } } } } for (TunnelLink& link : m_tunnelLinks) { for (std::size_t i = 0; i < link.items.size(); ++i) { TunnelTransitItem& ti = link.items[i]; ti.progress += m_progressPerTick_tpt; if (ti.progress > link.length) { ti.progress = link.length; } if (i > 0) { const double maxProgress = link.items[i - 1].progress - 0.25; if (ti.progress > maxProgress) { ti.progress = maxProgress; if (ti.progress < 0.0) { ti.progress = 0.0; } } } } } } void BeltSystem::moveItemsToNextTile() { // Belt items advancing into the next tile. for (std::map, BeltTile>::iterator it = m_belts.begin(); it != m_belts.end(); ++it) { BeltTile& bt = it->second; if (bt.itemSlots.empty() || bt.itemSlots.front().progress < 1.0) { continue; } const QPoint here = QPoint(it->first.first, it->first.second); const QPoint next = adjacentTile(here, bt.direction); const std::map, BeltTile>::iterator nextBelt = m_belts.find(key(next)); const std::map, SplitterTile>::iterator nextSplitter = m_splitters.find(key(next)); if (nextBelt != m_belts.end()) { if (tryPlaceOnBelt(next, bt.itemSlots.front().item)) { bt.itemSlots.erase(bt.itemSlots.begin()); } // else: next belt is full — item stays blocked at progress 1.0. } else if (nextSplitter != m_splitters.end()) { if (nextSplitter->second.back.size() < 2) { nextSplitter->second.back.push_back(BeltItemSlot{bt.itemSlots.front().item, 0.0}); nextSplitter->second.backDir.push_back(bt.direction); bt.itemSlots.erase(bt.itemSlots.begin()); } } else { const std::map, TunnelEntryTile>::iterator nextEntry = m_tunnelEntries.find(key(next)); if (nextEntry != m_tunnelEntries.end() && nextEntry->second.itemSlots.size() < 4) { nextEntry->second.itemSlots.push_back( BeltItemSlot{bt.itemSlots.front().item, 0.0}); bt.itemSlots.erase(bt.itemSlots.begin()); } } } // Splitter front slots advancing into downstream belt tiles. for (std::map, 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 (tryPushToTile(dest, st.frontA->item, st.outputA)) { st.frontA = std::nullopt; } } if (st.frontB && st.frontB->progress >= 1.0) { const QPoint dest = adjacentTile(here, st.outputB); if (tryPushToTile(dest, st.frontB->item, st.outputB)) { st.frontB = std::nullopt; } } } // Tunnel exit items advancing into downstream tiles. for (std::map, TunnelExitTile>::iterator it = m_tunnelExits.begin(); it != m_tunnelExits.end(); ++it) { TunnelExitTile& tx = it->second; if (tx.itemSlots.empty() || tx.itemSlots.front().progress < 1.0) { continue; } const QPoint here = QPoint(it->first.first, it->first.second); const QPoint next = adjacentTile(here, tx.direction); if (tryPushToTile(next, tx.itemSlots.front().item, tx.direction)) { tx.itemSlots.erase(tx.itemSlots.begin()); } } } void BeltSystem::moveTunnelItems() { for (TunnelLink& link : m_tunnelLinks) { // Entry front → transit const std::map, TunnelEntryTile>::iterator teIt = m_tunnelEntries.find(key(link.entryTile)); if (teIt != m_tunnelEntries.end()) { TunnelEntryTile& te = teIt->second; if (!te.itemSlots.empty() && te.itemSlots.front().progress >= 1.0) { const bool canEnter = link.items.empty() || link.items.back().progress >= 0.25; if (canEnter) { TunnelTransitItem ti; ti.item = te.itemSlots.front().item; ti.progress = 0.0; link.items.push_back(ti); te.itemSlots.erase(te.itemSlots.begin()); } } } // Transit front → exit if (!link.items.empty() && link.items.front().progress >= link.length) { const std::map, TunnelExitTile>::iterator txIt = m_tunnelExits.find(key(link.exitTile)); if (txIt != m_tunnelExits.end()) { TunnelExitTile& tx = txIt->second; if (tx.itemSlots.size() < 4) { tx.itemSlots.push_back(BeltItemSlot{link.items.front().item, 0.0}); link.items.erase(link.items.begin()); } } } } } void BeltSystem::routeSplitterItems() { for (std::map, SplitterTile>::iterator it = m_splitters.begin(); it != m_splitters.end(); ++it) { SplitterTile& st = it->second; if (st.back.empty() || st.back.front().progress < 0.5) { continue; } const Item& item = st.back.front().item; const bool matchesA = st.filterA.empty() || std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end(); const bool matchesB = st.filterB.empty() || std::find(st.filterB.begin(), st.filterB.end(), item.type) != st.filterB.end(); bool routed = false; if (matchesA && !matchesB) { if (!st.frontA) { st.frontA = BeltItemSlot{item, 0.0}; routed = true; } } else if (matchesB && !matchesA) { if (!st.frontB) { st.frontB = BeltItemSlot{item, 0.0}; routed = true; } } else if (matchesA && matchesB) { // Alternation: try preferred output first, fall back to other if preferred full. const bool preferA = st.nextOutputIsA; if (preferA && !st.frontA) { st.frontA = BeltItemSlot{item, 0.0}; st.nextOutputIsA = false; routed = true; } else if (!preferA && !st.frontB) { st.frontB = BeltItemSlot{item, 0.0}; 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}; routed = true; } else if (!preferA && !st.frontA) { // Preferred (B) is full — fall back to A; nextOutputIsA stays. st.frontA = BeltItemSlot{item, 0.75}; routed = true; } // else both fronts occupied — back stays. } // else (!matchesA && !matchesB): stall — back stays. if (routed) { st.back.erase(st.back.begin()); st.backDir.erase(st.backDir.begin()); } } } bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item) { const std::map, BeltTile>::iterator it = m_belts.find(key(tile)); if (it == m_belts.end()) { return false; } BeltTile& bt = it->second; if (bt.itemSlots.size() < 4) { bt.itemSlots.push_back(BeltItemSlot{item, 0.0}); return true; } return false; // all slots occupied } bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir) { if (tryPlaceOnBelt(dest, item)) { return true; } const std::map, SplitterTile>::iterator splIt = m_splitters.find(key(dest)); if (splIt != m_splitters.end()) { if (splIt->second.back.size() < 2) { splIt->second.back.push_back(BeltItemSlot{item, 0.0}); splIt->second.backDir.push_back(fromDir); return true; } return false; } const std::map, TunnelEntryTile>::iterator teIt = m_tunnelEntries.find(key(dest)); if (teIt != m_tunnelEntries.end()) { if (teIt->second.itemSlots.size() < 4) { teIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0}); return true; } return false; } const std::map, TunnelExitTile>::iterator txIt = m_tunnelExits.find(key(dest)); if (txIt != m_tunnelExits.end()) { if (txIt->second.itemSlots.size() < 4) { txIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0}); return true; } return false; } return false; } // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- void BeltSystem::forEachVisualItem(QRect viewportTiles, std::function visit) const { for (const std::pair, BeltTile>& entry : m_belts) { const QPoint tile(entry.first.first, entry.first.second); if (!viewportTiles.contains(tile)) { continue; } const BeltTile& bt = entry.second; // Render least-progressed first (bottom) → most-progressed last (top). for (int i = static_cast(bt.itemSlots.size()) - 1; i >= 0; --i) { VisualItem vi; vi.type = bt.itemSlots[i].item.type; vi.worldPos = slotWorldPos(tile, bt.direction, bt.itemSlots[i].progress); visit(vi); } } for (const std::pair, SplitterTile>& entry : m_splitters) { const QPoint tile(entry.first.first, entry.first.second); if (!viewportTiles.contains(tile)) { continue; } const SplitterTile& st = entry.second; // Unassigned items: least-progressed first (bottom), then higher-progressed (top). for (int i = static_cast(st.back.size()) - 1; i >= 0; --i) { VisualItem vi; vi.type = st.back[i].item.type; vi.worldPos = slotWorldPos(tile, st.backDir[i], st.back[i].progress); visit(vi); } // Output-slot items rendered on top of unassigned, in clockwise order from East. // East=0, South=1, West=2, North=3 — lower rank rendered first (bottom). auto clockwiseRank = [](Rotation r) -> int { switch (r) { case Rotation::East: return 0; case Rotation::South: return 1; case Rotation::West: return 2; case Rotation::North: return 3; } return 0; }; const bool aBeforeB = clockwiseRank(st.outputA) <= clockwiseRank(st.outputB); auto renderFront = [&](const std::optional& slot, Rotation dir) { if (slot) { VisualItem vi; vi.type = slot->item.type; vi.worldPos = slotWorldPos(tile, dir, slot->progress); visit(vi); } }; if (aBeforeB) { renderFront(st.frontA, st.outputA); renderFront(st.frontB, st.outputB); } else { renderFront(st.frontB, st.outputB); renderFront(st.frontA, st.outputA); } } for (const std::pair, TunnelEntryTile>& entry : m_tunnelEntries) { const QPoint tile(entry.first.first, entry.first.second); if (!viewportTiles.contains(tile)) { continue; } const TunnelEntryTile& te = entry.second; for (int i = static_cast(te.itemSlots.size()) - 1; i >= 0; --i) { VisualItem vi; vi.type = te.itemSlots[i].item.type; vi.worldPos = slotWorldPos(tile, te.direction, te.itemSlots[i].progress); visit(vi); } } for (const std::pair, TunnelExitTile>& entry : m_tunnelExits) { const QPoint tile(entry.first.first, entry.first.second); if (!viewportTiles.contains(tile)) { continue; } const TunnelExitTile& tx = entry.second; for (int i = static_cast(tx.itemSlots.size()) - 1; i >= 0; --i) { VisualItem vi; vi.type = tx.itemSlots[i].item.type; vi.worldPos = slotWorldPos(tile, tx.direction, tx.itemSlots[i].progress); visit(vi); } } }