Files
dota_factory/src/lib/sim/BeltSystem.cpp

967 lines
30 KiB
C++

#include "BeltSystem.h"
#include <algorithm>
#include "Tick.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;
}
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<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);
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;
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<std::pair<int, int>, 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<std::pair<int, int>, 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<double>(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<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())
{
teIt->second.itemSlots.clear();
for (TunnelLink& link : m_tunnelLinks)
{
if (link.entryTile == tile)
{
link.items.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();
}
}
}
}
}
// ---------------------------------------------------------------------------
// 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)
{
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<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)
{
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<std::pair<int, int>, 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<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);
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;
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<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)
{
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 = slotWorldPos(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 = 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<BeltItemSlot>& 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<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 = slotWorldPos(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 = slotWorldPos(tile, tx.direction, tx.itemSlots[i].progress);
visit(vi);
}
}
}