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
210 lines
8.0 KiB
C++
210 lines
8.0 KiB
C++
#pragma once
|
|
|
|
#include <functional>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <QPoint>
|
|
#include <QPointF>
|
|
#include <QRect>
|
|
|
|
#include "BeltSlot.h"
|
|
#include "Item.h"
|
|
#include "ItemType.h"
|
|
#include "Port.h"
|
|
#include "Rotation.h"
|
|
|
|
class Hasher;
|
|
|
|
// 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 beltSpeed_tps);
|
|
|
|
// -- Placement -----------------------------------------------------------
|
|
// Register a new belt tile. Any items already on this tile are cleared.
|
|
void placeBelt(QPoint tile, Rotation direction);
|
|
|
|
// Register a new tunnel entry tile.
|
|
void placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance);
|
|
|
|
// Register a new tunnel exit tile.
|
|
void placeTunnelExit(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 deconstruct). 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);
|
|
|
|
struct SplitterInfo
|
|
{
|
|
Rotation outputA;
|
|
Rotation outputB;
|
|
std::vector<ItemType> filterA;
|
|
std::vector<ItemType> filterB;
|
|
};
|
|
std::optional<SplitterInfo> getSplitterInfo(QPoint tile) const;
|
|
|
|
// -- 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 tile.
|
|
// Returns false if the tile is not a belt/splitter/tunnel entry, tile full,
|
|
// or the item would enter through the tile's output edge (REQ-MAT-ACCEPT-DIR).
|
|
// fromDir: travel direction of the item (used for splitter animation and for
|
|
// the output-edge check).
|
|
bool tryPutItem(QPoint tile, Item item, Rotation fromDir = Rotation::West);
|
|
|
|
// 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);
|
|
|
|
// peekItem: return the type of the leading item without removing it.
|
|
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty.
|
|
std::optional<ItemType> peekItem(Port port) const;
|
|
|
|
// Progress advanced per tick at the configured belt speed (tile fraction per
|
|
// tick). Shared with building output belts so emerging items travel at exactly
|
|
// the same speed as real belts (REQ-MAT-OUTPUT-EMERGE).
|
|
double getProgressPerTick_tpt() const { return m_progressPerTick_tpt; }
|
|
|
|
// -- Maintenance ---------------------------------------------------------
|
|
// Removes every item from the given tiles (REQ-UI-BELT-CLEAR). A tunnel exit takes
|
|
// the items in transit through its tunnel with it; a tunnel entry does not, so what
|
|
// a clear removes is exactly what countItems reported for the same tile.
|
|
void clearTiles(const std::vector<QPoint>& tiles);
|
|
void tick();
|
|
|
|
// -- Rendering -----------------------------------------------------------
|
|
void forEachVisualItem(QRect viewportTiles,
|
|
std::function<void(VisualItem)> visit) const;
|
|
|
|
// -- Inspection ----------------------------------------------------------
|
|
// What the given tiles carry, summed per item type (REQ-UI-BELT-ITEMS). Items in
|
|
// transit through a tunnel are counted on its exit, that being where they will
|
|
// arrive and the only end whose clear discards them (REQ-BLD-TUNNEL-TRANSIT).
|
|
//
|
|
// A method rather than an exposed container: what a tile holds and how it holds it
|
|
// is this subsystem's alone to know, so the per-tile representation stays swappable
|
|
// (architecture.md, Belt Subsystem).
|
|
std::map<ItemType, int> countItems(const std::vector<QPoint>& tiles) const;
|
|
|
|
// -- Determinism ---------------------------------------------------------
|
|
// Folds all transport state (belt/splitter/tunnel tiles and their items)
|
|
// into the hasher in deterministic order (see docs/replay_design.md).
|
|
void appendChecksum(Hasher& hasher) const;
|
|
|
|
private:
|
|
void advanceProgress();
|
|
void advanceTunnelProgress();
|
|
void moveItemsToNextTile();
|
|
void moveTunnelItems();
|
|
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);
|
|
|
|
// Push an item to any tile type (belt, splitter, tunnel entry, tunnel exit).
|
|
bool tryPushToTile(QPoint dest, Item item, Rotation fromDir);
|
|
|
|
void reevaluateTunnelPairing();
|
|
|
|
static std::pair<int, int> key(QPoint tile);
|
|
static QPoint adjacentTile(QPoint tile, Rotation dir);
|
|
static Rotation oppositeRotation(Rotation dir);
|
|
|
|
// True if an item travelling in travelDir would enter the transport tile at
|
|
// `tile` through one of that tile's output edges (and must therefore be
|
|
// refused). Returns false if no transport tile occupies `tile`.
|
|
bool entersThroughOutputEdge(QPoint tile, Rotation travelDir) const;
|
|
|
|
struct BeltTile
|
|
{
|
|
Rotation direction;
|
|
// front (highest progress) at index 0; back (just entered) at end. Max 4.
|
|
std::vector<BeltItemSlot> itemSlots;
|
|
};
|
|
|
|
struct SplitterTile
|
|
{
|
|
Rotation outputA;
|
|
Rotation outputB;
|
|
std::vector<ItemType> filterA; // empty = accept all
|
|
std::vector<ItemType> filterB;
|
|
bool nextOutputIsA; // alternation state
|
|
// Unassigned items: [0] = routing candidate (higher progress, caps at 0.5). Max 2.
|
|
std::vector<BeltItemSlot> back;
|
|
std::vector<Rotation> backDir; // feeding belt direction, parallel to back
|
|
std::optional<BeltItemSlot> frontA; // progress [0, 1]; routed to outputA
|
|
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
|
|
};
|
|
|
|
struct TunnelEntryTile
|
|
{
|
|
Rotation direction;
|
|
int maxDistance;
|
|
// front (highest progress) at index 0; back at end. Max 4.
|
|
std::vector<BeltItemSlot> itemSlots;
|
|
};
|
|
|
|
struct TunnelExitTile
|
|
{
|
|
Rotation direction;
|
|
// front (highest progress) at index 0; back at end. Max 4.
|
|
std::vector<BeltItemSlot> itemSlots;
|
|
};
|
|
|
|
struct TunnelTransitItem
|
|
{
|
|
Item item;
|
|
double progress;
|
|
};
|
|
|
|
struct TunnelLink
|
|
{
|
|
QPoint entryTile;
|
|
QPoint exitTile;
|
|
double length;
|
|
std::vector<TunnelTransitItem> items; // front (highest progress) to back
|
|
};
|
|
|
|
// Folds a run of item slots (front-to-back order is canonical) into the hasher.
|
|
static void appendItemSlots(Hasher& hasher, const std::vector<BeltItemSlot>& slotRun);
|
|
|
|
double m_progressPerTick_tpt; // beltSpeed_tps / kTickRateHz
|
|
|
|
std::map<std::pair<int, int>, BeltTile> m_belts;
|
|
std::map<std::pair<int, int>, SplitterTile> m_splitters;
|
|
std::map<std::pair<int, int>, TunnelEntryTile> m_tunnelEntries;
|
|
std::map<std::pair<int, int>, TunnelExitTile> m_tunnelExits;
|
|
std::vector<TunnelLink> m_tunnelLinks;
|
|
};
|
|
|