diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 3d7afcd..f989202 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -334,109 +334,6 @@ void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id, // Tick hooks // --------------------------------------------------------------------------- -void BuildingSystem::tickConstruction(FactoryState& state, Tick currentTick) -{ - TRACE(); - if (state.constructionQueue.empty()) - { - return; - } - - ConstructionSite& front = state.constructionQueue.front(); - - // Guard: if somehow the front site was never started, start it now. - if (front.completesAt == 0) - { - const BuildingDef* def = m_config.buildings.findBuildingDef(front.type); - if (def) - { - front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds); - } - return; - } - - if (currentTick < front.completesAt) - { - return; - } - - // Promote construction site to an operational Building. - const BuildingDef* def = m_config.buildings.findBuildingDef(front.type); - const ParsedSurfaceMask mask = parseSurfaceMask( - def ? def->surfaceMask : std::vector{}, - front.rotation); - - Building building; - building.id = front.id; - building.anchor = front.anchor; - building.footprint = front.footprint; - building.rotation = front.rotation; - building.type = front.type; - building.recipeId = front.recipeId; - building.shipLayout = front.shipLayout; - - for (const QPoint& cell : mask.bodyCells) - { - building.bodyCells.push_back(front.anchor + cell); - } - for (const Port& port : mask.outputPorts) - { - Port absPort; - absPort.tile = front.anchor + port.tile; - absPort.direction = port.direction; - building.outputPorts.push_back(absPort); - } - building.emergingItems.resize(building.outputPorts.size()); - building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts); - building.incomingItems.assign(building.inputPorts.size(), {}); - - if (building.type == BuildingType::SalvageBay) - { - initSalvageBayBuffer(m_config, building); - } - else if (isAutoRecipeBuildingType(building.type)) - { - // Smelter/Reprocessing Plant need no recipe selection; buffers are set - // up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). - initAutoBuffers(m_config, building); - } - else if (!building.recipeId.empty()) - { - if (building.type == BuildingType::Shipyard) - { - initShipyardBuffers(m_config, building); - } - else - { - const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type); - if (recipe) - { - initBuffers(building, *recipe); - } - } - } - - // Register with BeltSystem before the move (mask/building stays valid). Any - // filters configured while under construction carry over (REQ-BLD-SITE-CONFIG). - reregisterBeltTile(m_belts, m_config, building, front.splitterFilterA, front.splitterFilterB); - - state.buildings.push_back(std::move(building)); - - state.constructionQueue.pop_front(); - - // Start next queued site if present. - if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0) - { - const BuildingDef* nextDef = - m_config.buildings.findBuildingDef(state.constructionQueue.front().type); - if (nextDef) - { - state.constructionQueue.front().completesAt = - currentTick + secondsToTicks(nextDef->constructionTimeSeconds); - } - } -} - void BuildingSystem::tickDeconstruction(FactoryState& state, Tick currentTick) { TRACE(); diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index 1e67a76..27e8c9f 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -104,7 +104,6 @@ public: const std::vector& filterB); // -- Tick hooks (called from Simulation::tick in the documented order) --- - 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. diff --git a/src/lib/sim/CMakeLists.txt b/src/lib/sim/CMakeLists.txt index 4ad6779..06ba44a 100644 --- a/src/lib/sim/CMakeLists.txt +++ b/src/lib/sim/CMakeLists.txt @@ -13,6 +13,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/Building.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h + ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h @@ -43,6 +44,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp diff --git a/src/lib/sim/ConstructionSystem.cpp b/src/lib/sim/ConstructionSystem.cpp new file mode 100644 index 0000000..7e04c30 --- /dev/null +++ b/src/lib/sim/ConstructionSystem.cpp @@ -0,0 +1,112 @@ +#include "ConstructionSystem.h" + +#include "BuildingBuffers.h" +#include "BuildingType.h" +#include "FactoryQueries.h" +#include "PortGeometry.h" +#include "SurfaceMask.h" +#include "tracing.h" + +void ConstructionSystem::tick(FactoryState& state, BeltSystem& belts, Tick currentTick) +{ + TRACE(); + if (state.constructionQueue.empty()) + { + return; + } + + ConstructionSite& front = state.constructionQueue.front(); + + // Guard: if somehow the front site was never started, start it now. + if (front.completesAt == 0) + { + const BuildingDef* def = m_config.buildings.findBuildingDef(front.type); + if (def) + { + front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds); + } + return; + } + + if (currentTick < front.completesAt) + { + return; + } + + // Promote construction site to an operational Building. + const BuildingDef* def = m_config.buildings.findBuildingDef(front.type); + const ParsedSurfaceMask mask = parseSurfaceMask( + def ? def->surfaceMask : std::vector{}, + front.rotation); + + Building building; + building.id = front.id; + building.anchor = front.anchor; + building.footprint = front.footprint; + building.rotation = front.rotation; + building.type = front.type; + building.recipeId = front.recipeId; + building.shipLayout = front.shipLayout; + + for (const QPoint& cell : mask.bodyCells) + { + building.bodyCells.push_back(front.anchor + cell); + } + for (const Port& port : mask.outputPorts) + { + Port absPort; + absPort.tile = front.anchor + port.tile; + absPort.direction = port.direction; + building.outputPorts.push_back(absPort); + } + building.emergingItems.resize(building.outputPorts.size()); + building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts); + building.incomingItems.assign(building.inputPorts.size(), {}); + + if (building.type == BuildingType::SalvageBay) + { + initSalvageBayBuffer(m_config, building); + } + else if (isAutoRecipeBuildingType(building.type)) + { + // Smelter/Reprocessing Plant need no recipe selection; buffers are set + // up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). + initAutoBuffers(m_config, building); + } + else if (!building.recipeId.empty()) + { + if (building.type == BuildingType::Shipyard) + { + initShipyardBuffers(m_config, building); + } + else + { + const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type); + if (recipe) + { + initBuffers(building, *recipe); + } + } + } + + // Register with BeltSystem before the move (mask/building stays valid). Any + // filters configured while under construction carry over (REQ-BLD-SITE-CONFIG). + reregisterBeltTile(belts, m_config, building, front.splitterFilterA, front.splitterFilterB); + + state.buildings.push_back(std::move(building)); + + state.constructionQueue.pop_front(); + + // Start next queued site if present. + if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0) + { + const BuildingDef* nextDef = + m_config.buildings.findBuildingDef(state.constructionQueue.front().type); + if (nextDef) + { + state.constructionQueue.front().completesAt = + currentTick + secondsToTicks(nextDef->constructionTimeSeconds); + } + } +} + diff --git a/src/lib/sim/ConstructionSystem.h b/src/lib/sim/ConstructionSystem.h new file mode 100644 index 0000000..6ef5dc7 --- /dev/null +++ b/src/lib/sim/ConstructionSystem.h @@ -0,0 +1,30 @@ +#pragma once + +#include "BeltSystem.h" +#include "FactoryState.h" +#include "GameConfig.h" +#include "Tick.h" + +// Advances the construction queue and turns a finished site into an operational +// building (REQ-BLD-CONSTRUCTION). One site is built at a time, in queue order: +// the front site's timer runs, and when it elapses the site becomes a Building — +// its ports and buffers are derived from its definition, its belt tile is handed +// back to BeltSystem, and the next queued site starts. +// +// It completes the building itself rather than handing the finished site back to +// BuildingSystem: everything materialisation needs is either in FactoryState, the +// config, or a free function (see BuildingBuffers.h, PortGeometry.h), so there is +// no intermediate value to pass and no ordering rule between two calls. +// +// Holds only the config; the world it works on arrives per tick, like the other +// systems in lib/ecs/system. +class ConstructionSystem +{ +public: + explicit ConstructionSystem(const GameConfig& config) : m_config(config) {} + + void tick(FactoryState& state, BeltSystem& belts, Tick currentTick); + +private: + const GameConfig& m_config; +}; diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 57b196b..12eec68 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -1,6 +1,7 @@ #include "Simulation.h" #include "FactoryQueries.h" +#include "ConstructionSystem.h" #include "PlacementRules.h" #include @@ -123,6 +124,7 @@ void Simulation::initializeSubsystems() }, [this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); }, m_rng); + m_constructionSystem = std::make_unique(m_config); m_shipSystem = std::make_unique(m_config, m_admin); m_aiSystem = std::make_unique(m_config); m_movementIntentSystem = std::make_unique(); @@ -243,7 +245,7 @@ void Simulation::tick() m_waveSystem->tickThreatAccumulation(); // Construction + production pipeline - m_buildingSystem->tickConstruction(m_factoryState, m_currentTick); + m_constructionSystem->tick(m_factoryState, m_beltSystem, 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 diff --git a/src/lib/sim/Simulation.h b/src/lib/sim/Simulation.h index db01c94..cc3e2d0 100644 --- a/src/lib/sim/Simulation.h +++ b/src/lib/sim/Simulation.h @@ -25,6 +25,7 @@ class AiSystem; class BuildingSystem; +class ConstructionSystem; struct Command; class Hasher; class CombatSystem; @@ -218,6 +219,7 @@ private: FactoryState m_factoryState; BeltSystem m_beltSystem; std::unique_ptr m_buildingSystem; + std::unique_ptr m_constructionSystem; std::unique_ptr m_shipSystem; std::unique_ptr m_aiSystem; std::unique_ptr m_movementIntentSystem; diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index ed7ef27..dc99699 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -15,6 +15,7 @@ #include "BeltSystem.h" #include "Building.h" #include "BuildingSystem.h" +#include "ConstructionSystem.h" #include "FactoryState.h" #include "BuildingType.h" #include "ConfigLoader.h" @@ -63,6 +64,7 @@ struct Fixture std::mt19937 rng; EntityAdmin admin; BuildingSystem buildings; + ConstructionSystem construction; ShipSystem ships; AiSystem ai; SalvagerSystem salvager; @@ -85,6 +87,7 @@ struct Fixture [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng) + , construction(cfg) , ships(cfg, admin) , ai(cfg) , salvager(admin) @@ -957,7 +960,7 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b Tick t = 0; for (int i = 0; i < 500; ++i) { - f.buildings.tickConstruction(f.state, t++); + f.construction.tick(f.state, f.belts, t++); if (findBuilding(f.state, bayId) != nullptr) { break; @@ -992,7 +995,7 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo", Tick t = 0; for (int i = 0; i < 500; ++i) { - f.buildings.tickConstruction(f.state, t++); + f.construction.tick(f.state, f.belts, t++); if (findBuilding(f.state, bayId) != nullptr) { break; } } const Building* bay = findBuilding(f.state, bayId); diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 45c46d5..01371ac 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -15,6 +15,7 @@ #include "BeltSystem.h" #include "Building.h" #include "BuildingSystem.h" +#include "ConstructionSystem.h" #include "FactoryState.h" #include "BuildingType.h" #include "ConfigLoader.h" @@ -53,12 +54,12 @@ static Port westPort(QPoint tile) } // Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick. -static void runTicks(BuildingSystem& bs, FactoryState& state_bs, BeltSystem& belts, - int n, Tick& tick) +static void runTicks(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state_bs, + BeltSystem& belts, int n, Tick& tick) { for (int i = 0; i < n; ++i) { - bs.tickConstruction(state_bs, tick); + ConstructionSystem(cfg).tick(state_bs, belts, tick); bs.tickDeconstruction(state_bs, tick); bs.tickBeltPull(state_bs); bs.tickProduction(state_bs, tick); @@ -240,7 +241,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con // Complete construction (1 s). Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, cfg, 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); @@ -353,7 +354,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[ // 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, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getAllSites(state_bs).empty()); REQUIRE(findBuilding(state_bs, id) != nullptr); @@ -368,7 +369,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.state, f.belts, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); } REQUIRE(findBuilding(f.state, id) != nullptr); } @@ -393,7 +394,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.state, f.belts, static_cast(secondsToTicks(0.1)) + 1, tick); + runTicks(f.bs, f.cfg, 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); @@ -417,14 +418,14 @@ TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time", // 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.state, f.belts, static_cast(secondsToTicks(0.1)) + 1, tick); + runTicks(f.bs, f.cfg, 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.state, f.belts, static_cast(secondsToTicks(0.1)) + 2, tick); + runTicks(f.bs, f.cfg, 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)); } @@ -450,7 +451,7 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n REQUIRE(f.stock == 0); // It is never removed even after more than a deconstruction interval passes. - runTicks(f.bs, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 5, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 5, tick); REQUIRE(findBuilding(f.state, id) != nullptr); REQUIRE(f.stock == 0); } @@ -520,7 +521,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil // Process through tick 300 to complete first miner's construction. Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getAllSites(state_bs).size() == 1); REQUIRE(getAllSites(state_bs).front().id == id2); @@ -552,7 +553,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui 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, state_bs, belts, + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); @@ -589,7 +590,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, state_bs, belts, + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(10.0)) + 2 * static_cast(secondsToTicks(1.0)) + 2, tick); @@ -631,10 +632,10 @@ 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, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getProductionBuildingCount(state_bs) == 1); - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)), tick); + runTicks(bs, cfg, 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 @@ -642,7 +643,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); bs.setRecipe(state_bs, minerId, "mine_iron_ore"); - runTicks(bs, state_bs, belts, 1, tick); + runTicks(bs, cfg, state_bs, belts, 1, tick); REQUIRE(getActiveProductionBuildingCount(state_bs) == 1); } @@ -670,12 +671,12 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); // Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330). - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(bs, cfg, 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, state_bs, belts, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); const Building* b = findBuilding(state_bs, id); REQUIRE(b != nullptr); @@ -713,7 +714,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing // Complete construction (15s → tick 450+1 = 451 ticks). Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, cfg, 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); @@ -750,7 +751,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, cfg, 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")); @@ -793,7 +794,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(bs, cfg, 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. @@ -836,7 +837,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, cfg, 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). @@ -849,7 +850,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", } // iron_ingot recipe cycle is 2s; run to completion. - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(2.0)) + 2, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(2.0)) + 2, tick); const Building* b = findBuilding(state_bs, sid); REQUIRE(b != nullptr); @@ -883,7 +884,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(bs, cfg, 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. @@ -897,7 +898,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete } // copper_ingot cycle is 2.5s; run to completion. - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(2.5)) + 2, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(2.5)) + 2, tick); const Building* b = findBuilding(state_bs, sid); REQUIRE(b != nullptr); @@ -943,13 +944,13 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui Tick tick = 0; // Construction (10s) + 1 production cycle (1s) + 1 extra tick. - runTicks(bs, state_bs, belts, + runTicks(bs, cfg, 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, state_bs, belts, 1, tick); + runTicks(bs, cfg, state_bs, belts, 1, tick); const std::optional item = belts.tryTakeItem(eastPort(QPoint(1, 1))); REQUIRE(item.has_value()); @@ -985,7 +986,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p Tick tick = 0; // Smelter build (15s) + margin for coupling and a smelt cycle. - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(30.0)), tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(30.0)), tick); const Building* smelter = findBuilding(state_bs, smelterId); REQUIRE(smelter != nullptr); @@ -1025,7 +1026,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu Tick tick = 0; // Both miners build sequentially (10s each), then the producer runs and jams. - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)), tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(25.0)), tick); const Building* miner = findBuilding(state_bs, minerId); const Building* sink = findBuilding(state_bs, sinkId); @@ -1061,7 +1062,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production" Tick tick = 0; // Run until first item is in output buffer. - runTicks(bs, state_bs, belts, + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); @@ -1107,7 +1108,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max // Complete construction (25s). Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); const Building* b = findBuilding(state_bs, id); REQUIRE(b != nullptr); @@ -1140,7 +1141,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta // Complete construction (25s). Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(bs, cfg, 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). @@ -1162,7 +1163,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, state_bs, belts, static_cast(secondsToTicks(3.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(3.0)) + 1, tick); const Building* b = findBuilding(state_bs, id); REQUIRE(b != nullptr); @@ -1239,7 +1240,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(getAllSites(state_bs).empty()); const std::optional result = @@ -1421,7 +1422,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(findBuilding(state_bs, id) != nullptr); const Building& before = *findBuilding(state_bs, id); @@ -1453,7 +1454,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(bs, cfg, state_bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); bs.rotateInPlace(state_bs, id, Rotation::North); @@ -1473,7 +1474,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split Tick tick = 0; while (getAllBuildings(f.state).empty() && tick < 100000) { - runTicks(f.bs, f.state, f.belts, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); } REQUIRE(getAllBuildings(f.state).size() == 1); @@ -1518,7 +1519,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.state, f.belts, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); } REQUIRE(getAllBuildings(f.state).size() == 1); REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter); @@ -1672,12 +1673,12 @@ namespace // Advances the sim until the given site becomes an operational building, or a // safety cap is reached. - void buildToCompletion(BuildingSystem& bs, FactoryState& state, + void buildToCompletion(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state, BeltSystem& belts, BuildingId id, Tick& tick) { for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i) { - runTicks(bs, state, belts, 1, tick); + runTicks(bs, cfg, state, belts, 1, tick); } } } @@ -1712,7 +1713,7 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu 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); + buildToCompletion(f.bs, f.cfg, f.state, f.belts, id, tick); REQUIRE(findBuilding(f.state, id) != nullptr); const std::vector builtPorts = getInputPorts(f.state, f.cfg, id);