implement belt system

This commit is contained in:
2026-04-19 16:18:39 +02:00
parent ffe69f08b5
commit f2d912b4eb
10 changed files with 864 additions and 8 deletions

373
src/lib/sim/BeltSystem.cpp Normal file
View File

@@ -0,0 +1,373 @@
#include "BeltSystem.h"
#include <algorithm>
#include "Tick.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 beltSpeedTilesPerSecond)
: m_progressPerTick(beltSpeedTilesPerSecond * 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::removeTile(QPoint tile)
{
m_belts.erase(key(tile));
m_splitters.erase(key(tile));
}
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;
}
// ---------------------------------------------------------------------------
// Port interface
// ---------------------------------------------------------------------------
bool BeltSystem::tryPutItem(Port port, Item item)
{
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(port.tile));
if (it == m_belts.end())
{
return false;
}
if (it->second.direction != port.direction)
{
return false;
}
return tryPlaceOnBelt(port.tile, item);
}
std::optional<Item> BeltSystem::tryTakeItem(Port port)
{
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(port.tile));
if (it == m_belts.end())
{
return std::nullopt;
}
if (it->second.direction != port.direction)
{
return std::nullopt;
}
BeltTile& bt = it->second;
if (bt.front)
{
const Item taken = bt.front->item;
bt.front = bt.back;
bt.back = std::nullopt;
return taken;
}
if (bt.back)
{
const Item taken = bt.back->item;
bt.back = std::nullopt;
return taken;
}
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.front = std::nullopt;
bIt->second.back = std::nullopt;
}
const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
if (sIt != m_splitters.end())
{
sIt->second.heldItem = std::nullopt;
}
}
}
// ---------------------------------------------------------------------------
// Tick
// ---------------------------------------------------------------------------
void BeltSystem::tick()
{
advanceProgress();
moveItemsToNextTile();
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;
if (bt.front)
{
bt.front->progress += m_progressPerTick;
if (bt.front->progress > 1.0)
{
bt.front->progress = 1.0;
}
}
if (bt.back)
{
bt.back->progress += m_progressPerTick;
// Back must not overtake front.
if (bt.front && bt.back->progress >= bt.front->progress)
{
bt.back->progress = bt.front->progress - m_progressPerTick;
if (bt.back->progress < 0.0)
{
bt.back->progress = 0.0;
}
}
if (bt.back->progress > 1.0)
{
bt.back->progress = 1.0;
}
}
}
}
void BeltSystem::moveItemsToNextTile()
{
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
it != m_belts.end(); ++it)
{
BeltTile& bt = it->second;
if (!bt.front || bt.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.front->item))
{
bt.front = bt.back;
bt.back = std::nullopt;
}
// else: next belt is full — item stays blocked at progress 1.0.
}
else if (nextSplitter != m_splitters.end())
{
if (!nextSplitter->second.heldItem)
{
nextSplitter->second.heldItem = bt.front->item;
bt.front = bt.back;
bt.back = std::nullopt;
}
// else: splitter busy — item stays blocked at progress 1.0.
}
// else: no tile registered (e.g. open space, or building input port).
// Items leaving into unregistered tiles are not consumed here — the
// building pull step uses tryTakeItem for that.
}
}
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.heldItem)
{
continue;
}
const Item& item = *st.heldItem;
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();
if (matchesA && !matchesB)
{
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputA);
if (tryPlaceOnBelt(dest, item))
{
st.heldItem = std::nullopt;
}
}
else if (matchesB && !matchesA)
{
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputB);
if (tryPlaceOnBelt(dest, item))
{
st.heldItem = std::nullopt;
}
}
else if (matchesA && matchesB)
{
// Alternation: try preferred output first, fall back to other.
const Rotation preferred = st.nextOutputIsA ? st.outputA : st.outputB;
const Rotation fallback = st.nextOutputIsA ? st.outputB : st.outputA;
const QPoint prefDest = adjacentTile(QPoint(it->first.first, it->first.second), preferred);
const QPoint fbDest = adjacentTile(QPoint(it->first.first, it->first.second), fallback);
if (tryPlaceOnBelt(prefDest, item))
{
st.heldItem = std::nullopt;
st.nextOutputIsA = !st.nextOutputIsA;
}
else if (tryPlaceOnBelt(fbDest, item))
{
st.heldItem = std::nullopt;
// nextOutputIsA stays: preferred was blocked, so we still owe it next.
}
// else both blocked — item stays.
}
// else (!matchesA && !matchesB): stall — item stays in splitter.
}
}
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.front)
{
bt.front = BeltItemSlot{item, 0.0};
return true;
}
if (!bt.back)
{
bt.back = BeltItemSlot{item, 0.0};
// Ensure ordering invariant: front has higher progress.
if (bt.back->progress > bt.front->progress)
{
std::swap(bt.front, bt.back);
}
return true;
}
return false; // both slots occupied
}
// ---------------------------------------------------------------------------
// 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;
if (bt.front)
{
VisualItem vi;
vi.type = bt.front->item.type;
vi.worldPos = slotWorldPos(tile, bt.direction, bt.front->progress);
visit(vi);
}
if (bt.back)
{
VisualItem vi;
vi.type = bt.back->item.type;
vi.worldPos = slotWorldPos(tile, bt.direction, bt.back->progress);
visit(vi);
}
}
}

117
src/lib/sim/BeltSystem.h Normal file
View File

@@ -0,0 +1,117 @@
#pragma once
#include <functional>
#include <map>
#include <optional>
#include <utility>
#include <vector>
#include <QPoint>
#include <QPointF>
#include <QRect>
#include "Item.h"
#include "ItemType.h"
#include "Port.h"
#include "Rotation.h"
// Carries item type and fractional world position for the renderer.
// worldPos is in tile units (1 tile = 1.0 unit); origin matches tile coords.
struct VisualItem
{
ItemType type;
QPointF worldPos;
};
// Isolated belt-and-splitter transport layer. See architecture.md §Belt Subsystem.
//
// Buildings interact only through tryPutItem / tryTakeItem.
// Rendering reads only through forEachVisualItem.
// No other system inspects tile contents.
class BeltSystem
{
public:
explicit BeltSystem(double beltSpeedTilesPerSecond);
// -- Placement -----------------------------------------------------------
// Register a new belt tile. Any items already on this tile are cleared.
void placeBelt(QPoint tile, Rotation direction);
// Register a new splitter tile. outputA and outputB are the two exit
// directions (e.g. West and East for a default-rotation splitter).
// Items entering from any adjacent belt whose direction points into this
// tile are held and routed to one of the two outputs.
void placeSplitter(QPoint tile, Rotation outputA, Rotation outputB);
// Remove a belt or splitter tile (on demolish). Items are discarded.
void removeTile(QPoint tile);
// -- Splitter filter configuration (REQ-BLD-SPLITTER) -------------------
// filterA / filterB: empty means "accept all".
void setSplitterFilters(QPoint tile,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB);
// -- Port interface (buildings <-> belts) --------------------------------
// port.tile = the belt tile adjacent to the building
// port.direction = direction items flow on that tile
//
// tryPutItem: place item onto port.tile entering from the opposite side.
// Returns false if the tile is not a belt, direction mismatches, or tile full.
bool tryPutItem(Port port, Item item);
// tryTakeItem: remove and return the leading item from port.tile.
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty.
std::optional<Item> tryTakeItem(Port port);
// -- Maintenance ---------------------------------------------------------
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
void tick();
// -- Rendering -----------------------------------------------------------
void forEachVisualItem(QRect viewportTiles,
std::function<void(VisualItem)> visit) const;
private:
void advanceProgress();
void moveItemsToNextTile();
void routeSplitterItems();
// Place item into back slot of an existing belt tile at progress 0.
// Returns false if tile is not a belt or is full.
bool tryPlaceOnBelt(QPoint tile, Item item);
static std::pair<int, int> key(QPoint tile);
static QPoint adjacentTile(QPoint tile, Rotation dir);
// Returns the world-space centre of a slot given tile origin and progress.
static QPointF slotWorldPos(QPoint tile, Rotation dir, double progress);
struct BeltItemSlot
{
Item item;
double progress; // [0.0, 1.0]: 0 = just entered, 1 = at output edge
};
struct BeltTile
{
Rotation direction;
std::optional<BeltItemSlot> front; // higher progress; closer to output
std::optional<BeltItemSlot> back; // lower progress; closer to input
};
struct SplitterTile
{
Rotation outputA;
Rotation outputB;
std::vector<ItemType> filterA; // empty = accept all
std::vector<ItemType> filterB;
bool nextOutputIsA; // alternation state
std::optional<Item> heldItem; // item buffered waiting to exit
};
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
std::map<std::pair<int, int>, BeltTile> m_belts;
std::map<std::pair<int, int>, SplitterTile> m_splitters;
};

View File

@@ -2,6 +2,7 @@ SET(HDRS
${HDRS}
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.h
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
PARENT_SCOPE
)
@@ -9,6 +10,7 @@ SET(SRCS
${SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
PARENT_SCOPE
)