diff --git a/docs/architecture.md b/docs/architecture.md index 170733b..5a0db73 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -190,6 +190,10 @@ public: void forEachVisualItem(QRect viewportTiles, std::function visit) const; + // Inspection + std::map countItems( // REQ-UI-BELT-ITEMS + const std::vector& tiles) const; // transit items on the exit + // Determinism (docs/replay_design.md) void appendChecksum(Hasher& hasher) const; }; @@ -200,7 +204,7 @@ struct VisualItem { }; ``` -Item *transport* is still reached only through push and pull: `tryPutItem` / `tryTakeItem` move items, `peekItem` reveals the leading item's type but never an identity, and rendering reads only through `forEachVisualItem`. The growth is in tile **topology** — placement, removal and splitter filters — which `BuildingSystem` drives because belts are `Building`s for cost, construction and deconstruction. That coupling is real and is not going away. +Item *transport* is still reached only through push and pull: `tryPutItem` / `tryTakeItem` move items, `peekItem` reveals the leading item's type but never an identity, and rendering reads only through `forEachVisualItem`. `countItems` is a query of the same kind, added for the selection panel's item list (REQ-UI-BELT-ITEMS): it answers *how many of what* over a set of tiles and is deliberately a method rather than exposed tile containers, which would freeze the per-tile representation into its callers and cost the v2 migration described below. The growth is in tile **topology** — placement, removal and splitter filters — which `BuildingSystem` drives because belts are `Building`s for cost, construction and deconstruction. That coupling is real and is not going away. ### Implementation Strategy diff --git a/docs/requirements.md b/docs/requirements.md index 7b18139..02d9a3f 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -639,7 +639,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE Selections sharing a row of this table get the same content and differ only in the name and symbol in the header. A count in the right slot appears only for an aggregated multi-selection (REQ-UI-SELECTION-AGGREGATE); a single selection of those types shows an empty slot. - REQ-UI-SELECTION-STATUS: **Status indicator.** For a building whose production state is already rendered in the world as a status light (REQ-UI-STATUS-LIGHT) — Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay — the header's right slot repeats that same state as a colored dot with a short caption beside it, so the panel and the world never disagree. The state is derived from the evaluation defined in REQ-UI-STATUS-LIGHT rather than from a second definition, and the dot uses that state's fill color from `visuals.toml [status_light]`. The captions name the state: `no recipe` (grey), `producing` (green), `missing input` (red), `output full` (yellow); for the Salvage Bay, `holding scrap` (green) and `empty` (red). A selected **construction site** shows the caption `constructing` with no dot, whatever its type. Buildings with no status light — belts, splitters, tunnel ends, the HQ — show nothing in the slot. - REQ-UI-SELECTION-AGGREGATE: **Aggregating a homogeneous multi-selection.** When several objects are selected and their content can be shown as one — the same content, with its values aggregated over the whole selection — the panel shows that single content with the number of selected objects in the header's right slot (`x`), instead of the count summary of REQ-UI-MULTI-SELECTION / REQ-UI-FIELD-MULTI-SELECTION. This applies where every part of the **runtime** content aggregates. A **per-object configuration** does not stand in the way: it is simply not shown, there being no single object for it to configure. - - **Belt-subsystem tiles** — any mix of belts, tunnel entries, tunnel exits, and splitters. Their content is the item list and the clear action (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR), both of which already read and act on the whole selection. A splitter's output filters are per-object, so they are shown only when that splitter is the only selected object (REQ-BLD-SPLITTER); an aggregate holding one or more splitters shows no filters at all. The card is named after the type of the **first tile** in the selection, whatever the mix, and the count in the right slot says how many are held. + - **Belt-subsystem tiles** — any mix of belts, tunnel entries, tunnel exits, and splitters. Their content is the item list and the clear action (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR), both of which already read and act on the whole selection. A splitter's output filters are per-object, so they are shown only when that splitter is the only selected object (REQ-BLD-SPLITTER); an aggregate holding one or more splitters shows no filters at all. The card is named after the type of the **first tile** in the selection, whatever the mix, and the count in the right slot says how many are held. **Construction sites do not aggregate**: a selection holding one falls back to the count summary, a site having neither items on it nor a tile the belt subsystem knows (REQ-BLD-SITE-CONFIG). - **Debris** — several pieces of debris and nothing else. Their remaining scrap sums into one value (REQ-UI-DEBRIS-PANEL). Every other multi-selection falls back to the count summary. In particular several production buildings of one type — three miners, say — still do not aggregate, and the relaxation above does not change that: what stops them is their **runtime** content, not their recipe control. A buffer chip states a count against *that building's* capacity (`a / b`, REQ-MAT-OUTPUT-BUFFER) and a production section states *one* cycle's completion; summing three miners' ore into one chip would state a figure no buffer holds and no belt can draw, and averaging three cycle percentages would describe no building. Only where the runtime content is genuinely a property of the whole selection — items lying on a set of tiles, scrap remaining across a set of debris — is there one card to show. @@ -676,7 +676,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE - REQ-UI-BELT-ITEMS: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel's runtime group shows a captioned **`Items`** section stating what those tiles are carrying, above the clear action (REQ-UI-BELT-CLEAR) — the player reads what is there before removing it. Each item type present is shown as an **item chip** bearing the item's icon on its colored square (REQ-UI-ITEM-ICON) with the number of units held and the item's name below it, and hovering or clicking the chip shows that item's production tooltip (REQ-UI-ITEM-TOOLTIP) — the same chip the buffer sections and the HQ's block stock use (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL). This is what makes a belt's contents readable at all: on a moving line the items are too small and too transient to count by eye, and inside a tunnel they cannot be seen. - **Counts sum over the whole selection**, so there is one chip per item type however many tiles are held, matching the clear action's reach (REQ-UI-SELECTION-AGGREGATE). - **Items in transit through a tunnel** (REQ-BLD-TUNNEL-TRANSIT) are counted on the **tunnel exit**, that being where they will arrive; a selected tunnel entry counts only what is on its own tile. Selecting the exit is therefore how the player sees what a tunnel is currently swallowing, and how a tunnel is emptied (REQ-UI-BELT-CLEAR). - - Unlike a building's buffers, a belt has no fixed set of item types it can hold, so the section lists only the types actually present and shows no `0` chips. The chips keep the order the items are declared in config, so a chip does not jump about as counts change. When the selection carries nothing at all the section stays and reads the note `No items`, so the card holds one shape while items come and go. + - Unlike a building's buffers, a belt has no fixed set of item types it can hold, so the section lists only the types actually present and shows no `0` chips. The chips are ordered by **item id**, so a chip does not jump about as counts change — config declares no global item list to take an order from, items being named only where recipes and visuals use them. When the selection carries nothing at all the section stays and reads the note `No items`, so the card holds one shape while items come and go. - REQ-UI-BELT-CLEAR: Below that list, the panel's runtime group shows a **"Clear items"** button that removes all items from the selected tiles. It is named for what it does rather than for a state it assumes: the items on a belt need not be stuck, and emptying a line is just as often what the player wants of a line that is running. It removes exactly what the list above it shows, tile for tile: clearing a **tunnel exit** therefore also discards the items in transit through that tunnel, which are counted on the exit, while clearing a **tunnel entry** leaves them travelling (REQ-BLD-TUNNEL-TRANSIT, REQ-UI-BELT-ITEMS). This can be used to resolve stalled belts, splitters, and tunnels. The button acts on every selected tile, which is why any mix of belt-subsystem tiles aggregates into one content rather than a count summary (REQ-UI-SELECTION-AGGREGATE). Both the list and the button appear only where the selection holds **nothing but** belt-subsystem tiles. A selection mixing them with other buildings shows the count summary (REQ-UI-MULTI-SELECTION) and carries neither: a button that silently acted on part of a selection would be worse than no button, and the player can select the tiles alone. diff --git a/src/lib/sim/BeltSystem.cpp b/src/lib/sim/BeltSystem.cpp index b148c3e..c0fc705 100644 --- a/src/lib/sim/BeltSystem.cpp +++ b/src/lib/sim/BeltSystem.cpp @@ -408,14 +408,11 @@ void BeltSystem::clearTiles(const std::vector& tiles) 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(); - for (TunnelLink& link : m_tunnelLinks) - { - if (link.entryTile == tile) - { - link.items.clear(); - } - } } const std::map, TunnelExitTile>::iterator txIt = @@ -434,6 +431,83 @@ void BeltSystem::clearTiles(const std::vector& tiles) } } +std::map BeltSystem::countItems(const std::vector& tiles) const +{ + std::map 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, 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, 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, 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, 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 // --------------------------------------------------------------------------- diff --git a/src/lib/sim/BeltSystem.h b/src/lib/sim/BeltSystem.h index de4d730..8d70757 100644 --- a/src/lib/sim/BeltSystem.h +++ b/src/lib/sim/BeltSystem.h @@ -95,13 +95,26 @@ public: double getProgressPerTick_tpt() const { return m_progressPerTick_tpt; } // -- Maintenance --------------------------------------------------------- - void clearTiles(const std::vector& tiles); // REQ-UI-BELT-CLEAR + // 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& tiles); void tick(); // -- Rendering ----------------------------------------------------------- void forEachVisualItem(QRect viewportTiles, std::function 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 countItems(const std::vector& 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). diff --git a/src/lib/sim/FactoryQueries.cpp b/src/lib/sim/FactoryQueries.cpp index a89f533..5c63000 100644 --- a/src/lib/sim/FactoryQueries.cpp +++ b/src/lib/sim/FactoryQueries.cpp @@ -233,3 +233,25 @@ TunnelTileMap collectTunnelTiles(const FactoryState& state) } return tunnels; } + +std::vector collectBeltTiles(const FactoryState& state, + const std::vector& ids) +{ + std::vector tiles; + for (BuildingId id : ids) + { + // findBuilding fails for a construction site, which is what keeps a site out: + // its tile carries nothing and is not registered with the belt subsystem until + // it is built (REQ-BLD-SITE-CONFIG). + const Building* building = findBuilding(state, id); + if (!building || !isBeltSubsystemType(building->type)) + { + continue; + } + for (const QPoint& cell : building->bodyCells) + { + tiles.push_back(cell); + } + } + return tiles; +} diff --git a/src/lib/sim/FactoryQueries.h b/src/lib/sim/FactoryQueries.h index d026ca9..4d320d2 100644 --- a/src/lib/sim/FactoryQueries.h +++ b/src/lib/sim/FactoryQueries.h @@ -82,3 +82,11 @@ std::vector buildingsInBox(const FactoryState& state, // Every tunnel entry and exit, built or still a construction site, indexed by its // single-cell tile. Shared by the placement preview and the selection highlight. TunnelTileMap collectTunnelTiles(const FactoryState& state); + +// The tiles of the belt-subsystem buildings among the given ids, in the order the ids +// arrive; anything else in the selection is skipped, as are construction sites, whose +// tiles the belt subsystem does not know yet (REQ-BLD-SITE-CONFIG). Shared by the panel's +// item list and its clear action, so both speak of exactly the same tiles +// (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR). +std::vector collectBeltTiles(const FactoryState& state, + const std::vector& ids); diff --git a/src/test/BeltSystemTest.cpp b/src/test/BeltSystemTest.cpp index f52cf73..3cb6dc6 100644 --- a/src/test/BeltSystemTest.cpp +++ b/src/test/BeltSystemTest.cpp @@ -1,5 +1,6 @@ #include "catch.hpp" +#include #include #include #include @@ -327,6 +328,87 @@ TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]") REQUIRE(seen[0].id == "copper_ingot"); } +// --------------------------------------------------------------------------- +// countItems (REQ-UI-BELT-ITEMS) +// --------------------------------------------------------------------------- + +TEST_CASE("BeltSystem: countItems sums one item type over several tiles", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + const QPoint first(0, 0); + const QPoint second(1, 0); + bs.placeBelt(first, Rotation::East); + bs.placeBelt(second, Rotation::East); + bs.tryPutItem(first, makeItem("iron_ore"), Rotation::East); + bs.tryPutItem(second, makeItem("iron_ore"), Rotation::East); + bs.tryPutItem(second, makeItem("copper_ore"), Rotation::East); + + const std::map counts = bs.countItems({first, second}); + + REQUIRE(counts.size() == 2); + REQUIRE(counts.at(ItemType{"iron_ore"}) == 2); + REQUIRE(counts.at(ItemType{"copper_ore"}) == 1); +} + +TEST_CASE("BeltSystem: countItems ignores tiles outside the given selection", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + const QPoint selected(0, 0); + const QPoint other(1, 0); + bs.placeBelt(selected, Rotation::East); + bs.placeBelt(other, Rotation::East); + bs.tryPutItem(other, makeItem("iron_ore"), Rotation::East); + + REQUIRE(bs.countItems({selected}).empty()); +} + +TEST_CASE("BeltSystem: countItems counts a splitter's held items", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + const QPoint feed(0, 0); + const QPoint splitter(1, 0); + bs.placeBelt(feed, Rotation::East); + bs.placeSplitter(splitter, Rotation::North, Rotation::South); + + // One item routed onto an output slot, one still unassigned in the back. + bs.tryPutItem(splitter, makeItem("iron_ore"), Rotation::East); + bs.tick(); + bs.tryPutItem(splitter, makeItem("iron_ore"), Rotation::East); + + REQUIRE(bs.countItems({splitter}).at(ItemType{"iron_ore"}) == 2); +} + +TEST_CASE("BeltSystem: countItems counts transit items on the exit, not the entry", + "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + + const QPoint entry(0, 0); + const QPoint exit(5, 0); + + bs.placeTunnelEntry(entry, Rotation::East, 10); + bs.placeTunnelExit(exit, Rotation::East); + + bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::East); + bs.tick(); // item enters the entry's front slot + bs.tick(); // entry front -> transit + + // Invisible in the world and drawn nowhere, so the exit's count is the only place + // the player can see it (REQ-BLD-TUNNEL-TRANSIT). + REQUIRE(bs.countItems({entry}).empty()); + REQUIRE(bs.countItems({exit}).at(ItemType{"iron_ore"}) == 1); +} + +TEST_CASE("BeltSystem: countItems on empty tiles reports nothing", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + const QPoint tile(0, 0); + bs.placeBelt(tile, Rotation::East); + + REQUIRE(bs.countItems({tile}).empty()); + REQUIRE(bs.countItems({}).empty()); +} + // --------------------------------------------------------------------------- // Splitter — basic alternation (no filters) // --------------------------------------------------------------------------- @@ -1000,7 +1082,34 @@ TEST_CASE("BeltSystem: deconstruct entry discards transit items", "[belt]") REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value()); } -TEST_CASE("BeltSystem: clearTiles discards tunnel transit items", "[belt]") +// A clear removes exactly what countItems reports for the tile it acts on +// (REQ-UI-BELT-CLEAR, REQ-UI-BELT-ITEMS): the transit items belong to the exit, so only +// the exit's clear discards them. +TEST_CASE("BeltSystem: clearTiles on a tunnel exit discards transit items", "[belt]") +{ + BeltSystem bs(kFastBeltSpeed); + + const QPoint entry(0, 0); + const QPoint exit(5, 0); + + bs.placeTunnelEntry(entry, Rotation::East, 10); + bs.placeTunnelExit(exit, Rotation::East); + + bs.tryPutItem(entry, makeItem("iron_ore"), Rotation::East); + bs.tick(); + bs.tick(); + + bs.clearTiles({exit}); + + for (int i = 0; i < 30; ++i) + { + bs.tick(); + } + REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value()); +} + +TEST_CASE("BeltSystem: clearTiles on a tunnel entry leaves transit items travelling", + "[belt]") { BeltSystem bs(kFastBeltSpeed); @@ -1020,7 +1129,7 @@ TEST_CASE("BeltSystem: clearTiles discards tunnel transit items", "[belt]") { bs.tick(); } - REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value()); + REQUIRE(bs.peekItem(Port{exit, Rotation::East}).has_value()); } TEST_CASE("BeltSystem: belt to entry to transit to exit to belt full chain", "[belt]") diff --git a/src/ui/selection/BeltContent.cpp b/src/ui/selection/BeltContent.cpp index 40f75dc..4a086bd 100644 --- a/src/ui/selection/BeltContent.cpp +++ b/src/ui/selection/BeltContent.cpp @@ -2,6 +2,7 @@ #include +#include "BeltItemList.h" #include "BuildingTarget.h" #include "ClearBeltControl.h" #include "SelectionNames.h" @@ -17,6 +18,10 @@ BeltContent::BeltContent(const SelectionContext& context, parent) , m_ids(request.buildings) { + // What is there, then the button that removes it (REQ-UI-BELT-ITEMS, + // REQ-UI-BELT-CLEAR). + m_itemList = new BeltItemList(context, m_ids, this); + getRuntimeLayout()->addWidget(m_itemList); getRuntimeLayout()->addWidget(new ClearBeltControl(context, m_ids, this)); } @@ -28,11 +33,17 @@ void BeltContent::refreshConfiguration() return; } - // An aggregated selection may mix belts with tunnel ends, so it is named after the - // first tile; the count says how many are held (REQ-UI-SELECTION-AGGREGATE). + // An aggregated selection may mix belts with tunnel ends and splitters, so it is + // named after the first tile; the count says how many are held + // (REQ-UI-SELECTION-AGGREGATE). setBuildingIdentity(target.type, getBuildingTypeName(target.type)); if (m_ids.size() > 1) { setCountSlot(static_cast(m_ids.size())); } } + +void BeltContent::refreshRuntime() +{ + m_itemList->refresh(); +} diff --git a/src/ui/selection/BeltContent.h b/src/ui/selection/BeltContent.h index ba46b26..244e04a 100644 --- a/src/ui/selection/BeltContent.h +++ b/src/ui/selection/BeltContent.h @@ -6,13 +6,17 @@ #include "SelectionContent.h" #include "SelectionContentFactory.h" -// The card for a belt, a tunnel entry or a tunnel exit -// (REQ-UI-SELECTION-CONTENT). These carry no configuration and no buffers of their own; -// their card is the clear action alone (REQ-UI-BELT-CLEAR). +class BeltItemList; + +// The card for a belt, a tunnel entry or a tunnel exit (REQ-UI-SELECTION-CONTENT). These +// carry no configuration and no buffers of their own; their card is what the tiles are +// carrying with the clear action beneath it (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR). // -// Because that action already operates on the whole selection, several of them aggregate -// into this one card with the count in the header (REQ-UI-SELECTION-AGGREGATE) -- the -// splitter is not among them, as its output filters are per-object. +// Because both parts read and act on the whole selection, any mix of belt-subsystem tiles +// aggregates into this one card with the count in the header +// (REQ-UI-SELECTION-AGGREGATE) -- splitters among them, whose per-object output filters +// are simply not shown for an aggregate; a lone splitter gets its own card instead +// (SplitterContent). class BeltContent : public SelectionContent { Q_OBJECT @@ -23,8 +27,9 @@ public: protected: void refreshConfiguration() override; - void refreshRuntime() override {} + void refreshRuntime() override; private: std::vector m_ids; + BeltItemList* m_itemList; }; diff --git a/src/ui/selection/BeltItemList.cpp b/src/ui/selection/BeltItemList.cpp new file mode 100644 index 0000000..9be3c8b --- /dev/null +++ b/src/ui/selection/BeltItemList.cpp @@ -0,0 +1,63 @@ +#include "BeltItemList.h" + +#include +#include + +#include +#include + +#include "BeltSystem.h" +#include "DisplayName.h" +#include "EmptyNote.h" +#include "FactoryQueries.h" +#include "ItemChipRow.h" +#include "ItemType.h" +#include "SectionBox.h" +#include "Simulation.h" + +BeltItemList::BeltItemList(const SelectionContext& context, + const std::vector& ids, QWidget* parent) + : QWidget(parent) + , m_context(context) + , m_ids(ids) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + m_section = new SectionBox(tr("Items"), this); + m_chips = new ItemChipRow(m_context, m_section); + m_emptyNote = new EmptyNote(tr("No items"), m_section); + m_section->getContentLayout()->addWidget(m_chips); + m_section->getContentLayout()->addWidget(m_emptyNote); + layout->addWidget(m_section); + + refresh(); +} + +void BeltItemList::refresh() +{ + // Both the list and the clear action derive their tiles the same way, so what is + // listed is what the button removes (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR). + const std::vector tiles = + collectBeltTiles(m_context.sim->getFactoryState(), m_ids); + const std::map counts = m_context.sim->getBelts().countItems(tiles); + + // Keyed by item type, so the chips come out ordered by item id and a chip does not + // move as its count changes (REQ-UI-BELT-ITEMS). + std::vector entries; + entries.reserve(counts.size()); + for (const std::pair& count : counts) + { + ItemChipRow::Entry entry; + entry.itemId = count.first.id; + entry.countText = QString::number(count.second); + entry.subLine = QString::fromStdString(toDisplayName(count.first.id)); + entries.push_back(entry); + } + + // A belt has no fixed set of item types it can hold, unlike a building's buffers, so + // there is no chip to show as `0` and the note takes the section's place instead. + m_chips->setEntries(entries); + m_emptyNote->setVisible(entries.empty()); +} diff --git a/src/ui/selection/BeltItemList.h b/src/ui/selection/BeltItemList.h new file mode 100644 index 0000000..da16c35 --- /dev/null +++ b/src/ui/selection/BeltItemList.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include + +#include "BuildingId.h" +#include "SelectionContext.h" + +class EmptyNote; +class ItemChipRow; +class SectionBox; + +// What the selected belt-subsystem tiles are carrying (REQ-UI-BELT-ITEMS): one item chip +// per item type held across the whole selection, in the same form the buffer sections and +// the HQ's block stock use (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL). +// +// It stands above the clear action so the player reads a line's contents before emptying +// it, and it is the only place those contents 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 (REQ-BLD-TUNNEL-TRANSIT). +// +// Shared by the belt card and the splitter card, which show the same thing over different +// selections (REQ-UI-SELECTION-AGGREGATE). +class BeltItemList : public QWidget +{ + Q_OBJECT + +public: + BeltItemList(const SelectionContext& context, const std::vector& ids, + QWidget* parent = nullptr); + + // Re-reads what the tiles hold. Called per refresh of the owning card's runtime + // group, the counts changing as often as items move. + void refresh(); + +private: + SelectionContext m_context; + std::vector m_ids; + SectionBox* m_section; + ItemChipRow* m_chips; + // Shown in the chips' place while the tiles carry nothing, so the card holds one + // shape as items come and go (REQ-UI-BELT-ITEMS). + EmptyNote* m_emptyNote; +}; diff --git a/src/ui/selection/CMakeLists.txt b/src/ui/selection/CMakeLists.txt index 28ed646..3954230 100644 --- a/src/ui/selection/CMakeLists.txt +++ b/src/ui/selection/CMakeLists.txt @@ -17,6 +17,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h ${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h + ${CMAKE_CURRENT_SOURCE_DIR}/BeltItemList.h ${CMAKE_CURRENT_SOURCE_DIR}/BufferedBuildingContent.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeProductionContent.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipyardContent.h @@ -50,6 +51,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/BeltItemList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BufferedBuildingContent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RecipeProductionContent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipyardContent.cpp diff --git a/src/ui/selection/ClearBeltControl.cpp b/src/ui/selection/ClearBeltControl.cpp index 7dcd4ee..03f2335 100644 --- a/src/ui/selection/ClearBeltControl.cpp +++ b/src/ui/selection/ClearBeltControl.cpp @@ -4,7 +4,6 @@ #include #include -#include "Building.h" #include "Command.h" #include "CommandRequestedEvent.h" #include "EventManager.h" @@ -21,7 +20,7 @@ ClearBeltControl::ClearBeltControl(const SelectionContext& context, layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - QPushButton* button = new QPushButton(tr("Clear stuck items"), this); + QPushButton* button = new QPushButton(tr("Clear items"), this); layout->addWidget(button); connect(button, &QPushButton::clicked, this, [this]() { clearSelectedTiles(); }); @@ -29,18 +28,10 @@ ClearBeltControl::ClearBeltControl(const SelectionContext& context, void ClearBeltControl::clearSelectedTiles() const { - std::vector tiles; - for (BuildingId id : m_ids) - { - const Building* building = findBuilding(m_context.sim->getFactoryState(), id); - if (building && isBeltSubsystemType(building->type)) - { - for (const QPoint& cell : building->bodyCells) - { - tiles.push_back(cell); - } - } - } + // The same tiles the item list above states the contents of (REQ-UI-BELT-ITEMS), so + // the button cannot act on more or less than what the player was shown. + const std::vector tiles = + collectBeltTiles(m_context.sim->getFactoryState(), m_ids); if (tiles.empty()) { return; @@ -48,7 +39,7 @@ void ClearBeltControl::clearSelectedTiles() const std::shared_ptr command = std::make_shared(); - command->tiles = std::move(tiles); + command->tiles = tiles; EventManager::getInstance()->sendEventImmediately( std::make_shared(command)); } diff --git a/src/ui/selection/ClearBeltControl.h b/src/ui/selection/ClearBeltControl.h index 5a43b30..9ec0c80 100644 --- a/src/ui/selection/ClearBeltControl.h +++ b/src/ui/selection/ClearBeltControl.h @@ -7,14 +7,20 @@ #include "BuildingId.h" #include "SelectionContext.h" -// The "Clear stuck items" action of the card's runtime group (REQ-UI-BELT-CLEAR): it -// removes every item from the selected belt, splitter and tunnel tiles, which is how a -// stalled line is resolved. +// The "Clear items" action of the card's runtime group (REQ-UI-BELT-CLEAR): it removes +// every item from the selected belt, splitter and tunnel tiles -- how a stalled line is +// resolved, and equally how a running one is emptied, which is why the button is named +// for what it does rather than for a state the items need not be in. // -// It acts on the whole selection rather than on one tile, which is why a selection of -// belts and tunnel ends aggregates into a single card instead of a count summary -// (REQ-UI-SELECTION-AGGREGATE), and why the count summary shows the action too when a -// belt is among the selected buildings. +// It stands below the item list and removes exactly what that list shows +// (REQ-UI-BELT-ITEMS): a tunnel exit takes its tunnel's invisible cargo with it, a tunnel +// entry does not. +// +// It acts on the whole selection rather than on one tile, which is why any mix of +// belt-subsystem tiles aggregates into a single card instead of a count summary +// (REQ-UI-SELECTION-AGGREGATE). It is shown nowhere else: a selection mixing belt tiles +// with other buildings shows the count summary and carries neither the list nor this +// action, a button acting on part of a selection being worse than no button. class ClearBeltControl : public QWidget { Q_OBJECT diff --git a/src/ui/selection/MultiBuildingContent.cpp b/src/ui/selection/MultiBuildingContent.cpp index 5a0d5fe..9b1561f 100644 --- a/src/ui/selection/MultiBuildingContent.cpp +++ b/src/ui/selection/MultiBuildingContent.cpp @@ -6,7 +6,6 @@ #include "Building.h" #include "BuildingIconCache.h" -#include "ClearBeltControl.h" #include "CountRow.h" #include "FactoryQueries.h" #include "GameConfig.h" @@ -38,22 +37,11 @@ MultiBuildingContent::MultiBuildingContent(const SelectionContext& context, buildSummary(); - // A selection holding any belt-subsystem tile can still be cleared as a whole - // (REQ-UI-BELT-CLEAR), even though the mixture is what kept it from aggregating. - bool hasBeltTile = false; - for (BuildingId id : m_ids) - { - const Building* building = findBuilding(context.sim->getFactoryState(), id); - if (building && isBeltSubsystemType(building->type)) - { - hasBeltTile = true; - break; - } - } - if (hasBeltTile) - { - getRuntimeLayout()->addWidget(new ClearBeltControl(context, m_ids, this)); - } + // No item list and no clear action here, even when the selection holds belt tiles: + // both speak of belt-subsystem tiles alone, and a button acting on part of a + // selection would be worse than no button (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR). + // Selecting the tiles by themselves aggregates them into the belt card instead + // (REQ-UI-SELECTION-AGGREGATE). } void MultiBuildingContent::buildSummary() diff --git a/src/ui/selection/SelectionContentFactory.cpp b/src/ui/selection/SelectionContentFactory.cpp index 79a85a6..8098707 100644 --- a/src/ui/selection/SelectionContentFactory.cpp +++ b/src/ui/selection/SelectionContentFactory.cpp @@ -51,16 +51,6 @@ SelectionContentKind getKindForType(BuildingType type) return SelectionContentKind::MultiBuilding; } -// A belt, tunnel entry or tunnel exit -- the types whose card is the clear action alone -// and therefore aggregates (REQ-UI-SELECTION-AGGREGATE). The splitter is deliberately -// excluded: it carries per-object output filters, which have no aggregate. -bool isAggregatableBeltType(BuildingType type) -{ - return type == BuildingType::Belt - || type == BuildingType::TunnelEntry - || type == BuildingType::TunnelExit; -} - ContentKey chooseBuildingContent(const SelectionRequest& request, Simulation& sim) { const FactoryState& state = sim.getFactoryState(); @@ -83,21 +73,25 @@ ContentKey chooseBuildingContent(const SelectionRequest& request, Simulation& si return {}; } - // Several buildings aggregate into one card only when every part of that card - // aggregates (REQ-UI-SELECTION-AGGREGATE). Construction sites are excluded from the - // belt case: a site's card is its own construction progress, which several sites - // cannot share. - bool allAggregatableBelts = true; + // Several buildings aggregate into one card only when every part of that card's + // runtime content aggregates (REQ-UI-SELECTION-AGGREGATE). Belt-subsystem tiles do: + // the item list and the clear action already read and act on the whole selection + // (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR). Splitters are among them -- their output + // filters are per-object configuration, which an aggregate simply does not show. + // + // Construction sites are excluded: findBuilding fails for a site, whose card is its + // own construction progress and whose tile the belt subsystem does not know yet. + bool allBeltTiles = true; for (BuildingId id : request.buildings) { const Building* building = findBuilding(state, id); - if (!building || !isAggregatableBeltType(building->type)) + if (!building || !isBeltSubsystemType(building->type)) { - allAggregatableBelts = false; + allBeltTiles = false; break; } } - if (allAggregatableBelts) + if (allBeltTiles) { return { SelectionContentKind::Belt, false }; } diff --git a/src/ui/selection/SplitterContent.cpp b/src/ui/selection/SplitterContent.cpp index 60a754a..6b5b293 100644 --- a/src/ui/selection/SplitterContent.cpp +++ b/src/ui/selection/SplitterContent.cpp @@ -10,6 +10,7 @@ #include #include +#include "BeltItemList.h" #include "BeltSystem.h" #include "BuildingTarget.h" #include "ClearBeltControl.h" @@ -102,6 +103,10 @@ SplitterContent::SplitterContent(const SelectionContext& context, getConfigurationLayout()->addWidget(m_filterBLabel); getConfigurationLayout()->addWidget(m_filterBList); + // What the splitter is holding, then the button that removes it (REQ-UI-BELT-ITEMS, + // REQ-UI-BELT-CLEAR) -- the same pair the belt card shows, over one tile. + m_itemList = new BeltItemList(context, request.buildings, this); + getRuntimeLayout()->addWidget(m_itemList); getRuntimeLayout()->addWidget( new ClearBeltControl(context, request.buildings, this)); @@ -125,6 +130,11 @@ void SplitterContent::refreshConfiguration() setBuildingIdentity(target.type, getBuildingTypeName(target.type)); } +void SplitterContent::refreshRuntime() +{ + m_itemList->refresh(); +} + void SplitterContent::populateFilters() { const BuildingTarget target = resolveBuildingTarget(getContext(), m_id); diff --git a/src/ui/selection/SplitterContent.h b/src/ui/selection/SplitterContent.h index 5f85d93..3c8d95a 100644 --- a/src/ui/selection/SplitterContent.h +++ b/src/ui/selection/SplitterContent.h @@ -6,16 +6,22 @@ #include "SelectionContent.h" #include "SelectionContentFactory.h" +class BeltItemList; class QLabel; class QListWidget; -// The card for a Splitter (REQ-UI-SELECTION-CONTENT): its two per-output item filters -// (REQ-BLD-SPLITTER) plus the clear action every belt-subsystem tile has -// (REQ-UI-BELT-CLEAR). +// The card for a single selected Splitter (REQ-UI-SELECTION-CONTENT): its two per-output +// item filters (REQ-BLD-SPLITTER), then what it is holding and the clear action every +// belt-subsystem tile has (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR). +// +// Only for a splitter selected on its own. The filters are per-object, so a selection +// holding a splitter among other belt-subsystem tiles aggregates into BeltContent and +// shows none (REQ-UI-SELECTION-AGGREGATE). // // The filters are configuration, so they are shown for a construction site too and set -// through the site's own command (REQ-BLD-SITE-CONFIG); the clear action is runtime and -// therefore is not, because a site's tile is not registered with the belt subsystem yet. +// through the site's own command (REQ-BLD-SITE-CONFIG); the item list and the clear +// action are runtime and therefore are not, because a site's tile is not registered with +// the belt subsystem yet. class SplitterContent : public SelectionContent { Q_OBJECT @@ -26,7 +32,7 @@ public: protected: void refreshConfiguration() override; - void refreshRuntime() override {} + void refreshRuntime() override; private: // Sends the checked items of both lists as the splitter's new filters. Routed to the @@ -37,11 +43,12 @@ private: // the player is in the middle of clicking. void populateFilters(); - BuildingId m_id; - bool m_isSite; - QPoint m_tile; - QLabel* m_filterALabel; - QListWidget* m_filterAList; - QLabel* m_filterBLabel; - QListWidget* m_filterBList; + BuildingId m_id; + bool m_isSite; + QPoint m_tile; + BeltItemList* m_itemList; + QLabel* m_filterALabel; + QListWidget* m_filterAList; + QLabel* m_filterBLabel; + QListWidget* m_filterBList; };