Files
dota_factory/src/lib/sim/BeltSystem.cpp
Malte Langkabel fbb1af85e3 list what a belt is carrying, and clear it by name
The belt card's only content was a button reading "Clear stuck items", which
assumed a state the items need not be in. It now reads "Clear items", and above
it the card lists what the selected tiles hold -- one item chip per type, the
same chip the buffer sections and the HQ's block stock draw -- so a line's
contents can be read before they are removed, and can be read at all: items on a
moving belt are too small and too transient to count by eye, and items inside a
tunnel are drawn nowhere.

BeltSystem gains countItems(tiles), a query of the same kind as
forEachVisualItem: a method rather than exposed tile containers, so the per-tile
representation stays swappable. It walks the same five containers as clearTiles,
in the same order, so the list and the button cannot drift apart.

The tunnel's two ends are now told apart. Items in transit are counted on the
exit they are travelling toward, and a clear removes exactly what the panel
listed for the tile it acts on: the exit discards them, the entry leaves them
travelling. BeltSystemTest's tunnel case splits in two accordingly.

Splitters now aggregate with belts and tunnel ends. Their output filters are
per-object configuration, which an aggregate simply does not show -- a splitter
selected alone still gets them. Both cards share one BeltItemList widget, and
both derive their tiles from collectBeltTiles, so nothing is stated twice.

The mixed count summary loses the clear action it carried: a button acting on
part of a selection is worse than no button, and the tiles can be selected by
themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-19 14:38:45 +02:00

1129 lines
36 KiB
C++

#include "BeltSystem.h"
#include <algorithm>
#include "StateChecksum.h"
#include "Tick.h"
#include "TunnelCompletion.h"
#include "tracing.h"
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
std::pair<int, int> 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;
}
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<std::pair<int, int>, BeltTile>::const_iterator beltIt =
m_belts.find(key(tile));
if (beltIt != m_belts.end())
{
return entryEdge == beltIt->second.direction;
}
const std::map<std::pair<int, int>, 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<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
return entryEdge == teIt->second.direction;
}
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
m_tunnelExits.find(key(tile));
if (txIt != m_tunnelExits.end())
{
return entryEdge == txIt->second.direction;
}
return false;
}
// ---------------------------------------------------------------------------
// 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<ItemType>& filterA,
const std::vector<ItemType>& filterB)
{
const std::map<std::pair<int, int>, 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::SplitterInfo> BeltSystem::getSplitterInfo(QPoint tile) const
{
const std::map<std::pair<int, int>, 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<TunnelLink> oldLinks;
std::swap(oldLinks, m_tunnelLinks);
// Tunnel index over this system's own (completed) tunnel tiles, shared with the
// scan primitive so the pairing rule lives in one place (REQ-BLD-TUNNEL-PAIR).
const TunnelLookup lookup = [this](QPoint tile) -> std::optional<TunnelTileInfo>
{
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
return TunnelTileInfo{BuildingType::TunnelEntry, teIt->second.direction};
}
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
m_tunnelExits.find(key(tile));
if (txIt != m_tunnelExits.end())
{
return TunnelTileInfo{BuildingType::TunnelExit, txIt->second.direction};
}
return std::nullopt;
};
for (const std::pair<const std::pair<int, int>, 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;
// The first same-direction tunnel ahead forms a pair only when it is an exit;
// a same-direction entry blocks (firstTunnelFacing stops at it either way).
const std::optional<QPoint> target =
firstTunnelFacing(lookup, entryPos, dir, dir, maxDist);
if (!target.has_value() || m_tunnelExits.find(key(*target)) == m_tunnelExits.end())
{
continue;
}
TunnelLink link;
link.entryTile = entryPos;
link.exitTile = *target;
// The exit is colinear with the entry along `dir`, so the tile-coordinate
// distance is the Manhattan distance.
link.length = static_cast<double>((*target - entryPos).manhattanLength());
for (const TunnelLink& old : oldLinks)
{
if (old.entryTile == entryPos && old.exitTile == *target)
{
link.items = old.items;
break;
}
}
m_tunnelLinks.push_back(std::move(link));
}
}
// ---------------------------------------------------------------------------
// Port interface
// ---------------------------------------------------------------------------
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<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
if (bIt != m_belts.end())
{
return tryPlaceOnBelt(tile, item);
}
const std::map<std::pair<int, int>, 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<std::pair<int, int>, 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<Item> BeltSystem::tryTakeItem(Port port)
{
const std::map<std::pair<int, int>, 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<std::pair<int, int>, 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<std::pair<int, int>, 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<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.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0)
{
return bt.itemSlots.front().item.type;
}
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;
}
}
const std::map<std::pair<int, int>, 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<QPoint>& tiles)
{
for (const QPoint& tile : tiles)
{
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
if (bIt != m_belts.end())
{
bIt->second.itemSlots.clear();
}
const std::map<std::pair<int, int>, 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<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
// Only what sits on the entry itself: the items already inside the tunnel
// are counted on its exit (REQ-UI-BELT-ITEMS) and are cleared from there, so
// a clear never removes more than the panel showed for the tile it acted on
// (REQ-BLD-TUNNEL-TRANSIT). Emptying a tunnel is done from its exit.
teIt->second.itemSlots.clear();
}
const std::map<std::pair<int, int>, 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();
}
}
}
}
}
std::map<ItemType, int> BeltSystem::countItems(const std::vector<QPoint>& tiles) const
{
std::map<ItemType, int> counts;
// The same five containers clearTiles walks, in the same order, so that what the
// panel lists and what the button removes cannot drift apart (REQ-UI-BELT-ITEMS,
// REQ-UI-BELT-CLEAR).
for (const QPoint& tile : tiles)
{
const std::map<std::pair<int, int>, BeltTile>::const_iterator bIt =
m_belts.find(key(tile));
if (bIt != m_belts.end())
{
for (const BeltItemSlot& slot : bIt->second.itemSlots)
{
counts[slot.item.type]++;
}
}
const std::map<std::pair<int, int>, SplitterTile>::const_iterator sIt =
m_splitters.find(key(tile));
if (sIt != m_splitters.end())
{
const SplitterTile& splitter = sIt->second;
for (const BeltItemSlot& slot : splitter.back)
{
counts[slot.item.type]++;
}
if (splitter.frontA)
{
counts[splitter.frontA->item.type]++;
}
if (splitter.frontB)
{
counts[splitter.frontB->item.type]++;
}
}
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
for (const BeltItemSlot& slot : teIt->second.itemSlots)
{
counts[slot.item.type]++;
}
}
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
m_tunnelExits.find(key(tile));
if (txIt != m_tunnelExits.end())
{
for (const BeltItemSlot& slot : txIt->second.itemSlots)
{
counts[slot.item.type]++;
}
// The tunnel's invisible cargo, counted on the end it is travelling toward.
// This is the only place the player can see it at all, the tunnel drawing
// nothing of what is inside it (REQ-BLD-TUNNEL-TRANSIT).
for (const TunnelLink& link : m_tunnelLinks)
{
if (link.exitTile != tile)
{
continue;
}
for (const TunnelTransitItem& transit : link.items)
{
counts[transit.item.type]++;
}
}
}
}
return counts;
}
// ---------------------------------------------------------------------------
// Tick
// ---------------------------------------------------------------------------
void BeltSystem::tick()
{
TRACE();
advanceProgress();
advanceTunnelProgress();
moveItemsToNextTile();
moveTunnelItems();
routeSplitterItems();
}
void BeltSystem::advanceProgress()
{
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
it != m_belts.end(); ++it)
{
advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
}
for (std::map<std::pair<int, int>, 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<std::pair<int, int>, TunnelEntryTile>::iterator it = m_tunnelEntries.begin();
it != m_tunnelEntries.end(); ++it)
{
advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
}
for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
it != m_tunnelExits.end(); ++it)
{
advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
}
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<std::pair<int, int>, 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);
// 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<std::pair<int, int>, BeltTile>::iterator nextBelt = m_belts.find(key(next));
const std::map<std::pair<int, int>, 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<std::pair<int, int>, 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<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 (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<std::pair<int, int>, 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<std::pair<int, int>, 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<std::pair<int, int>, 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<std::pair<int, int>, 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;
// 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, frontEntryProgress};
routed = true;
}
}
else if (matchesB && !matchesA)
{
if (!st.frontB)
{
st.frontB = BeltItemSlot{item, frontEntryProgress};
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, frontEntryProgress};
st.nextOutputIsA = false;
routed = true;
}
else if (!preferA && !st.frontB)
{
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, frontEntryProgress};
routed = true;
}
else if (!preferA && !st.frontA)
{
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
st.frontA = BeltItemSlot{item, frontEntryProgress};
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<std::pair<int, int>, 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)
{
// 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;
}
const std::map<std::pair<int, int>, 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<std::pair<int, int>, 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<std::pair<int, int>, 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<void(VisualItem)> visit) const
{
for (const std::pair<const std::pair<int, int>, 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<int>(bt.itemSlots.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = bt.itemSlots[i].item.type;
vi.worldPos = beltSlotWorldPos(tile, bt.direction, bt.itemSlots[i].progress);
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;
// Unassigned items: least-progressed first (bottom), then higher-progressed (top).
for (int i = static_cast<int>(st.back.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = st.back[i].item.type;
vi.worldPos = beltSlotWorldPos(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<BeltItemSlot>& slot, Rotation dir)
{
if (slot)
{
VisualItem vi;
vi.type = slot->item.type;
vi.worldPos = beltSlotWorldPos(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<const std::pair<int, int>, 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<int>(te.itemSlots.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = te.itemSlots[i].item.type;
vi.worldPos = beltSlotWorldPos(tile, te.direction, te.itemSlots[i].progress);
visit(vi);
}
}
for (const std::pair<const std::pair<int, int>, 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<int>(tx.itemSlots.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = tx.itemSlots[i].item.type;
vi.worldPos = beltSlotWorldPos(tile, tx.direction, tx.itemSlots[i].progress);
visit(vi);
}
}
}
void BeltSystem::appendItemSlots(Hasher& hasher, const std::vector<BeltItemSlot>& slotRun)
{
hasher.append(slotRun.size());
for (const BeltItemSlot& slot : slotRun)
{
hasher.append(slot.item.type.id);
hasher.append(slot.progress);
}
}
void BeltSystem::appendChecksum(Hasher& hasher) const
{
// std::map iterates in sorted key order, so all tile loops are deterministic.
hasher.append(m_belts.size());
for (const std::pair<const std::pair<int, int>, BeltTile>& entry : m_belts)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
hasher.append(entry.second.direction);
appendItemSlots(hasher, entry.second.itemSlots);
}
hasher.append(m_splitters.size());
for (const std::pair<const std::pair<int, int>, SplitterTile>& entry : m_splitters)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
const SplitterTile& s = entry.second;
hasher.append(s.outputA);
hasher.append(s.outputB);
hasher.append(s.filterA.size());
for (const ItemType& type : s.filterA) { hasher.append(type.id); }
hasher.append(s.filterB.size());
for (const ItemType& type : s.filterB) { hasher.append(type.id); }
hasher.append(s.nextOutputIsA);
appendItemSlots(hasher, s.back);
hasher.append(s.backDir.size());
for (Rotation dir : s.backDir) { hasher.append(dir); }
hasher.append(s.frontA.has_value());
if (s.frontA.has_value())
{
hasher.append(s.frontA->item.type.id);
hasher.append(s.frontA->progress);
}
hasher.append(s.frontB.has_value());
if (s.frontB.has_value())
{
hasher.append(s.frontB->item.type.id);
hasher.append(s.frontB->progress);
}
}
hasher.append(m_tunnelEntries.size());
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
hasher.append(entry.second.direction);
hasher.append(entry.second.maxDistance);
appendItemSlots(hasher, entry.second.itemSlots);
}
hasher.append(m_tunnelExits.size());
for (const std::pair<const std::pair<int, int>, TunnelExitTile>& entry : m_tunnelExits)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
hasher.append(entry.second.direction);
appendItemSlots(hasher, entry.second.itemSlots);
}
// m_tunnelLinks preserves insertion order, which is itself deterministic.
hasher.append(m_tunnelLinks.size());
for (const TunnelLink& link : m_tunnelLinks)
{
hasher.append(link.entryTile);
hasher.append(link.exitTile);
hasher.append(link.length);
hasher.append(link.items.size());
for (const TunnelTransitItem& item : link.items)
{
hasher.append(item.item.type.id);
hasher.append(item.progress);
}
}
}