From a90218f5c0a53c0f20772cd726371319540667c9 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Tue, 4 Aug 2026 23:00:09 +0200 Subject: [PATCH] make BuildingSystem stateless: FactoryState becomes a parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The member reference is gone. All 23 methods that read or write the factory now take FactoryState& (const for the two item walks and the checksum fold), so a BuildingSystem is no longer bound to one state and its signatures say which data each call touches. It holds only config, belts, rng and the callbacks — the same shape as AiSystem and CombatSystem. This completes what phase 2 set out to do; the ownership move landed earlier, but the systems kept reaching the data through a member until the queries were off them. Seeding the asteroid bound moved with the state, and that broke four tests: the fixtures build their own FactoryState, which defaulted the bound to 0 and refused every placement on the asteroid. Rather than fix the four call sites, makeFactoryState() now creates a run's state from the config, and Simulation, ArenaSimulation and the test fixtures all use it — there is one place that knows what a fresh factory looks like. Verified with a golden-checksum capture before and after — all four sample ticks identical — and by re-running the declaration/definition check over the header. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG --- src/balancing/ArenaSimulation.cpp | 11 +- src/lib/sim/BuildingSystem.cpp | 183 +++++++------- src/lib/sim/BuildingSystem.h | 53 ++-- src/lib/sim/FactoryState.h | 12 + src/lib/sim/Simulation.cpp | 53 ++-- src/test/BehaviorSystemTest.cpp | 12 +- src/test/BlueprintTest.cpp | 14 +- src/test/BuildingConfigTest.cpp | 11 +- src/test/BuildingTest.cpp | 397 +++++++++++++++--------------- src/test/CombatSystemTest.cpp | 4 +- src/test/CommandTest.cpp | 2 +- src/test/ShipModuleTest.cpp | 27 +- src/test/ShipyardTest.cpp | 11 +- src/test/SimulationTestAccess.h | 2 + src/ui/GameWorldView.cpp | 4 +- 15 files changed, 401 insertions(+), 395 deletions(-) diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index fc5da88..bf944b5 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -46,9 +46,10 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig, , m_finished(false) , m_stopRequested(false) { + m_factoryState = makeFactoryState(m_gameConfig); + m_buildingSystem = std::make_unique( m_gameConfig, - m_factoryState, m_beltSystem, [this]() { return allocateBuildingId(); }, [](int) {}, @@ -163,7 +164,7 @@ void ArenaSimulation::placeStructures() hp, hp, false); // Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR). m_admin.addComponent(m_team1HqEntity); - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } // Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon. @@ -184,7 +185,7 @@ void ArenaSimulation::placeStructures() hp, hp, true); // Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR). m_admin.addComponent(m_team2HqEntity); - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy) @@ -238,7 +239,7 @@ void ArenaSimulation::placeStructures() m_admin.addComponent(wChild, ModuleOwnerComponent{stationEntity}); } - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); }; for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations) @@ -393,7 +394,7 @@ void ArenaSimulation::tickDeaths() for (entt::entity deadEntity : deadStations) { const StationBodyComponent& sb = m_admin.get(deadEntity); - m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); + m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells); { std::vector stationChildren; m_admin.forEach( diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 0052d00..36a5c68 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -26,7 +26,6 @@ bool inputLaneEntryFree(const std::vector& lane) } // namespace BuildingSystem::BuildingSystem(const GameConfig& config, - FactoryState& state, BeltSystem& belts, std::function allocateBuildingId, std::function addBuildingBlocks, @@ -35,7 +34,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config, std::function isItemUnlocked, std::mt19937& rng) : m_config(config) - , m_state(state) , m_belts(belts) , m_allocateBuildingId(std::move(allocateBuildingId)) , m_addBuildingBlocks(std::move(addBuildingBlocks)) @@ -43,7 +41,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config, , m_isItemUnlocked(std::move(isItemUnlocked)) , m_rng(rng) { - m_state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles; } // --------------------------------------------------------------------------- @@ -213,7 +210,7 @@ std::vector BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe // Placement // --------------------------------------------------------------------------- -std::optional BuildingSystem::place(BuildingType type, QPoint anchor, +std::optional BuildingSystem::place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation, Tick currentTick) { const BuildingDef* def = m_config.buildings.findBuildingDef(type); @@ -221,7 +218,7 @@ std::optional BuildingSystem::place(BuildingType type, QPoint anchor const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation); // Reject placements that fall outside the world (REQ-BLD-PLACE-VALID). - if (!bodyCellsWithinWorldBounds(m_state, m_config, mask.bodyCells, anchor)) + if (!bodyCellsWithinWorldBounds(state, m_config, mask.bodyCells, anchor)) { return std::nullopt; } @@ -232,7 +229,7 @@ std::optional BuildingSystem::place(BuildingType type, QPoint anchor for (const QPoint& cell : mask.bodyCells) { const QPoint absCell = anchor + cell; - m_state.grid.occupy(absCell, id); + state.grid.occupy(absCell, id); } // Build construction site. @@ -247,13 +244,13 @@ std::optional BuildingSystem::place(BuildingType type, QPoint anchor site.bodyCells.push_back(anchor + cell); } - if (m_state.constructionQueue.empty()) + if (state.constructionQueue.empty()) { site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds); } // else: completesAt remains 0 (queued, not yet started). - m_state.constructionQueue.push_back(std::move(site)); + state.constructionQueue.push_back(std::move(site)); return id; } @@ -261,19 +258,19 @@ std::optional BuildingSystem::place(BuildingType type, QPoint anchor // Deconstruct // --------------------------------------------------------------------------- -int BuildingSystem::deconstruct(BuildingId id, Tick currentTick) +int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick currentTick) { // Construction site? Removed instantly with the full refund; never queued // for deconstruction (REQ-BLD-DECONSTRUCT). - for (std::deque::iterator it = m_state.constructionQueue.begin(); - it != m_state.constructionQueue.end(); + for (std::deque::iterator it = state.constructionQueue.begin(); + it != state.constructionQueue.end(); ++it) { if (it->id == id) { const BuildingDef* def = m_config.buildings.findBuildingDef(it->type); - m_state.grid.release(it->bodyCells); - m_state.constructionQueue.erase(it); + state.grid.release(it->bodyCells); + state.constructionQueue.erase(it); if (def) { return def->cost; @@ -285,7 +282,7 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick) // Operational building? Append it to the deconstruction queue rather than // removing it now; the partial refund is credited on completion in // tickDeconstruction (REQ-BLD-DECON-QUEUE). - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { if (building.id != id) { continue; } if (building.queuedForDeconstruction) { return 0; } // already queued @@ -313,11 +310,11 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick) m_belts.removeTile(building.anchor); } - const bool wasEmpty = m_state.deconstructionQueue.empty(); - m_state.deconstructionQueue.push_back(std::move(entry)); + const bool wasEmpty = state.deconstructionQueue.empty(); + state.deconstructionQueue.push_back(std::move(entry)); if (wasEmpty) { - startFrontDeconstruction(currentTick); + startFrontDeconstruction(state, currentTick); } return 0; } @@ -325,10 +322,10 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick) return 0; } -void BuildingSystem::startFrontDeconstruction(Tick currentTick) +void BuildingSystem::startFrontDeconstruction(FactoryState& state, Tick currentTick) { - if (m_state.deconstructionQueue.empty()) { return; } - DeconstructionEntry& front = m_state.deconstructionQueue.front(); + if (state.deconstructionQueue.empty()) { return; } + DeconstructionEntry& front = state.deconstructionQueue.front(); if (front.completesAt == 0) { front.completesAt = @@ -340,10 +337,10 @@ void BuildingSystem::startFrontDeconstruction(Tick currentTick) // Set recipe // --------------------------------------------------------------------------- -void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId) +void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId) { // Construction site: store recipe for when building completes. - for (ConstructionSite& site : m_state.constructionQueue) + for (ConstructionSite& site : state.constructionQueue) { if (site.id == id) { @@ -366,7 +363,7 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId) } // Operational building: clear buffers and re-init. - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { if (building.id == id) { @@ -416,9 +413,9 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId) } } -void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout) +void BuildingSystem::setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout) { - for (ConstructionSite& site : m_state.constructionQueue) + for (ConstructionSite& site : state.constructionQueue) { if (site.id == id) { @@ -427,7 +424,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout } } - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { if (building.id == id) { @@ -451,11 +448,11 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout } } -void BuildingSystem::setSiteSplitterFilters(BuildingId id, +void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id, const std::vector& filterA, const std::vector& filterB) { - for (ConstructionSite& site : m_state.constructionQueue) + for (ConstructionSite& site : state.constructionQueue) { if (site.id == id && site.type == BuildingType::Splitter) { @@ -470,15 +467,15 @@ void BuildingSystem::setSiteSplitterFilters(BuildingId id, // Tick hooks // --------------------------------------------------------------------------- -void BuildingSystem::tickConstruction(Tick currentTick) +void BuildingSystem::tickConstruction(FactoryState& state, Tick currentTick) { TRACE(); - if (m_state.constructionQueue.empty()) + if (state.constructionQueue.empty()) { return; } - ConstructionSite& front = m_state.constructionQueue.front(); + ConstructionSite& front = state.constructionQueue.front(); // Guard: if somehow the front site was never started, start it now. if (front.completesAt == 0) @@ -556,18 +553,18 @@ void BuildingSystem::tickConstruction(Tick currentTick) // filters configured while under construction carry over (REQ-BLD-SITE-CONFIG). reregisterBeltTile(building, front.splitterFilterA, front.splitterFilterB); - m_state.buildings.push_back(std::move(building)); + state.buildings.push_back(std::move(building)); - m_state.constructionQueue.pop_front(); + state.constructionQueue.pop_front(); // Start next queued site if present. - if (!m_state.constructionQueue.empty() && m_state.constructionQueue.front().completesAt == 0) + if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0) { const BuildingDef* nextDef = - m_config.buildings.findBuildingDef(m_state.constructionQueue.front().type); + m_config.buildings.findBuildingDef(state.constructionQueue.front().type); if (nextDef) { - m_state.constructionQueue.front().completesAt = + state.constructionQueue.front().completesAt = currentTick + secondsToTicks(nextDef->constructionTimeSeconds); } } @@ -601,20 +598,20 @@ void BuildingSystem::reregisterBeltTile(const Building& building, } } -void BuildingSystem::tickDeconstruction(Tick currentTick) +void BuildingSystem::tickDeconstruction(FactoryState& state, Tick currentTick) { TRACE(); - if (m_state.deconstructionQueue.empty()) + if (state.deconstructionQueue.empty()) { return; } - DeconstructionEntry& front = m_state.deconstructionQueue.front(); + DeconstructionEntry& front = state.deconstructionQueue.front(); // Guard: if the front entry's timer was never started, start it now. if (front.completesAt == 0) { - startFrontDeconstruction(currentTick); + startFrontDeconstruction(state, currentTick); return; } @@ -626,15 +623,15 @@ void BuildingSystem::tickDeconstruction(Tick currentTick) // Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT). // Belt/tunnel/splitter tiles were already unregistered when the building was // queued (see deconstruct), so only tile occupancy and the record remain. - for (std::vector::iterator it = m_state.buildings.begin(); - it != m_state.buildings.end(); + for (std::vector::iterator it = state.buildings.begin(); + it != state.buildings.end(); ++it) { if (it->id != front.id) { continue; } const BuildingDef* def = m_config.buildings.findBuildingDef(it->type); - m_state.grid.release(it->bodyCells); - m_state.buildings.erase(it); + state.grid.release(it->bodyCells); + state.buildings.erase(it); if (def) { m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100); @@ -642,16 +639,16 @@ void BuildingSystem::tickDeconstruction(Tick currentTick) break; } - m_state.deconstructionQueue.pop_front(); + state.deconstructionQueue.pop_front(); // Start the next queued deconstruction, if any. - startFrontDeconstruction(currentTick); + startFrontDeconstruction(state, currentTick); } -void BuildingSystem::cancelDeconstruction(BuildingId id) +void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id) { - for (std::deque::iterator it = m_state.deconstructionQueue.begin(); - it != m_state.deconstructionQueue.end(); + for (std::deque::iterator it = state.deconstructionQueue.begin(); + it != state.deconstructionQueue.end(); ++it) { if (it->id != id) { continue; } @@ -659,27 +656,27 @@ void BuildingSystem::cancelDeconstruction(BuildingId id) // Resume operation: clear the flag and re-register belt/tunnel/splitter // tiles that were unregistered at enqueue (which re-pairs tunnels, // REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund. - if (Building* building = findBuilding(m_state, id)) + if (Building* building = findBuilding(state, id)) { building->queuedForDeconstruction = false; reregisterBeltTile(*building, it->splitterFilterA, it->splitterFilterB); } - m_state.deconstructionQueue.erase(it); + state.deconstructionQueue.erase(it); // If the running front was removed, the new front (completesAt == 0) has // its timer started by the next tickDeconstruction guard. return; } } -void BuildingSystem::tickBeltPull() +void BuildingSystem::tickBeltPull(FactoryState& state) { TRACE(); // Same per-tick step as the belts, so items travel inward at belt speed // (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE). const double progressPerTick = m_belts.getProgressPerTick_tpt(); - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE). if (building.queuedForDeconstruction) { continue; } @@ -759,17 +756,17 @@ void BuildingSystem::depositToInputBelt(Building& consumer, consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0}); } -bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId, +bool BuildingSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId, const Port& outputPort, const Item& item) { - const std::optional ownerId = m_state.grid.findOwner(outputPort.tile); + const std::optional ownerId = state.grid.findOwner(outputPort.tile); if (!ownerId.has_value() || *ownerId == producerId) { return false; } - Building* consumer = findBuilding(m_state, *ownerId); + Building* consumer = findBuilding(state, *ownerId); if (!consumer) { return false; // an unbuilt construction site, or not an operational building @@ -794,10 +791,10 @@ bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId, return false; } -void BuildingSystem::tickProduction(Tick currentTick) +void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick) { TRACE(); - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE). if (building.queuedForDeconstruction) { continue; } @@ -897,10 +894,10 @@ void BuildingSystem::tickProduction(Tick currentTick) } } -void BuildingSystem::tickShipyardProduction(Tick currentTick) +void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTick) { TRACE(); - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE). if (building.queuedForDeconstruction) { continue; } @@ -993,14 +990,14 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick) } } -void BuildingSystem::tickOutputBelts() +void BuildingSystem::tickOutputBelts(FactoryState& state) { TRACE(); // Use BeltSystem's own per-tick step so emerging items travel at exactly the // same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE). const double progressPerTick = m_belts.getProgressPerTick_tpt(); - for (Building& building : m_state.buildings) + for (Building& building : state.buildings) { // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE). if (building.queuedForDeconstruction) { continue; } @@ -1023,7 +1020,7 @@ void BuildingSystem::tickOutputBelts() { const Item item = lane.front().item; if (m_belts.tryPutItem(port.tile, item, port.direction) - || tryDirectCoupleDeposit(building.id, port, item)) + || tryDirectCoupleDeposit(state, building.id, port, item)) { lane.erase(lane.begin()); } @@ -1043,10 +1040,10 @@ void BuildingSystem::tickOutputBelts() } } -void BuildingSystem::forEachEmergingItem( +void BuildingSystem::forEachEmergingItem(const FactoryState& state, const std::function& visit) const { - for (const Building& building : m_state.buildings) + for (const Building& building : state.buildings) { for (std::size_t p = 0; p < building.outputPorts.size(); ++p) { @@ -1065,10 +1062,10 @@ void BuildingSystem::forEachEmergingItem( } } -void BuildingSystem::forEachIncomingItem( +void BuildingSystem::forEachIncomingItem(const FactoryState& state, const std::function& visit) const { - for (const Building& building : m_state.buildings) + for (const Building& building : state.buildings) { for (std::size_t p = 0; p < building.inputPorts.size(); ++p) { @@ -1096,10 +1093,10 @@ void BuildingSystem::forEachIncomingItem( -void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation) +void BuildingSystem::rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation) { // Construction site path — just update rotation; no ports to recompute. - for (ConstructionSite& site : m_state.constructionQueue) + for (ConstructionSite& site : state.constructionQueue) { if (site.id == id) { @@ -1109,7 +1106,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation) } // Operational building path. - for (Building& b : m_state.buildings) + for (Building& b : state.buildings) { if (b.id != id) { continue; } @@ -1161,7 +1158,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation) } } -BuildingId BuildingSystem::placeImmediate(BuildingType type, +BuildingId BuildingSystem::placeImmediate(FactoryState& state, BuildingType type, const std::vector& surfaceMask, QPoint anchor, Rotation rotation) { @@ -1179,7 +1176,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type, { const QPoint absCell = anchor + cell; building.bodyCells.push_back(absCell); - m_state.grid.occupy(absCell, id); + state.grid.occupy(absCell, id); } for (const Port& port : mask.outputPorts) { @@ -1197,14 +1194,14 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type, initSalvageBayBuffer(building); } - m_state.buildings.push_back(std::move(building)); + state.buildings.push_back(std::move(building)); return id; } -bool BuildingSystem::removeBuilding(BuildingId id) +bool BuildingSystem::removeBuilding(FactoryState& state, BuildingId id) { - for (std::vector::iterator it = m_state.buildings.begin(); - it != m_state.buildings.end(); + for (std::vector::iterator it = state.buildings.begin(); + it != state.buildings.end(); ++it) { if (it->id == id) @@ -1214,31 +1211,31 @@ bool BuildingSystem::removeBuilding(BuildingId id) { m_belts.removeTile(it->anchor); } - m_state.grid.release(it->bodyCells); - m_state.buildings.erase(it); + state.grid.release(it->bodyCells); + state.buildings.erase(it); return true; } } return false; } -void BuildingSystem::forEachBuilding(std::function fn) +void BuildingSystem::forEachBuilding(FactoryState& state, std::function fn) { - for (Building& b : m_state.buildings) + for (Building& b : state.buildings) { fn(b); } } -void BuildingSystem::registerTileOccupancy(const std::vector& cells, +void BuildingSystem::registerTileOccupancy(FactoryState& state, const std::vector& cells, BuildingId ownerPlaceholder) { - m_state.grid.occupy(cells, ownerPlaceholder); + state.grid.occupy(cells, ownerPlaceholder); } -void BuildingSystem::unregisterTileOccupancy(const std::vector& cells) +void BuildingSystem::unregisterTileOccupancy(FactoryState& state, const std::vector& cells) { - m_state.grid.release(cells); + state.grid.release(cells); } namespace @@ -1270,12 +1267,12 @@ void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer) } } // namespace -void BuildingSystem::appendChecksum(Hasher& hasher) const +void BuildingSystem::appendChecksum(const FactoryState& state, Hasher& hasher) const { - // m_state.buildings keeps a stable, deterministic order (append on build, swap-free + // state.buildings keeps a stable, deterministic order (append on build, swap-free // erase aside — both runs perform identical operations, so order matches). - hasher.append(m_state.buildings.size()); - for (const Building& b : m_state.buildings) + hasher.append(state.buildings.size()); + for (const Building& b : state.buildings) { hasher.append(b.id); hasher.append(b.anchor); @@ -1318,8 +1315,8 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const hasher.append(b.queuedForDeconstruction); } - hasher.append(m_state.constructionQueue.size()); - for (const ConstructionSite& s : m_state.constructionQueue) + hasher.append(state.constructionQueue.size()); + for (const ConstructionSite& s : state.constructionQueue) { hasher.append(s.id); hasher.append(s.anchor); @@ -1336,8 +1333,8 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); } } - hasher.append(m_state.deconstructionQueue.size()); - for (const DeconstructionEntry& e : m_state.deconstructionQueue) + hasher.append(state.deconstructionQueue.size()); + for (const DeconstructionEntry& e : state.deconstructionQueue) { hasher.append(e.id); hasher.append(e.completesAt); @@ -1347,5 +1344,5 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); } } - m_state.grid.appendChecksum(hasher); + state.grid.appendChecksum(hasher); } diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index 78b15a0..9ead72e 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -37,7 +37,6 @@ class BuildingSystem { public: BuildingSystem(const GameConfig& config, - FactoryState& state, BeltSystem& belts, std::function allocateBuildingId, std::function addBuildingBlocks, @@ -53,7 +52,7 @@ public: // queue. Terrain type (A vs S) is NOT checked here so that tests can stage // arbitrary layouts; the player-facing entry point // (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid. - std::optional place(BuildingType type, QPoint anchor, Rotation rotation, + std::optional place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation, Tick currentTick); // Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and @@ -65,7 +64,8 @@ public: // Sets the current buildable asteroid width in tiles. Grows the left // placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK). // Defaults to world.regions.asteroid_width_tiles at construction. - void setAsteroidWidth_tiles(int widthTiles) { m_state.asteroidWidth_tiles = widthTiles; } + void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const + { state.asteroidWidth_tiles = widthTiles; } // Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT). // A construction site is removed instantly and the full cost is returned. @@ -73,23 +73,23 @@ public: // (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is // credited later, on completion in tickDeconstruction, so this returns 0 for // it. Returns 0 for unknown ids and for a building already queued. - int deconstruct(BuildingId id, Tick currentTick); + int deconstruct(FactoryState& state, BuildingId id, Tick currentTick); // Take a building back out of the deconstruction queue before it is removed // (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation // (re-registering belt/tunnel/splitter tiles); discards deconstruction // progress and credits no refund. No-op if the id is not queued. - void cancelDeconstruction(BuildingId id); + void cancelDeconstruction(FactoryState& state, BuildingId id); // True if the building is currently in the deconstruction queue. // Set the recipe (or schematic id for shipyard) on a building or queued // construction site. Clears both buffers on an operational building. - void setRecipe(BuildingId id, const std::string& recipeId); + void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId); // Set the module layout for a shipyard. Cancels in-progress production // (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD). - void setShipLayout(BuildingId id, const ShipLayoutConfig& layout); + void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout); // Splitter filter configuration for a queued/under-construction Splitter // site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through @@ -98,23 +98,23 @@ public: // output directions (derived from its surface mask) and stored filters, or // nullopt if the id is not a Splitter site. The stored filters are applied // to BeltSystem when the splitter finishes building (tickConstruction). - void setSiteSplitterFilters(BuildingId id, + void setSiteSplitterFilters(FactoryState& state, BuildingId id, const std::vector& filterA, const std::vector& filterB); // -- Tick hooks (called from Simulation::tick in the documented order) --- - void tickConstruction(Tick currentTick); + void tickConstruction(FactoryState& state, Tick currentTick); // Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a // time, in parallel with tickConstruction. Removes the front building and // credits its refund when its timer elapses. - void tickDeconstruction(Tick currentTick); - void tickBeltPull(); - void tickProduction(Tick currentTick); - void tickShipyardProduction(Tick currentTick); + void tickDeconstruction(FactoryState& state, Tick currentTick); + void tickBeltPull(FactoryState& state); + void tickProduction(FactoryState& state, Tick currentTick); + void tickShipyardProduction(FactoryState& state, Tick currentTick); // Advances each building's virtual output belts, hands finished items off onto // the adjacent real belt, and feeds new buffered items into them // (REQ-MAT-OUTPUT-EMERGE). - void tickOutputBelts(); + void tickOutputBelts(FactoryState& state); // -- Queries ------------------------------------------------------------- @@ -134,19 +134,19 @@ public: // virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its // world-space centre (in tile units). Least-progressed first (drawn bottom) so // callers can paint in visit order (REQ-GW-TILE-SIZE ordering). - void forEachEmergingItem( + void forEachEmergingItem(const FactoryState& state, const std::function& visit) const; // Visits every item currently travelling inward on a building input port's // virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its // world-space centre (in tile units). Least-progressed first (drawn bottom). - void forEachIncomingItem( + void forEachIncomingItem(const FactoryState& state, const std::function& visit) const; // Rotate an existing building or construction site to newRotation in place. // For belt-type operational buildings, re-registers with BeltSystem (items // currently on the tile are discarded by BeltSystem::removeTile). - void rotateInPlace(BuildingId id, Rotation newRotation); + void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation); // Input-capable adjacent tiles for a building or construction site @@ -155,8 +155,8 @@ public: // the target. Output-port edges are excluded. Empty for an unknown id. // Register / unregister tile occupancy for ECS station entities. - void registerTileOccupancy(const std::vector& cells, BuildingId ownerPlaceholder); - void unregisterTileOccupancy(const std::vector& cells); + void registerTileOccupancy(FactoryState& state, const std::vector& cells, BuildingId ownerPlaceholder); + void unregisterTileOccupancy(FactoryState& state, const std::vector& cells); // Place one "scrap" item into a SalvageBay's output buffer. // Returns false if bay not found, wrong type, or output buffer is full. @@ -164,26 +164,26 @@ public: // Bypass the construction queue and create a fully-operational Building // immediately. Used for pre-placed structures (HQ, defence stations). // surfaceMask comes from the relevant config struct. - BuildingId placeImmediate(BuildingType type, + BuildingId placeImmediate(FactoryState& state, BuildingType type, const std::vector& surfaceMask, QPoint anchor, Rotation rotation); // Remove an operational building by id without refund (used for deaths). // Returns true if found and removed. - bool removeBuilding(BuildingId id); + bool removeBuilding(FactoryState& state, BuildingId id); // Mutable iteration over all operational buildings. - void forEachBuilding(std::function fn); + void forEachBuilding(FactoryState& state, std::function fn); // -- Determinism --------------------------------------------------------- // Folds all building, construction-site, and tile-occupancy state into the // hasher in deterministic order (see docs/replay_design.md). - void appendChecksum(Hasher& hasher) const; + void appendChecksum(const FactoryState& state, Hasher& hasher) const; private: // Starts the front deconstruction-queue entry's timer if not yet started // (mirrors how tickConstruction starts a queued construction site). - void startFrontDeconstruction(Tick currentTick); + void startFrontDeconstruction(FactoryState& state, Tick currentTick); // Registers a belt/splitter/tunnel building's tile with the belt subsystem // (on construction completion, or when un-queuing a deconstruction). No-op for @@ -206,7 +206,7 @@ private: // Attempts to hand an emerging output item straight into a directly adjacent // building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE). // Returns true if the item was accepted onto the consumer's input belt. - bool tryDirectCoupleDeposit(BuildingId producerId, + bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId, const Port& outputPort, const Item& item); @@ -233,9 +233,6 @@ private: const GameConfig& m_config; - // The factory's world data — buildings, queued work, tile ownership. Owned by - // Simulation, not by this system (see FactoryState.h). - FactoryState& m_state; BeltSystem& m_belts; std::function m_allocateBuildingId; diff --git a/src/lib/sim/FactoryState.h b/src/lib/sim/FactoryState.h index 46f2d1f..74a0493 100644 --- a/src/lib/sim/FactoryState.h +++ b/src/lib/sim/FactoryState.h @@ -4,6 +4,7 @@ #include #include "Building.h" +#include "GameConfig.h" #include "BuildingGrid.h" #include "BuildingId.h" #include "ItemType.h" @@ -52,3 +53,14 @@ struct FactoryState // Seeded from config by BuildingSystem's constructor. int asteroidWidth_tiles = 0; }; + +// A fresh factory for a new run: nothing built, and the asteroid bound seeded from +// config. Every owner of a FactoryState creates it this way — the bound has no +// sensible default without the config, so a default-constructed state would refuse +// every placement on the asteroid. +inline FactoryState makeFactoryState(const GameConfig& config) +{ + FactoryState state; + state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles; + return state; +} diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index d7042f2..57b196b 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -50,6 +50,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed) { m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[1] = entt::null; + m_factoryState = makeFactoryState(m_config); initializeSubsystems(); @@ -97,7 +98,7 @@ void Simulation::reset(unsigned int seed) m_pendingSchematicChoices.clear(); m_admin.clear(); - m_factoryState = FactoryState{}; + m_factoryState = makeFactoryState(m_config); m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps); initializeSubsystems(); @@ -109,7 +110,6 @@ void Simulation::initializeSubsystems() { m_buildingSystem = std::make_unique( m_config, - m_factoryState, m_beltSystem, [this]() { return allocateBuildingId(); }, [this](int amount) { m_buildingBlocksStock += amount; }, @@ -153,15 +153,15 @@ void Simulation::apply(const Command& command) const BuildingId id = *placed; if (c.recipeId.has_value()) { - m_buildingSystem->setRecipe(id, *c.recipeId); + m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId); } if (c.shipLayout.has_value()) { - m_buildingSystem->setShipLayout(id, *c.shipLayout); + m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout); } if (c.hasSplitterFilters) { - m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB); + m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB); } break; } @@ -174,26 +174,26 @@ void Simulation::apply(const Command& command) case CommandKind::RotateInPlace: { const RotateInPlaceCommand& c = static_cast(command); - m_buildingSystem->rotateInPlace(*c.id, c.newRotation); + m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation); break; } case CommandKind::SetRecipe: { const SetRecipeCommand& c = static_cast(command); - m_buildingSystem->setRecipe(*c.id, c.recipeId); + m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId); break; } case CommandKind::SetShipLayout: { const SetShipLayoutCommand& c = static_cast(command); - m_buildingSystem->setShipLayout(*c.id, c.layout); + m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout); break; } case CommandKind::SetSiteSplitterFilters: { const SetSiteSplitterFiltersCommand& c = static_cast(command); - m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB); + m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB); break; } case CommandKind::SetSplitterFilters: @@ -243,12 +243,12 @@ void Simulation::tick() m_waveSystem->tickThreatAccumulation(); // Construction + production pipeline - m_buildingSystem->tickConstruction(m_currentTick); - m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction - m_buildingSystem->tickBeltPull(); // step 3 - m_buildingSystem->tickProduction(m_currentTick); // step 4 - m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b - m_buildingSystem->tickOutputBelts(); // step 5 + m_buildingSystem->tickConstruction(m_factoryState, m_currentTick); + m_buildingSystem->tickDeconstruction(m_factoryState, m_currentTick); // parallel to construction + m_buildingSystem->tickBeltPull(m_factoryState); // step 3 + m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4 + m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b + m_buildingSystem->tickOutputBelts(m_factoryState); // step 5 m_beltSystem.tick(); // step 6 // Step 7: ship behavior systems (movement arbitration via intent priority) @@ -306,8 +306,7 @@ void Simulation::placeInitialStructures() (m_config.world.heightTiles - hqParsed.footprint.height()) / 2; const float hqHp = static_cast(m_config.stations.hq.hpFormula.evaluate(0.0)); - m_hqBuildingId = m_buildingSystem->placeImmediate( - BuildingType::Hq, + m_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq, m_config.stations.hq.surfaceMask, QPoint(hqAnchorX, hqAnchorY), Rotation::East); @@ -356,7 +355,7 @@ void Simulation::placeInitialStructures() m_admin.addComponent(wChild, ModuleOwnerComponent{m_playerStation1Entity}); } - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } { const QPoint anchor(psAnchorX, ps2Y); @@ -373,7 +372,7 @@ void Simulation::placeInitialStructures() m_admin.addComponent(wChild, ModuleOwnerComponent{m_playerStation2Entity}); } - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } // Rally point: center of the player defence stations' X column, world vertical midpoint. @@ -428,7 +427,7 @@ void Simulation::placeEnemyStationSet(int generation) m_admin.addComponent(wChild, ModuleOwnerComponent{m_currentEnemyStationEntities[0]}); } - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } { const QPoint anchor(anchorX, y2); @@ -445,7 +444,7 @@ void Simulation::placeEnemyStationSet(int generation) m_admin.addComponent(wChild, ModuleOwnerComponent{m_currentEnemyStationEntities[1]}); } - m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); + m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } } @@ -519,7 +518,7 @@ void Simulation::tickDeathsAndLoot() { m_debrisSystem->spawn(pos.value, scrap, despawnAt); } - m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); + m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells); { std::vector stationChildren; m_admin.forEach( @@ -671,7 +670,7 @@ unsigned long long Simulation::computeStateChecksum() const m_unlockState.appendChecksum(hasher); // Subsystems contribute their own state. - m_buildingSystem->appendChecksum(hasher); + m_buildingSystem->appendChecksum(m_factoryState, hasher); m_beltSystem.appendChecksum(hasher); // ECS component state. View iteration order is a pure function of the @@ -784,7 +783,7 @@ void Simulation::tryExpandAsteroid() } m_buildingBlocksStock -= cost; ++m_expansionsPurchased; - m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles()); + m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles()); } bool Simulation::isGameOver() const @@ -880,17 +879,17 @@ std::optional Simulation::tryPlaceBuilding(BuildingType type, QPoint return std::nullopt; } m_buildingBlocksStock -= cost; - return m_buildingSystem->place(type, anchor, rotation, m_currentTick); + return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick); } void Simulation::deconstruct(BuildingId id) { - m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick); + m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick); } void Simulation::cancelDeconstruction(BuildingId id) { - m_buildingSystem->cancelDeconstruction(id); + m_buildingSystem->cancelDeconstruction(m_factoryState, id); } BuildingSystem& Simulation::getBuildingsMutable() diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 4a09b4f..ed7ef27 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -56,7 +56,7 @@ struct Fixture { GameConfig cfg; - FactoryState state; + FactoryState state = makeFactoryState(cfg); BeltSystem belts; BuildingId nextBuildingId; int stock; @@ -79,7 +79,7 @@ struct Fixture , nextBuildingId(1) , stock(0) , rng(42) - , buildings(cfg, state, belts, + , buildings(cfg, belts, [this]() { return nextBuildingId++; }, [this](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -952,12 +952,12 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b { Fixture f; - const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, + const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay, QPoint(-4, 0), Rotation::East, 0).value(); Tick t = 0; for (int i = 0; i < 500; ++i) { - f.buildings.tickConstruction(t++); + f.buildings.tickConstruction(f.state, t++); if (findBuilding(f.state, bayId) != nullptr) { break; @@ -987,12 +987,12 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo", { Fixture f; - const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, + const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay, QPoint(-4, 0), Rotation::East, 0).value(); Tick t = 0; for (int i = 0; i < 500; ++i) { - f.buildings.tickConstruction(t++); + f.buildings.tickConstruction(f.state, t++); if (findBuilding(f.state, bayId) != nullptr) { break; } } const Building* bay = findBuilding(f.state, bayId); diff --git a/src/test/BlueprintTest.cpp b/src/test/BlueprintTest.cpp index 4a84483..b56906d 100644 --- a/src/test/BlueprintTest.cpp +++ b/src/test/BlueprintTest.cpp @@ -666,7 +666,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", " const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); - SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore"); const ConstructionSite* site = findSite(sim.getFactoryState(), id); REQUIRE(site != nullptr); @@ -680,7 +680,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); - SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore"); // Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300. // Run 301 ticks (0..300) to process the completion tick. @@ -726,7 +726,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue const BuildingId id = SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); - SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore"); const Blueprint bp = captureBlueprintFromSelection(sim, { id }); @@ -743,7 +743,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site const BuildingId idA = SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(idA != kInvalidBuildingId); - SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore"); for (int i = 0; i <= static_cast(secondsToTicks(10.0)); ++i) { sim.tick(); } REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr); @@ -751,7 +751,7 @@ TEST_CASE("Blueprint creation: mixed operational building and 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"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore"); REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr); const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB }); @@ -854,7 +854,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout pm.rotation = Rotation::East; layout.placedModules.push_back(pm); - SimulationTestAccess::buildings(sim).setShipLayout(id, layout); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout); const ConstructionSite* site = findSite(sim.getFactoryState(), id); REQUIRE(site != nullptr); @@ -878,7 +878,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc pm.rotation = Rotation::North; layout.placedModules.push_back(pm); - SimulationTestAccess::buildings(sim).setShipLayout(id, layout); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout); // Shipyard construction_time_seconds = 30 in the test config. double constructionTime = 0.0; diff --git a/src/test/BuildingConfigTest.cpp b/src/test/BuildingConfigTest.cpp index bdce076..df4dc8d 100644 --- a/src/test/BuildingConfigTest.cpp +++ b/src/test/BuildingConfigTest.cpp @@ -37,8 +37,7 @@ BuildingId placeOperational(Simulation& sim, const GameConfig& cfg, { const BuildingDef* def = findDef(cfg, type); REQUIRE(def != nullptr); - return SimulationTestAccess::buildings(sim).placeImmediate( - type, def->surfaceMask, anchor, Rotation::East); + return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East); } const ShipDef* findAvailableSchematic(const GameConfig& cfg) @@ -67,7 +66,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]" Simulation sim(loadTestConfig(), 7); const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0)); - SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore"); const std::optional config = readBuildingConfig(sim, id); REQUIRE(config.has_value()); @@ -103,8 +102,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout", REQUIRE(schematic != nullptr); const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0)); - SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id); - SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{}); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{}); const std::optional config = readBuildingConfig(sim, id); REQUIRE(config.has_value()); @@ -126,7 +125,7 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]") REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr); REQUIRE(findSite(sim.getFactoryState(), id) != nullptr); - SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore"); const std::optional config = readBuildingConfig(sim, id); REQUIRE(config.has_value()); diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 6303b1a..45c46d5 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -53,15 +53,16 @@ static Port westPort(QPoint tile) } // Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick. -static void runTicks(BuildingSystem& bs, BeltSystem& belts, int n, Tick& tick) +static void runTicks(BuildingSystem& bs, FactoryState& state_bs, BeltSystem& belts, + int n, Tick& tick) { for (int i = 0; i < n; ++i) { - bs.tickConstruction(tick); - bs.tickDeconstruction(tick); - bs.tickBeltPull(); - bs.tickProduction(tick); - bs.tickOutputBelts(); + bs.tickConstruction(state_bs, tick); + bs.tickDeconstruction(state_bs, tick); + bs.tickBeltPull(state_bs); + bs.tickProduction(state_bs, tick); + bs.tickOutputBelts(state_bs); belts.tick(); ++tick; } @@ -87,7 +88,7 @@ static std::vector outputSideItems(const Building& b) struct PlacementFixture { GameConfig cfg = loadTestConfig(); - FactoryState state; + FactoryState state = makeFactoryState(cfg); BeltSystem belts{cfg.world.beltSpeed_tps}; int stock = 0; std::mt19937 rng{0}; @@ -95,7 +96,7 @@ struct PlacementFixture BuildingSystem bs; PlacementFixture() - : bs(cfg, state, belts, + : bs(cfg, belts, [this]() { return nextBuildingId++; }, [this](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -116,15 +117,15 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); // Miner mask ["AA","A>"] with East rotation → body at (0,0),(1,0),(0,1). @@ -143,7 +144,7 @@ TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[ // Miner mask ["AA","A>"] East → body at (0,0),(1,0),(0,1); at y=-1 the top // row sits above the world. - const std::optional id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0); + const std::optional id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, -1), Rotation::East, 0); REQUIRE_FALSE(id.has_value()); REQUIRE(getAllSites(f.state).empty()); REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0))); @@ -156,7 +157,7 @@ TEST_CASE("BuildingSystem: place rejects a building below the world (y >= height // Anchored on the last in-bounds row, the miner's lower body row reaches // y == heightTiles, which is outside the world. - const std::optional id = f.bs.place(BuildingType::Miner, + const std::optional id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, heightTiles - 1), Rotation::East, 0); REQUIRE_FALSE(id.has_value()); REQUIRE(getAllSites(f.state).empty()); @@ -167,7 +168,7 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge", PlacementFixture f; const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles; - const std::optional id = f.bs.place(BuildingType::Miner, + const std::optional id = f.bs.place(f.state, BuildingType::Miner, QPoint(leftEdgeX - 1, 0), Rotation::East, 0); REQUIRE_FALSE(id.has_value()); REQUIRE(getAllSites(f.state).empty()); @@ -180,7 +181,7 @@ TEST_CASE("BuildingSystem: place accepts a building flush against the world's le const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles; // Miner body min relative x is 0, so its leftmost cell sits exactly on the edge. - const BuildingId id = f.bs.place(BuildingType::Miner, + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(leftEdgeX, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(isTileOccupied(f.state, QPoint(leftEdgeX, 0))); @@ -191,7 +192,7 @@ TEST_CASE("BuildingSystem: place imposes no right-side bound (space extends righ { PlacementFixture f; - const BuildingId id = f.bs.place(BuildingType::Miner, + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(1000, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); } @@ -224,22 +225,22 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0); + bs.place(state_bs, BuildingType::Belt, QPoint(5, 5), Rotation::East, 0); // Belt is queued — not yet in BeltSystem. REQUIRE_FALSE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); // Complete construction (1 s). Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); REQUIRE(getAllBuildings(state_bs).size() == 1); @@ -254,15 +255,15 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(getAllSites(state_bs).size() == 1); REQUIRE(getAllBuildings(state_bs).empty()); @@ -274,11 +275,11 @@ TEST_CASE("BuildingSystem: deconstructing a construction site removes it instant { PlacementFixture f; const BuildingId id = - f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); // Still queued for construction (not yet built): instant removal, full cost // refunded immediately, never entering the deconstruction queue (REQ-BLD-DECONSTRUCT). - const int refund = f.bs.deconstruct(id, 0); + const int refund = f.bs.deconstruct(f.state, id, 0); REQUIRE(refund == 15); // Miner cost = 15 REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0))); @@ -297,15 +298,15 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); REQUIRE(getAllSites(state_bs).front().completesAt > 0); } @@ -316,16 +317,16 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0); + bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + bs.place(state_bs, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0); REQUIRE(getAllSites(state_bs).size() == 2); REQUIRE(getAllSites(state_bs)[0].completesAt > 0); @@ -339,20 +340,20 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[ int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300. // We need to process tick 300 itself, so run 301 ticks (ticks 0..300). Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getAllSites(state_bs).empty()); REQUIRE(findBuilding(state_bs, id) != nullptr); @@ -367,7 +368,7 @@ static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick) { for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i) { - runTicks(f.bs, f.belts, 1, tick); + runTicks(f.bs, f.state, f.belts, 1, tick); } REQUIRE(findBuilding(f.state, id) != nullptr); } @@ -377,14 +378,14 @@ TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund cre { PlacementFixture f; const BuildingId id = - f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runUntilBuilt(f, id, tick); // Deconstructing a built building returns nothing immediately and queues it, // stopping it operating while its tiles stay occupied (REQ-BLD-DECON-QUEUE). - const int refund = f.bs.deconstruct(id, tick); + const int refund = f.bs.deconstruct(f.state, id, tick); REQUIRE(refund == 0); REQUIRE(isQueuedForDeconstruction(f.state, id)); REQUIRE(isTileOccupied(f.state, QPoint(0, 0))); @@ -392,7 +393,7 @@ TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund cre // 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(secondsToTicks(0.1)) + 1, tick); + runTicks(f.bs, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 1, tick); REQUIRE(findBuilding(f.state, id) == nullptr); REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0))); REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100); @@ -401,29 +402,29 @@ TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund cre TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time", "[building][decon]") { PlacementFixture f; - const BuildingId a = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - const BuildingId b = f.bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); + const BuildingId a = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId b = f.bs.place(f.state, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); Tick tick = 0; runUntilBuilt(f, a, tick); runUntilBuilt(f, b, tick); // Queue both in one tick; 'a' is at the front of the deconstruction queue. - f.bs.deconstruct(a, tick); - f.bs.deconstruct(b, tick); + f.bs.deconstruct(f.state, a, tick); + f.bs.deconstruct(f.state, b, tick); 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(secondsToTicks(0.1)) + 1, tick); + runTicks(f.bs, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 1, tick); 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(secondsToTicks(0.1)) + 2, tick); + runTicks(f.bs, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 2, tick); REQUIRE(findBuilding(f.state, b) == nullptr); REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100)); } @@ -433,23 +434,23 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n { PlacementFixture f; const BuildingId id = - f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runUntilBuilt(f, id, tick); - f.bs.deconstruct(id, tick); + f.bs.deconstruct(f.state, id, tick); REQUIRE(isQueuedForDeconstruction(f.state, id)); // Un-queue before it drains: it operates again, no refund, tiles still occupied. - f.bs.cancelDeconstruction(id); + f.bs.cancelDeconstruction(f.state, id); 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(secondsToTicks(0.1)) + 5, tick); + runTicks(f.bs, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 5, tick); REQUIRE(findBuilding(f.state, id) != nullptr); REQUIRE(f.stock == 0); } @@ -458,7 +459,7 @@ TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it", { PlacementFixture f; const BuildingId id = - f.bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runUntilBuilt(f, id, tick); @@ -466,12 +467,12 @@ TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it", // Queuing a belt unregisters its tile from the belt subsystem, so it no longer // accepts or transports items, though the tile stays occupied (REQ-BLD-DECON-QUEUE). - f.bs.deconstruct(id, tick); + f.bs.deconstruct(f.state, id, tick); REQUIRE_FALSE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East)); REQUIRE(isTileOccupied(f.state, QPoint(0, 0))); // Un-queuing re-registers the belt tile so it transports again. - f.bs.cancelDeconstruction(id); + f.bs.cancelDeconstruction(f.state, id); REQUIRE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East)); } @@ -479,7 +480,7 @@ TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip" { PlacementFixture f; const BuildingId id = - f.bs.place(BuildingType::Splitter, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.place(f.state, BuildingType::Splitter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runUntilBuilt(f, id, tick); @@ -488,10 +489,10 @@ TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip" // Queue: the belt subsystem tile (and its filters) are unregistered, but the // filters are captured so an un-queue can restore them. - f.bs.deconstruct(id, tick); + f.bs.deconstruct(f.state, id, tick); REQUIRE_FALSE(f.belts.getSplitterInfo(QPoint(0, 0)).has_value()); - f.bs.cancelDeconstruction(id); + f.bs.cancelDeconstruction(f.state, id); const std::optional info = f.belts.getSplitterInfo(QPoint(0, 0)); REQUIRE(info.has_value()); REQUIRE(info->filterA.size() == 1); @@ -506,20 +507,20 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - const BuildingId id2 = bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); + bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id2 = bs.place(state_bs, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); // Process through tick 300 to complete first miner's construction. Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getAllSites(state_bs).size() == 1); REQUIRE(getAllSites(state_bs).front().id == id2); @@ -537,21 +538,21 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(id, "mine_iron_ore"); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, id, "mine_iron_ore"); Tick tick = 0; // Construction completes on tick 300; production cycle starts tick 300, // completes on tick 330. Process through tick 330: 331 ticks total. - runTicks(bs, belts, + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); @@ -571,16 +572,16 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(id, "mine_iron_ore"); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, id, "mine_iron_ore"); Tick tick = 0; // Construction (10s) then cycle 1 starts at tick 300 (completesAt=330). @@ -588,7 +589,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") // Cycle 2 starts at tick 331 (completesAt=361). // Cycle 2 completes at tick 361: deposit item → buffer=2, cycle 3 stalls. // Need to process through tick 361: 362 ticks total. - runTicks(bs, belts, + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 2 * static_cast(secondsToTicks(1.0)) + 2, tick); @@ -612,16 +613,16 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value(); + const BuildingId minerId = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId smelterId = bs.place(state_bs, BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value(); (void)smelterId; Tick tick = 0; @@ -630,18 +631,18 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", // 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(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getProductionBuildingCount(state_bs) == 1); - runTicks(bs, belts, static_cast(secondsToTicks(15.0)), tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)), tick); 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(getActiveProductionBuildingCount(state_bs) == 0); - bs.setRecipe(minerId, "mine_iron_ore"); - runTicks(bs, belts, 1, tick); + bs.setRecipe(state_bs, minerId, "mine_iron_ore"); + runTicks(bs, state_bs, belts, 1, tick); REQUIRE(getActiveProductionBuildingCount(state_bs) == 1); } @@ -653,28 +654,28 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(id, "mine_iron_ore"); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, id, "mine_iron_ore"); Tick tick = 0; // Not yet operational while under construction. REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); // Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330). - runTicks(bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); 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(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, state_bs, belts, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); const Building* b = findBuilding(state_bs, id); REQUIRE(b != nullptr); @@ -696,8 +697,8 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -706,20 +707,20 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing // Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1). // Output port (2,1) East. Input port example: (2,0) West. - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); // Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any // ore/scrap that is an input to a smelter recipe. // Complete construction (15s → tick 450+1 = 451 ticks). Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); // Place west-flowing belt at (2,0): belt flows West, delivers to smelter. belts.placeBelt(QPoint(2, 0), Rotation::West); belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); belts.tick(); - bs.tickBeltPull(); + bs.tickBeltPull(state_bs); const Building* b = findBuilding(state_bs, sid); REQUIRE(b != nullptr); @@ -739,22 +740,22 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); belts.placeBelt(QPoint(2, 0), Rotation::West); belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); belts.tick(); - bs.tickBeltPull(); // accepts the item onto the input belt at progress 0.0 + bs.tickBeltPull(state_bs); // accepts the item onto the input belt at progress 0.0 const Building* b = findBuilding(state_bs, sid); REQUIRE(b != nullptr); @@ -766,7 +767,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1); // One more pull tick advances the input belt to the centre; the item arrives. - bs.tickBeltPull(); + bs.tickBeltPull(state_bs); REQUIRE(b->inputBuffer.counts.at(ItemType{"iron_ore"}) == 1); REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1); } @@ -781,18 +782,18 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::ReprocessingPlant, + const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed scrap via an input belt without ever running production (only pull), so // the buffer fills and stays full. Try to over-fill it well past the cap. @@ -801,7 +802,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th { belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); belts.tick(); - bs.tickBeltPull(); + bs.tickBeltPull(state_bs); } const Building* b = findBuilding(state_bs, id); @@ -824,18 +825,18 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); // Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a // west-flowing belt at input port (2,0). @@ -844,11 +845,11 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", { belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); belts.tick(); - bs.tickBeltPull(); + bs.tickBeltPull(state_bs); } // iron_ingot recipe cycle is 2s; run to completion. - runTicks(bs, belts, static_cast(secondsToTicks(2.0)) + 2, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(2.0)) + 2, tick); const Building* b = findBuilding(state_bs, sid); REQUIRE(b != nullptr); @@ -871,18 +872,18 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); // Feed 1 iron_ore (iron_ingot needs 2 — incomplete) then 2 copper_ore // (copper_ingot needs 2 — satisfiable) via the west-flowing input belt. @@ -892,11 +893,11 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete { belts.tryPutItem(QPoint(2, 0), makeItem(id)); belts.tick(); - bs.tickBeltPull(); + bs.tickBeltPull(state_bs); } // copper_ingot cycle is 2.5s; run to completion. - runTicks(bs, belts, static_cast(secondsToTicks(2.5)) + 2, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(2.5)) + 2, tick); const Building* b = findBuilding(state_bs, sid); REQUIRE(b != nullptr); @@ -926,29 +927,29 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(id, "mine_iron_ore"); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, id, "mine_iron_ore"); // Belt at the miner's output port tile (1,1) flowing East. belts.placeBelt(QPoint(1, 1), Rotation::East); Tick tick = 0; // Construction (10s) + 1 production cycle (1s) + 1 extra tick. - runTicks(bs, belts, + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); // Item should have been pushed onto the belt this tick or a subsequent one. // Run one more tick to ensure tickBeltPush fires after the deposit tick. - runTicks(bs, belts, 1, tick); + runTicks(bs, state_bs, belts, 1, tick); const std::optional item = belts.tryTakeItem(eastPort(QPoint(1, 1))); REQUIRE(item.has_value()); @@ -966,8 +967,8 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -975,16 +976,16 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p rng); // Miner at (0,0): body (0,0),(1,0),(0,1); output port tile (1,1) flowing East. - const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(minerId, "mine_iron_ore"); + const BuildingId minerId = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, minerId, "mine_iron_ore"); // Smelter anchored at (1,1): body (1,1),(2,1),(1,2),(2,2). Its body cell (1,1) is // the miner's output-port tile, and its west input edge there faces East, so the // two ports meet — no belt placed anywhere. - const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0).value(); + const BuildingId smelterId = bs.place(state_bs, BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0).value(); Tick tick = 0; // Smelter build (15s) + margin for coupling and a smelt cycle. - runTicks(bs, belts, static_cast(secondsToTicks(30.0)), tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(30.0)), tick); const Building* smelter = findBuilding(state_bs, smelterId); REQUIRE(smelter != nullptr); @@ -1007,8 +1008,8 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -1016,15 +1017,15 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu rng); // Producing miner at (0,0), output port (1,1) East. - const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(minerId, "mine_iron_ore"); + const BuildingId minerId = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, minerId, "mine_iron_ore"); // A second, idle miner anchored at (1,1) occupies the output-port tile but takes // no inputs, so it cannot accept the iron_ore. - const BuildingId sinkId = bs.place(BuildingType::Miner, QPoint(1, 1), Rotation::East, 0).value(); + const BuildingId sinkId = bs.place(state_bs, BuildingType::Miner, QPoint(1, 1), Rotation::East, 0).value(); Tick tick = 0; // Both miners build sequentially (10s each), then the producer runs and jams. - runTicks(bs, belts, static_cast(secondsToTicks(25.0)), tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)), tick); const Building* miner = findBuilding(state_bs, minerId); const Building* sink = findBuilding(state_bs, sinkId); @@ -1047,20 +1048,20 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production" int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(id, "mine_iron_ore"); + const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + bs.setRecipe(state_bs, id, "mine_iron_ore"); Tick tick = 0; // Run until first item is in output buffer. - runTicks(bs, belts, + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); @@ -1070,7 +1071,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production" REQUIRE(b->getOutputItemCount() > 0); } - bs.setRecipe(id, "mine_copper_ore"); + bs.setRecipe(state_bs, id, "mine_copper_ore"); const Building* b = findBuilding(state_bs, id); // Clearing the output buffer on a recipe change also discards emerging items @@ -1091,22 +1092,22 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::ReprocessingPlant, + const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the // single reprocessing recipe is applied automatically on completion. // Complete construction (25s). Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); const Building* b = findBuilding(state_bs, id); REQUIRE(b != nullptr); @@ -1124,22 +1125,22 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta // Seed chosen so first roll produces 2-item output (iron_ingot), filling buffer. std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::ReprocessingPlant, + const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the // single reprocessing recipe is applied automatically on completion. // Complete construction (25s). Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed 5 scrap into the building via a belt at an input port. // Reprocessing plant body (East rotation) = 3×3 at (0,0). @@ -1149,7 +1150,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta { belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); belts.tick(); - bs.tickBeltPull(); + bs.tickBeltPull(state_bs); } // Verify all five scrap were accepted; some may still be travelling inward on @@ -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(secondsToTicks(3.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(3.0)) + 1, tick); const Building* b = findBuilding(state_bs, id); REQUIRE(b != nullptr); @@ -1183,8 +1184,8 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -1203,15 +1204,15 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); const std::optional result = findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::North); @@ -1227,18 +1228,18 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(getAllSites(state_bs).empty()); const std::optional result = @@ -1255,15 +1256,15 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); // Querying with Splitter at the same tile — type mismatch → nullopt. REQUIRE_FALSE( @@ -1278,8 +1279,8 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in pla int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -1288,8 +1289,8 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in pla // Even with a coincident same-type tunnel under the ghost, rotate-in-place is // never offered for tunnels (REQ-BLD-ROTATE-IN-PLACE exception). - bs.place(BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East, 0); - bs.place(BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0); + bs.place(state_bs, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East, 0); + bs.place(state_bs, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0); REQUIRE_FALSE( findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value()); @@ -1305,8 +1306,8 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -1314,7 +1315,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin rng); // Smelter at (0,0) occupies body tiles (0,0),(1,0),(0,1),(1,1). - bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); // Ghost anchored at (1,0) would cover (1,0),(2,0),(1,1),(2,1): // only (1,0) and (1,1) are occupied — not a full coincidence. @@ -1330,8 +1331,8 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -1340,7 +1341,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t // Smelter is a fully filled 2×2 footprint — rotating the ghost produces the // same four body tiles, so findRotateInPlaceTarget must still return the id. - const BuildingId id = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); const std::optional result = findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(0, 0), Rotation::North); @@ -1360,18 +1361,18 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(findSite(state_bs, id)->rotation == Rotation::East); - bs.rotateInPlace(id, Rotation::North); + bs.rotateInPlace(state_bs, id, Rotation::North); REQUIRE(findSite(state_bs, id)->rotation == Rotation::North); } @@ -1384,19 +1385,19 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); const Tick completesAt = findSite(state_bs, id)->completesAt; REQUIRE(completesAt > 0); - bs.rotateInPlace(id, Rotation::South); + bs.rotateInPlace(state_bs, id, Rotation::South); REQUIRE(findSite(state_bs, id)->completesAt == completesAt); } @@ -1409,24 +1410,24 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(findBuilding(state_bs, id) != nullptr); const Building& before = *findBuilding(state_bs, id); REQUIRE(before.outputPorts[0].direction == Rotation::East); - bs.rotateInPlace(id, Rotation::North); + bs.rotateInPlace(state_bs, id, Rotation::North); const Building& after = *findBuilding(state_bs, id); REQUIRE(after.rotation == Rotation::North); @@ -1441,20 +1442,20 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; - FactoryState state_bs; - BuildingSystem bs(cfg, state_bs, belts, + FactoryState state_bs = makeFactoryState(cfg); + BuildingSystem bs(cfg, belts, [&nextBuildingId]() { return nextBuildingId++; }, [&stock](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); - bs.rotateInPlace(id, Rotation::North); + bs.rotateInPlace(state_bs, id, Rotation::North); // Belt tile must still be registered after rotation — items can be placed on it. REQUIRE(belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"))); @@ -1466,13 +1467,13 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split PlacementFixture f; const QPoint tile(5, 5); - const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Splitter, tile, Rotation::East, 0).value(); // Run until construction completes, so the splitter is registered with BeltSystem. Tick tick = 0; while (getAllBuildings(f.state).empty() && tick < 100000) { - runTicks(f.bs, f.belts, 1, tick); + runTicks(f.bs, f.state, f.belts, 1, tick); } REQUIRE(getAllBuildings(f.state).size() == 1); @@ -1480,7 +1481,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split const std::vector filterB{ ItemType{"copper_ore"} }; f.belts.setSplitterFilters(tile, filterA, filterB); - f.bs.rotateInPlace(id, Rotation::North); + f.bs.rotateInPlace(f.state, id, Rotation::North); // The tile is re-registered with BeltSystem carrying the filters it had before // the rotation — rotating must not reset a configured splitter to "accept all". @@ -1496,14 +1497,14 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca PlacementFixture f; const QPoint tile(5, 5); - const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Splitter, tile, Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(findSite(f.state, id) != nullptr); // Configure an output filter on the still-queued splitter site. const std::vector filterA{ ItemType{"iron_ore"} }; const std::vector filterB{}; - f.bs.setSiteSplitterFilters(id, filterA, filterB); + f.bs.setSiteSplitterFilters(f.state, id, filterA, filterB); // The site reports its two output directions and the stored filters before // it is built; it is not yet registered with BeltSystem. @@ -1517,7 +1518,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca Tick tick = 0; while (getAllBuildings(f.state).empty() && tick < 100000) { - runTicks(f.bs, f.belts, 1, tick); + runTicks(f.bs, f.state, f.belts, 1, tick); } REQUIRE(getAllBuildings(f.state).size() == 1); REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter); @@ -1671,12 +1672,12 @@ namespace // Advances the sim until the given site becomes an operational building, or a // safety cap is reached. - void buildToCompletion(BuildingSystem& bs, const FactoryState& state, + void buildToCompletion(BuildingSystem& bs, FactoryState& state, BeltSystem& belts, BuildingId id, Tick& tick) { for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i) { - runTicks(bs, belts, 1, tick); + runTicks(bs, state, belts, 1, tick); } } } @@ -1685,7 +1686,7 @@ TEST_CASE("BuildingSystem: getInputPorts on a miner site lists every input edge" { PlacementFixture f; // Miner mask ["AA","A>"] East → body (0,0),(1,0),(0,1); output tile (1,1) East. - const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); const std::vector ports = getInputPorts(f.state, f.cfg, id); @@ -1708,7 +1709,7 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu { PlacementFixture f; Tick tick = 0; - const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); const std::vector sitePorts = getInputPorts(f.state, f.cfg, id); buildToCompletion(f.bs, f.state, f.belts, id, tick); @@ -1726,7 +1727,7 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu TEST_CASE("BuildingSystem: getInputPorts invariants hold for a rotated site", "[building]") { PlacementFixture f; - const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::South, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::South, 0).value(); const ConstructionSite* site = findSite(f.state, id); REQUIRE(site != nullptr); diff --git a/src/test/CombatSystemTest.cpp b/src/test/CombatSystemTest.cpp index 268fd8a..eced6c0 100644 --- a/src/test/CombatSystemTest.cpp +++ b/src/test/CombatSystemTest.cpp @@ -52,7 +52,7 @@ static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship) struct CombatFixture { GameConfig cfg; - FactoryState state; + FactoryState state = makeFactoryState(cfg); std::mt19937 rng; EntityAdmin admin; BuildingId nextBuildingId; @@ -67,7 +67,7 @@ struct CombatFixture , nextBuildingId(1) , belts(cfg.world.beltSpeed_tps) , ships(cfg, admin) - , buildings(cfg, state, belts, + , buildings(cfg, belts, [this]() { return nextBuildingId++; }, [](int){}, [](const std::string&, QVector2D, const std::optional&) {}, diff --git a/src/test/CommandTest.cpp b/src/test/CommandTest.cpp index e1b1bed..752ad95 100644 --- a/src/test/CommandTest.cpp +++ b/src/test/CommandTest.cpp @@ -47,7 +47,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe" const BuildingId id = SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); - SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore"); + SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore"); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } diff --git a/src/test/ShipModuleTest.cpp b/src/test/ShipModuleTest.cpp index ff09e15..b24dc35 100644 --- a/src/test/ShipModuleTest.cpp +++ b/src/test/ShipModuleTest.cpp @@ -62,8 +62,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg) static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef) { - return SimulationTestAccess::buildings(sim).placeImmediate( - BuildingType::Shipyard, + return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard, yardDef.surfaceMask, QPoint(0, 0), Rotation::East); @@ -73,7 +72,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def, const ShipLayoutConfig& layout) { - SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) { + SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b) { if (b.id != yardId) { return; @@ -208,7 +207,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials", REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor"); ShipLayoutConfig layout; PlacedModule pm; @@ -217,7 +216,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials", pm.rotation = Rotation::East; layout.placedModules.push_back(pm); - SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout); const Building* b = findBuilding(sim.getFactoryState(), yardId); REQUIRE(b != nullptr); @@ -237,7 +236,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production", REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor"); // Fill materials and tick to start production. ShipLayoutConfig emptyLayout; @@ -256,7 +255,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production", pm.rotation = Rotation::East; layout.placedModules.push_back(pm); - SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout); const Building* b2 = findBuilding(sim.getFactoryState(), yardId); REQUIRE(b2 != nullptr); @@ -279,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured", REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, "interceptor"); // Deliberately no setShipLayout: recipe set, layout left unconfigured. // Charge only the base-hull materials (an empty layout adds none). @@ -314,7 +313,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]") REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor"); ShipLayoutConfig layout; PlacedModule pm; @@ -322,13 +321,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]") pm.position = QPoint(0, 0); pm.rotation = Rotation::East; layout.placedModules.push_back(pm); - SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout); const Building* b1 = findBuilding(sim.getFactoryState(), yardId); REQUIRE(b1 != nullptr); REQUIRE(b1->shipLayout.has_value()); - SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"destroyer"); const Building* b2 = findBuilding(sim.getFactoryState(), yardId); REQUIRE(b2 != nullptr); @@ -343,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout", REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor"); ShipLayoutConfig layout; PlacedModule pm; @@ -351,14 +350,14 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout", pm.position = QPoint(0, 0); pm.rotation = Rotation::East; layout.placedModules.push_back(pm); - SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); + SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout); 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"); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor"); const Building* b2 = findBuilding(sim.getFactoryState(), yardId); REQUIRE(b2 != nullptr); diff --git a/src/test/ShipyardTest.cpp b/src/test/ShipyardTest.cpp index e62a026..0ebc94a 100644 --- a/src/test/ShipyardTest.cpp +++ b/src/test/ShipyardTest.cpp @@ -58,8 +58,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg) static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef) { - return SimulationTestAccess::buildings(sim).placeImmediate( - BuildingType::Shipyard, + return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard, yardDef.surfaceMask, QPoint(0, 0), Rotation::East); @@ -75,7 +74,7 @@ static int countShips(Simulation& sim) static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def) { - SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) + SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b) { if (b.id != yardId) { @@ -107,7 +106,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes", const BuildingId yardId = placeShipyard(sim, *yardDef); REQUIRE(yardId != kInvalidBuildingId); - SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id); fillMaterials(sim, yardId, *def); // First tick: materials consumed, production cycle starts — no ship yet. @@ -166,7 +165,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]") const int shipsBefore = countShips(sim); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id); // Materials remain at zero (default after setRecipe); no cycle starts. const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); @@ -188,7 +187,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); - SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id); + SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id); const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); diff --git a/src/test/SimulationTestAccess.h b/src/test/SimulationTestAccess.h index 6b0f627..6243cd4 100644 --- a/src/test/SimulationTestAccess.h +++ b/src/test/SimulationTestAccess.h @@ -7,6 +7,7 @@ #include "BuildingId.h" #include "BuildingType.h" #include "Rotation.h" +#include "FactoryState.h" #include "Simulation.h" class BeltSystem; @@ -25,6 +26,7 @@ class BuildingSystem; struct SimulationTestAccess { static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); } + static FactoryState& state(Simulation& sim) { return sim.m_factoryState; } static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); } static std::optional place(Simulation& sim, BuildingType type, diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 3951be7..4a643b1 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -1605,8 +1605,8 @@ void GameWorldView::drawPortItems(QPainter& painter) painter.save(); painter.setClipRegion(clip); - m_sim->getBuildings().forEachEmergingItem(drawItem); - m_sim->getBuildings().forEachIncomingItem(drawItem); + m_sim->getBuildings().forEachEmergingItem(m_sim->getFactoryState(), drawItem); + m_sim->getBuildings().forEachIncomingItem(m_sim->getFactoryState(), drawItem); painter.restore(); }