Collapse the two tunnel buttons into a single Tunnel button whose ghost resolves between Tunnel Entry and Tunnel Exit based on the hovered position, with a green connection preview. - Add pure core helper TunnelCompletion (firstTunnelFacing + resolveTunnelCompletion): the entry/exit completion scans and the sub-tile cursor tie-break, unit-tested in TunnelCompletionTest. - Dedup BeltSystem::reevaluateTunnelPairing onto firstTunnelFacing so the pairing scan rule lives in one place. - GameWorldView: resolve the ghost type on hover/rotate (matching built tunnels and construction sites), place the resolved type (drop the old auto-switch), and draw the matched partner + between-tiles in green. Hotkey 3 = Tunnel, 4 unused. - BuildButtonGrid: one "Tunnel" button; tunnel_exit stays player-placeable so blueprints and cost totals still account for exits. - Add the green tunnel_preview overlay color (VisualsConfig/Loader/toml). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
1055 lines
33 KiB
C++
1055 lines
33 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())
|
|
{
|
|
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)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|