migrate every factory query off BuildingSystem onto the free functions

This commit is contained in:
2026-08-05 06:45:49 +02:00
parent 1fb63cce4e
commit 58b94223f7
20 changed files with 212 additions and 244 deletions

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <cmath>
#include <random>
@@ -957,12 +958,12 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
for (int i = 0; i < 500; ++i)
{
f.buildings.tickConstruction(t++);
if (f.buildings.findBuilding(bayId) != nullptr)
if (findBuilding(f.state, bayId) != nullptr)
{
break;
}
}
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
REQUIRE(findBuilding(f.state, bayId) != nullptr);
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
@@ -992,9 +993,9 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
for (int i = 0; i < 500; ++i)
{
f.buildings.tickConstruction(t++);
if (f.buildings.findBuilding(bayId) != nullptr) { break; }
if (findBuilding(f.state, bayId) != nullptr) { break; }
}
const Building* bay = f.buildings.findBuilding(bayId);
const Building* bay = findBuilding(f.state, bayId);
REQUIRE(bay != nullptr);
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
REQUIRE(bay->outputBuffer.capacity == 20);
@@ -1015,7 +1016,7 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
// One unit handed over from cargo into the bay's output buffer.
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
const Building* bayAfter = f.buildings.findBuilding(bayId);
const Building* bayAfter = findBuilding(f.state, bayId);
REQUIRE(bayAfter != nullptr);
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
#include <climits>
@@ -529,9 +530,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
REQUIRE(idA != kInvalidBuildingId);
REQUIRE(idB != kInvalidBuildingId);
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(cursor)); // center not occupied
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetA)); // (-6, 0)
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), cursor)); // center not occupied
}
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
@@ -600,7 +601,7 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
REQUIRE_FALSE(id.has_value());
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
REQUIRE(sim.getBuildings().getAllSites().empty());
REQUIRE(getAllSites(sim.getFactoryState()).empty());
}
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
@@ -623,11 +624,11 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 0)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-2, 0)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 1)));
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 0)));
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 0)));
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 1)));
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(QPoint(-2, 1)));
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 1)));
}
// ---------------------------------------------------------------------------
@@ -667,7 +668,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const ConstructionSite* site = sim.getBuildings().findSite(id);
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
REQUIRE(site != nullptr);
REQUIRE(site->recipeId == "mine_iron_ore");
}
@@ -688,7 +689,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
sim.tick();
}
const Building* b = sim.getBuildings().findBuilding(id);
const Building* b = findBuilding(sim.getFactoryState(), id);
REQUIRE(b != nullptr);
REQUIRE(b->recipeId == "mine_copper_ore");
}
@@ -708,8 +709,8 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
@@ -744,14 +745,14 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
REQUIRE(idA != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
REQUIRE(sim.getBuildings().findBuilding(idA) != nullptr);
REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);
// Building B: place and configure, but leave as a construction site.
const BuildingId idB =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
REQUIRE(idB != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
REQUIRE(sim.getBuildings().findSite(idB) != nullptr);
REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
@@ -775,7 +776,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
}
@@ -855,7 +856,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
const ConstructionSite* site = sim.getBuildings().findSite(id);
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
REQUIRE(site != nullptr);
REQUIRE(site->shipLayout.has_value());
REQUIRE(site->shipLayout->placedModules.size() == 1);
@@ -892,7 +893,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
sim.tick();
}
const Building* b = sim.getBuildings().findBuilding(id);
const Building* b = findBuilding(sim.getFactoryState(), id);
REQUIRE(b != nullptr);
REQUIRE(b->shipLayout.has_value());
REQUIRE(b->shipLayout->placedModules.size() == 1);

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
@@ -122,8 +123,8 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <map>
#include <random>
@@ -125,11 +126,11 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
REQUIRE(id != kInvalidBuildingId);
// Miner mask ["AA","A>"] with East rotation → body at (0,0),(1,0),(0,1).
REQUIRE(bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(bs.isTileOccupied(QPoint(1, 0)));
REQUIRE(bs.isTileOccupied(QPoint(0, 1)));
REQUIRE(isTileOccupied(state_bs, QPoint(0, 0)));
REQUIRE(isTileOccupied(state_bs, QPoint(1, 0)));
REQUIRE(isTileOccupied(state_bs, QPoint(0, 1)));
// (1,1) is the output-port tile, NOT a body cell.
REQUIRE_FALSE(bs.isTileOccupied(QPoint(1, 1)));
REQUIRE_FALSE(isTileOccupied(state_bs, QPoint(1, 1)));
}
// -- World-bounds rejection (REQ-BLD-PLACE-VALID) ---------------------------
@@ -142,8 +143,8 @@ TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[
// row sits above the world.
const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0);
REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty());
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(getAllSites(f.state).empty());
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0)));
}
TEST_CASE("BuildingSystem: place rejects a building below the world (y >= height)", "[building]")
@@ -156,7 +157,7 @@ TEST_CASE("BuildingSystem: place rejects a building below the world (y >= height
const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner,
QPoint(0, heightTiles - 1), Rotation::East, 0);
REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty());
REQUIRE(getAllSites(f.state).empty());
}
TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge", "[building]")
@@ -167,7 +168,7 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge",
const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX - 1, 0), Rotation::East, 0);
REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty());
REQUIRE(getAllSites(f.state).empty());
}
TEST_CASE("BuildingSystem: place accepts a building flush against the world's left edge",
@@ -180,7 +181,7 @@ TEST_CASE("BuildingSystem: place accepts a building flush against the world's le
const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX, 0), Rotation::East, 0).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(f.bs.isTileOccupied(QPoint(leftEdgeX, 0)));
REQUIRE(isTileOccupied(f.state, QPoint(leftEdgeX, 0)));
}
TEST_CASE("BuildingSystem: place imposes no right-side bound (space extends rightward)",
@@ -240,9 +241,9 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
REQUIRE(bs.getAllBuildings().size() == 1);
REQUIRE(bs.getAllBuildings()[0].type == BuildingType::Belt);
REQUIRE(bs.getAllBuildings()[0].anchor == QPoint(5, 5));
REQUIRE(getAllBuildings(state_bs).size() == 1);
REQUIRE(getAllBuildings(state_bs)[0].type == BuildingType::Belt);
REQUIRE(getAllBuildings(state_bs)[0].anchor == QPoint(5, 5));
}
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
@@ -262,9 +263,9 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.getAllBuildings().empty());
REQUIRE(bs.findSite(id) != nullptr);
REQUIRE(getAllSites(state_bs).size() == 1);
REQUIRE(getAllBuildings(state_bs).empty());
REQUIRE(findSite(state_bs, id) != nullptr);
}
TEST_CASE("BuildingSystem: deconstructing a construction site removes it instantly with full refund",
@@ -279,8 +280,8 @@ TEST_CASE("BuildingSystem: deconstructing a construction site removes it instant
const int refund = f.bs.deconstruct(id, 0);
REQUIRE(refund == 15); // Miner cost = 15
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(f.bs.getAllSites().empty());
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0)));
REQUIRE(getAllSites(f.state).empty());
}
// ---------------------------------------------------------------------------
@@ -304,7 +305,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
rng);
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
REQUIRE(bs.getAllSites().front().completesAt > 0);
REQUIRE(getAllSites(state_bs).front().completesAt > 0);
}
TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]")
@@ -325,9 +326,9 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0);
REQUIRE(bs.getAllSites().size() == 2);
REQUIRE(bs.getAllSites()[0].completesAt > 0);
REQUIRE(bs.getAllSites()[1].completesAt == 0);
REQUIRE(getAllSites(state_bs).size() == 2);
REQUIRE(getAllSites(state_bs)[0].completesAt > 0);
REQUIRE(getAllSites(state_bs)[1].completesAt == 0);
}
TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]")
@@ -352,8 +353,8 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.getAllSites().empty());
REQUIRE(bs.findBuilding(id) != nullptr);
REQUIRE(getAllSites(state_bs).empty());
REQUIRE(findBuilding(state_bs, id) != nullptr);
}
// ---------------------------------------------------------------------------
@@ -363,11 +364,11 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
// Runs ticks until the building with the given id is operational, or fails.
static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick)
{
for (int i = 0; i < 100000 && f.bs.findBuilding(id) == nullptr; ++i)
for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i)
{
runTicks(f.bs, f.belts, 1, tick);
}
REQUIRE(f.bs.findBuilding(id) != nullptr);
REQUIRE(findBuilding(f.state, id) != nullptr);
}
TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund credited on completion",
@@ -384,15 +385,15 @@ TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund cre
// stopping it operating while its tiles stay occupied (REQ-BLD-DECON-QUEUE).
const int refund = f.bs.deconstruct(id, tick);
REQUIRE(refund == 0);
REQUIRE(f.bs.isQueuedForDeconstruction(id));
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(isQueuedForDeconstruction(f.state, id));
REQUIRE(isTileOccupied(f.state, QPoint(0, 0)));
REQUIRE(f.stock == 0);
// After the deconstruction time (0.1s = 3 ticks) it is removed and the partial
// refund (15 * 75 / 100 = 11) is credited exactly once.
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
REQUIRE(f.bs.findBuilding(id) == nullptr);
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(findBuilding(f.state, id) == nullptr);
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0)));
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
}
@@ -409,20 +410,20 @@ TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time",
// Queue both in one tick; 'a' is at the front of the deconstruction queue.
f.bs.deconstruct(a, tick);
f.bs.deconstruct(b, tick);
REQUIRE(f.bs.isQueuedForDeconstruction(a));
REQUIRE(f.bs.isQueuedForDeconstruction(b));
REQUIRE(isQueuedForDeconstruction(f.state, a));
REQUIRE(isQueuedForDeconstruction(f.state, b));
// After one deconstruction interval only the front building is gone; the
// second is still queued and its refund not yet credited.
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
REQUIRE(f.bs.findBuilding(a) == nullptr);
REQUIRE(f.bs.findBuilding(b) != nullptr);
REQUIRE(f.bs.isQueuedForDeconstruction(b));
REQUIRE(findBuilding(f.state, a) == nullptr);
REQUIRE(findBuilding(f.state, b) != nullptr);
REQUIRE(isQueuedForDeconstruction(f.state, b));
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
// The second drains next.
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
REQUIRE(f.bs.findBuilding(b) == nullptr);
REQUIRE(findBuilding(f.state, b) == nullptr);
REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100));
}
@@ -437,18 +438,18 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n
runUntilBuilt(f, id, tick);
f.bs.deconstruct(id, tick);
REQUIRE(f.bs.isQueuedForDeconstruction(id));
REQUIRE(isQueuedForDeconstruction(f.state, id));
// Un-queue before it drains: it operates again, no refund, tiles still occupied.
f.bs.cancelDeconstruction(id);
REQUIRE_FALSE(f.bs.isQueuedForDeconstruction(id));
REQUIRE(f.bs.findBuilding(id) != nullptr);
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
REQUIRE_FALSE(isQueuedForDeconstruction(f.state, id));
REQUIRE(findBuilding(f.state, id) != nullptr);
REQUIRE(isTileOccupied(f.state, QPoint(0, 0)));
REQUIRE(f.stock == 0);
// It is never removed even after more than a deconstruction interval passes.
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
REQUIRE(f.bs.findBuilding(id) != nullptr);
REQUIRE(findBuilding(f.state, id) != nullptr);
REQUIRE(f.stock == 0);
}
@@ -466,7 +467,7 @@ TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it",
// accepts or transports items, though the tile stays occupied (REQ-BLD-DECON-QUEUE).
f.bs.deconstruct(id, tick);
REQUIRE_FALSE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
REQUIRE(isTileOccupied(f.state, QPoint(0, 0)));
// Un-queuing re-registers the belt tile so it transports again.
f.bs.cancelDeconstruction(id);
@@ -519,9 +520,9 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.getAllSites().front().id == id2);
REQUIRE(bs.getAllSites().front().completesAt > 0);
REQUIRE(getAllSites(state_bs).size() == 1);
REQUIRE(getAllSites(state_bs).front().id == id2);
REQUIRE(getAllSites(state_bs).front().completesAt > 0);
}
// ---------------------------------------------------------------------------
@@ -553,7 +554,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
tick);
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
// No belt at the output port, so the produced item emerges and stays on the
// building's virtual output belt (REQ-MAT-OUTPUT-EMERGE).
@@ -591,7 +592,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 2,
tick);
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
// Both produced items are held on the output side (buffer + emerging lane),
// which is what the capacity rule counts (REQ-MAT-OUTPUT-EMERGE).
@@ -624,23 +625,23 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
Tick tick = 0;
// Both still under construction.
REQUIRE(bs.getProductionBuildingCount() == 0);
REQUIRE(getProductionBuildingCount(state_bs) == 0);
// The queue builds one at a time: miner (10s) completes at tick 300, then
// the smelter (15s) starts and completes at tick 300 + 450 = 750.
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.getProductionBuildingCount() == 1);
REQUIRE(getProductionBuildingCount(state_bs) == 1);
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
REQUIRE(bs.getProductionBuildingCount() == 2);
REQUIRE(getProductionBuildingCount(state_bs) == 2);
// Neither is producing yet: the miner has no recipe selected, and the
// smelter (auto-recipe, REQ-BLD-SMELTER) has no input feeding it.
REQUIRE(bs.getActiveProductionBuildingCount() == 0);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 0);
bs.setRecipe(minerId, "mine_iron_ore");
runTicks(bs, belts, 1, tick);
REQUIRE(bs.getActiveProductionBuildingCount() == 1);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 1);
}
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
@@ -664,21 +665,21 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
Tick tick = 0;
// Not yet operational while under construction.
REQUIRE(bs.getActiveProductionBuildingCount() == 0);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 0);
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(bs.getActiveProductionBuildingCount() == 1);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 1);
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
runTicks(bs, belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
REQUIRE(b->getOutputItemCount() == 2);
REQUIRE_FALSE(b->production.has_value());
REQUIRE(bs.getActiveProductionBuildingCount() == 0);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 0);
}
// ---------------------------------------------------------------------------
@@ -719,7 +720,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
bs.tickBeltPull();
const Building* b = bs.findBuilding(sid);
const Building* b = findBuilding(state_bs, sid);
REQUIRE(b != nullptr);
// The item was accepted; it may still be travelling inward on the input belt,
// so count buffered + in-transit (REQ-MAT-INPUT-INTAKE).
@@ -754,7 +755,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
belts.tick();
bs.tickBeltPull(); // accepts the item onto the input belt at progress 0.0
const Building* b = bs.findBuilding(sid);
const Building* b = findBuilding(state_bs, sid);
REQUIRE(b != nullptr);
// Reserved but not yet consumable: nothing in the buffer, but it counts against
// the cap via pendingInputCount.
@@ -802,7 +803,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
bs.tickBeltPull();
}
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
const int cap = b->inputBuffer.caps.at(ItemType{"scrap"});
REQUIRE(cap > 0);
@@ -848,7 +849,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
// iron_ingot recipe cycle is 2s; run to completion.
runTicks(bs, belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
const Building* b = bs.findBuilding(sid);
const Building* b = findBuilding(state_bs, sid);
REQUIRE(b != nullptr);
bool hasIronIngot = false;
for (const Item& item : outputSideItems(*b))
@@ -896,7 +897,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
// copper_ingot cycle is 2.5s; run to completion.
runTicks(bs, belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
const Building* b = bs.findBuilding(sid);
const Building* b = findBuilding(state_bs, sid);
REQUIRE(b != nullptr);
// Copper was smelted; the lone iron_ore still waits for a second unit.
@@ -984,7 +985,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p
// Smelter build (15s) + margin for coupling and a smelt cycle.
runTicks(bs, belts, static_cast<int>(secondsToTicks(30.0)), tick);
const Building* smelter = bs.findBuilding(smelterId);
const Building* smelter = findBuilding(state_bs, smelterId);
REQUIRE(smelter != nullptr);
// iron_ore reached the smelter over the direct coupling and was smelted.
bool hasIronIngot = false;
@@ -1024,8 +1025,8 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
// Both miners build sequentially (10s each), then the producer runs and jams.
runTicks(bs, belts, static_cast<int>(secondsToTicks(25.0)), tick);
const Building* miner = bs.findBuilding(minerId);
const Building* sink = bs.findBuilding(sinkId);
const Building* miner = findBuilding(state_bs, minerId);
const Building* sink = findBuilding(state_bs, sinkId);
REQUIRE(miner != nullptr);
REQUIRE(sink != nullptr);
// Nothing was delivered, and the producer's output side has backed up to its cap.
@@ -1063,14 +1064,14 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
tick);
{
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
REQUIRE(b->getOutputItemCount() > 0);
}
bs.setRecipe(id, "mine_copper_ore");
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
// Clearing the output buffer on a recipe change also discards emerging items
// (REQ-MAT-OUTPUT-EMERGE).
REQUIRE(b->getOutputItemCount() == 0);
@@ -1106,7 +1107,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
// reprocessing_cycle outputs: 2 iron_ingot (60%), 1 circuit_board (30%),
// 1 advanced_alloy (10%). Max per roll = 2. Capacity = 2 (1× max).
@@ -1153,7 +1154,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
// Verify all five scrap were accepted; some may still be travelling inward on
// the input belt (REQ-MAT-INPUT-INTAKE), so count buffered + in-transit.
{
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
REQUIRE(b->pendingInputCount(ItemType{"scrap"}) == 5);
}
@@ -1161,7 +1162,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
runTicks(bs, belts, static_cast<int>(secondsToTicks(3.0)) + 1, tick);
const Building* b = bs.findBuilding(id);
const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr);
// Cycle should have completed and output deposited.
REQUIRE_FALSE(b->outputBuffer.items.empty());
@@ -1237,7 +1238,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(bs.getAllSites().empty());
REQUIRE(getAllSites(state_bs).empty());
const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::South);
@@ -1367,11 +1368,11 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
REQUIRE(bs.findSite(id)->rotation == Rotation::East);
REQUIRE(findSite(state_bs, id)->rotation == Rotation::East);
bs.rotateInPlace(id, Rotation::North);
REQUIRE(bs.findSite(id)->rotation == Rotation::North);
REQUIRE(findSite(state_bs, id)->rotation == Rotation::North);
}
TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of a queued site",
@@ -1391,12 +1392,12 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
const Tick completesAt = bs.findSite(id)->completesAt;
const Tick completesAt = findSite(state_bs, id)->completesAt;
REQUIRE(completesAt > 0);
bs.rotateInPlace(id, Rotation::South);
REQUIRE(bs.findSite(id)->completesAt == completesAt);
REQUIRE(findSite(state_bs, id)->completesAt == completesAt);
}
TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direction on an operational building",
@@ -1419,14 +1420,14 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(bs.findBuilding(id) != nullptr);
REQUIRE(findBuilding(state_bs, id) != nullptr);
const Building& before = *bs.findBuilding(id);
const Building& before = *findBuilding(state_bs, id);
REQUIRE(before.outputPorts[0].direction == Rotation::East);
bs.rotateInPlace(id, Rotation::North);
const Building& after = *bs.findBuilding(id);
const Building& after = *findBuilding(state_bs, id);
REQUIRE(after.rotation == Rotation::North);
REQUIRE(after.outputPorts[0].direction == Rotation::North);
}
@@ -1468,11 +1469,11 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split
// Run until construction completes, so the splitter is registered with BeltSystem.
Tick tick = 0;
while (f.bs.getAllBuildings().empty() && tick < 100000)
while (getAllBuildings(f.state).empty() && tick < 100000)
{
runTicks(f.bs, f.belts, 1, tick);
}
REQUIRE(f.bs.getAllBuildings().size() == 1);
REQUIRE(getAllBuildings(f.state).size() == 1);
const std::vector<ItemType> filterA{ ItemType{"iron_ore"} };
const std::vector<ItemType> filterB{ ItemType{"copper_ore"} };
@@ -1496,7 +1497,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
const QPoint tile(5, 5);
const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(f.bs.findSite(id) != nullptr);
REQUIRE(findSite(f.state, id) != nullptr);
// Configure an output filter on the still-queued splitter site.
const std::vector<ItemType> filterA{ ItemType{"iron_ore"} };
@@ -1513,12 +1514,12 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
// Run until construction completes.
Tick tick = 0;
while (f.bs.getAllBuildings().empty() && tick < 100000)
while (getAllBuildings(f.state).empty() && tick < 100000)
{
runTicks(f.bs, f.belts, 1, tick);
}
REQUIRE(f.bs.getAllBuildings().size() == 1);
REQUIRE(f.bs.getAllBuildings()[0].type == BuildingType::Splitter);
REQUIRE(getAllBuildings(f.state).size() == 1);
REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter);
// The built splitter is registered with BeltSystem carrying the filters.
const std::optional<BeltSystem::SplitterInfo> builtInfo = f.belts.getSplitterInfo(tile);
@@ -1669,10 +1670,10 @@ namespace
// Advances the sim until the given site becomes an operational building, or a
// safety cap is reached.
void buildToCompletion(BuildingSystem& bs, BeltSystem& belts, BuildingId id,
Tick& tick)
void buildToCompletion(BuildingSystem& bs, const FactoryState& state,
BeltSystem& belts, BuildingId id, Tick& tick)
{
for (int i = 0; i < 20000 && bs.findBuilding(id) == nullptr; ++i)
for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i)
{
runTicks(bs, belts, 1, tick);
}
@@ -1709,8 +1710,8 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
const std::vector<Port> sitePorts = f.bs.getInputPorts(id);
buildToCompletion(f.bs, f.belts, id, tick);
REQUIRE(f.bs.findBuilding(id) != nullptr);
buildToCompletion(f.bs, f.state, f.belts, id, tick);
REQUIRE(findBuilding(f.state, id) != nullptr);
const std::vector<Port> builtPorts = f.bs.getInputPorts(id);
// The operational path (stored inputPorts) agrees with the site path (mask-derived).
@@ -1725,7 +1726,7 @@ TEST_CASE("BuildingSystem: getInputPorts invariants hold for a rotated site", "[
{
PlacementFixture f;
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::South, 0).value();
const ConstructionSite* site = f.bs.findSite(id);
const ConstructionSite* site = findSite(f.state, id);
REQUIRE(site != nullptr);
std::set<std::pair<int, int>> bodySet;

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include "Building.h"
#include "BuildingSystem.h"
@@ -218,7 +219,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b = sim.getBuildings().findBuilding(yardId);
const Building* b = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b != nullptr);
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
@@ -243,7 +244,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
fillMaterials(sim, yardId, *def, emptyLayout);
sim.tick();
const Building* b1 = sim.getBuildings().findBuilding(yardId);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->production.has_value());
@@ -257,7 +258,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b2 = sim.getBuildings().findBuilding(yardId);
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->production.has_value());
}
@@ -323,13 +324,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = sim.getBuildings().findBuilding(yardId);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value());
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
const Building* b2 = sim.getBuildings().findBuilding(yardId);
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->shipLayout.has_value());
}
@@ -352,14 +353,14 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = sim.getBuildings().findBuilding(yardId);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value());
// Re-selecting the same recipe must be a no-op and preserve the layout.
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
const Building* b2 = sim.getBuildings().findBuilding(yardId);
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b2 != nullptr);
REQUIRE(b2->shipLayout.has_value());
REQUIRE(b2->shipLayout->placedModules.size() == 1);

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
@@ -214,7 +215,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
// Verify the shipyard production field cleared (i.e. the cycle completed
// and is not still running).
bool productionCleared = false;
for (const Building& b : sim.getBuildings().getAllBuildings())
for (const Building& b : getAllBuildings(sim.getFactoryState()))
{
if (b.id == yardId)
{

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <random>
@@ -100,7 +101,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
// HQ is still a Building (for belt integration).
int hqCount = 0;
for (const Building& b : sim.getBuildings().getAllBuildings())
for (const Building& b : getAllBuildings(sim.getFactoryState()))
{
if (b.type == BuildingType::Hq) { ++hqCount; }
}
@@ -143,7 +144,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
{
const Simulation sim(loadTestConfig(), 42);
for (const Building& b : sim.getBuildings().getAllBuildings())
for (const Building& b : getAllBuildings(sim.getFactoryState()))
{
if (b.type != BuildingType::Hq) { continue; }
// Rightmost body cell must be at x = -1 (asteroid right edge).