make deconstruction its own system

This commit is contained in:
2026-08-05 07:10:46 +02:00
parent 1f4503176b
commit 60260540cd
8 changed files with 154 additions and 102 deletions

View File

@@ -181,7 +181,7 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current
state.deconstructionQueue.push_back(std::move(entry)); state.deconstructionQueue.push_back(std::move(entry));
if (wasEmpty) if (wasEmpty)
{ {
startFrontDeconstruction(state, currentTick); startFrontDeconstruction(state, m_config, currentTick);
} }
return 0; return 0;
} }
@@ -189,17 +189,6 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current
return 0; return 0;
} }
void BuildingSystem::startFrontDeconstruction(FactoryState& state, Tick currentTick)
{
if (state.deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = state.deconstructionQueue.front();
if (front.completesAt == 0)
{
front.completesAt =
currentTick + secondsToTicks(m_config.world.deconstructionTimeSeconds);
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Set recipe // Set recipe
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -334,53 +323,6 @@ void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id,
// Tick hooks // Tick hooks
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void BuildingSystem::tickDeconstruction(FactoryState& state, Tick currentTick)
{
TRACE();
if (state.deconstructionQueue.empty())
{
return;
}
DeconstructionEntry& front = state.deconstructionQueue.front();
// Guard: if the front entry's timer was never started, start it now.
if (front.completesAt == 0)
{
startFrontDeconstruction(state, currentTick);
return;
}
if (currentTick < front.completesAt)
{
return;
}
// 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<Building>::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);
state.grid.release(it->bodyCells);
state.buildings.erase(it);
if (def)
{
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
}
break;
}
state.deconstructionQueue.pop_front();
// Start the next queued deconstruction, if any.
startFrontDeconstruction(state, currentTick);
}
void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id) void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id)
{ {
for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin(); for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin();

View File

@@ -17,6 +17,7 @@
#include "Building.h" #include "Building.h"
#include "FactoryState.h" #include "FactoryState.h"
#include "BuildingBuffers.h" #include "BuildingBuffers.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h" #include "PlacementRules.h"
#include "ProductionRules.h" #include "ProductionRules.h"
#include "BuildingType.h" #include "BuildingType.h"
@@ -107,7 +108,6 @@ public:
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a // Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
// time, in parallel with tickConstruction. Removes the front building and // time, in parallel with tickConstruction. Removes the front building and
// credits its refund when its timer elapses. // credits its refund when its timer elapses.
void tickDeconstruction(FactoryState& state, Tick currentTick);
void tickBeltPull(FactoryState& state); void tickBeltPull(FactoryState& state);
void tickProduction(FactoryState& state, Tick currentTick); void tickProduction(FactoryState& state, Tick currentTick);
void tickShipyardProduction(FactoryState& state, Tick currentTick); void tickShipyardProduction(FactoryState& state, Tick currentTick);
@@ -183,7 +183,6 @@ public:
private: private:
// Starts the front deconstruction-queue entry's timer if not yet started // Starts the front deconstruction-queue entry's timer if not yet started
// (mirrors how tickConstruction starts a queued construction site). // (mirrors how tickConstruction starts a queued construction site).
void startFrontDeconstruction(FactoryState& state, Tick currentTick);
// Registers a belt/splitter/tunnel building's tile with the belt subsystem // Registers a belt/splitter/tunnel building's tile with the belt subsystem
// (on construction completion, or when un-queuing a deconstruction). No-op for // (on construction completion, or when un-queuing a deconstruction). No-op for

View File

@@ -14,6 +14,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
@@ -45,6 +46,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp

View File

@@ -0,0 +1,67 @@
#include "DeconstructionSystem.h"
#include <vector>
#include "Building.h"
#include "tracing.h"
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
Tick currentTick)
{
if (state.deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = state.deconstructionQueue.front();
if (front.completesAt == 0)
{
front.completesAt =
currentTick + secondsToTicks(config.world.deconstructionTimeSeconds);
}
}
void DeconstructionSystem::tick(FactoryState& state, Tick currentTick)
{
TRACE();
if (state.deconstructionQueue.empty())
{
return;
}
DeconstructionEntry& front = state.deconstructionQueue.front();
// Guard: if the front entry's timer was never started, start it now.
if (front.completesAt == 0)
{
startFrontDeconstruction(state, m_config, currentTick);
return;
}
if (currentTick < front.completesAt)
{
return;
}
// 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<Building>::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);
state.grid.release(it->bodyCells);
state.buildings.erase(it);
if (def)
{
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
}
break;
}
state.deconstructionQueue.pop_front();
// Start the next queued deconstruction, if any.
startFrontDeconstruction(state, m_config, currentTick);
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <functional>
#include "FactoryState.h"
#include "GameConfig.h"
#include "Tick.h"
// The queue timer for pending demolitions (REQ-BLD-DECON-QUEUE): one building at a
// time, in parallel with construction. When the front entry's timer elapses the
// building is removed from the world, its tiles are released, and its partial refund
// is credited.
//
// It needs no BeltSystem: a belt, splitter or tunnel end is unregistered the moment
// it is queued (see BuildingSystem::deconstruct), not when the timer completes.
//
// Holds the config and the refund sink; the world arrives per tick.
class DeconstructionSystem
{
public:
DeconstructionSystem(const GameConfig& config,
std::function<void(int)> addBuildingBlocks)
: m_config(config), m_addBuildingBlocks(std::move(addBuildingBlocks)) {}
void tick(FactoryState& state, Tick currentTick);
private:
const GameConfig& m_config;
std::function<void(int)> m_addBuildingBlocks;
};
// Starts the timer on the front entry of the deconstruction queue, if it has one and
// it has not started yet. Shared: BuildingSystem::deconstruct starts the timer when it
// queues the first entry, and DeconstructionSystem restarts it after each completion.
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
Tick currentTick);

View File

@@ -2,6 +2,7 @@
#include "FactoryQueries.h" #include "FactoryQueries.h"
#include "ConstructionSystem.h" #include "ConstructionSystem.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h" #include "PlacementRules.h"
#include <algorithm> #include <algorithm>
@@ -125,6 +126,8 @@ void Simulation::initializeSubsystems()
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); }, [this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
m_rng); m_rng);
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config); m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
m_deconstructionSystem = std::make_unique<DeconstructionSystem>(
m_config, [this](int amount) { m_buildingBlocksStock += amount; });
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin); m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>(m_config); m_aiSystem = std::make_unique<AiSystem>(m_config);
m_movementIntentSystem = std::make_unique<MovementIntentSystem>(); m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
@@ -246,7 +249,7 @@ void Simulation::tick()
// Construction + production pipeline // Construction + production pipeline
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick); m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
m_buildingSystem->tickDeconstruction(m_factoryState, m_currentTick); // parallel to construction m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction
m_buildingSystem->tickBeltPull(m_factoryState); // step 3 m_buildingSystem->tickBeltPull(m_factoryState); // step 3
m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4 m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b

View File

@@ -26,6 +26,7 @@
class AiSystem; class AiSystem;
class BuildingSystem; class BuildingSystem;
class ConstructionSystem; class ConstructionSystem;
class DeconstructionSystem;
struct Command; struct Command;
class Hasher; class Hasher;
class CombatSystem; class CombatSystem;
@@ -220,6 +221,7 @@ private:
BeltSystem m_beltSystem; BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem; std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ConstructionSystem> m_constructionSystem; std::unique_ptr<ConstructionSystem> m_constructionSystem;
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
std::unique_ptr<ShipSystem> m_shipSystem; std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<AiSystem> m_aiSystem; std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem; std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;

View File

@@ -16,6 +16,7 @@
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "ConstructionSystem.h" #include "ConstructionSystem.h"
#include "DeconstructionSystem.h"
#include "FactoryState.h" #include "FactoryState.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "ConfigLoader.h" #include "ConfigLoader.h"
@@ -55,12 +56,12 @@ static Port westPort(QPoint tile)
// Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick. // Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick.
static void runTicks(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state_bs, static void runTicks(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state_bs,
BeltSystem& belts, int n, Tick& tick) BeltSystem& belts, int& stock, int n, Tick& tick)
{ {
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
{ {
ConstructionSystem(cfg).tick(state_bs, belts, tick); ConstructionSystem(cfg).tick(state_bs, belts, tick);
bs.tickDeconstruction(state_bs, tick); DeconstructionSystem(cfg, [&stock](int n) { stock += n; }).tick(state_bs, tick);
bs.tickBeltPull(state_bs); bs.tickBeltPull(state_bs);
bs.tickProduction(state_bs, tick); bs.tickProduction(state_bs, tick);
bs.tickOutputBelts(state_bs); bs.tickOutputBelts(state_bs);
@@ -241,7 +242,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
// Complete construction (1 s). // Complete construction (1 s).
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
REQUIRE(getAllBuildings(state_bs).size() == 1); REQUIRE(getAllBuildings(state_bs).size() == 1);
@@ -354,7 +355,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
// Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300. // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
// We need to process tick 300 itself, so run 301 ticks (ticks 0..300). // We need to process tick 300 itself, so run 301 ticks (ticks 0..300).
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getAllSites(state_bs).empty()); REQUIRE(getAllSites(state_bs).empty());
REQUIRE(findBuilding(state_bs, id) != nullptr); REQUIRE(findBuilding(state_bs, id) != nullptr);
@@ -369,7 +370,7 @@ static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick)
{ {
for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i) for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i)
{ {
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick);
} }
REQUIRE(findBuilding(f.state, id) != nullptr); REQUIRE(findBuilding(f.state, id) != nullptr);
} }
@@ -394,7 +395,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 // After the deconstruction time (0.1s = 3 ticks) it is removed and the partial
// refund (15 * 75 / 100 = 11) is credited exactly once. // refund (15 * 75 / 100 = 11) is credited exactly once.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
REQUIRE(findBuilding(f.state, id) == nullptr); REQUIRE(findBuilding(f.state, id) == nullptr);
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0))); REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0)));
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100); REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
@@ -418,14 +419,14 @@ TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time",
// After one deconstruction interval only the front building is gone; the // After one deconstruction interval only the front building is gone; the
// second is still queued and its refund not yet credited. // second is still queued and its refund not yet credited.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
REQUIRE(findBuilding(f.state, a) == nullptr); REQUIRE(findBuilding(f.state, a) == nullptr);
REQUIRE(findBuilding(f.state, b) != nullptr); REQUIRE(findBuilding(f.state, b) != nullptr);
REQUIRE(isQueuedForDeconstruction(f.state, b)); REQUIRE(isQueuedForDeconstruction(f.state, b));
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100); REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
// The second drains next. // The second drains next.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
REQUIRE(findBuilding(f.state, b) == nullptr); REQUIRE(findBuilding(f.state, b) == nullptr);
REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100)); REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100));
} }
@@ -451,7 +452,7 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n
REQUIRE(f.stock == 0); REQUIRE(f.stock == 0);
// It is never removed even after more than a deconstruction interval passes. // It is never removed even after more than a deconstruction interval passes.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
REQUIRE(findBuilding(f.state, id) != nullptr); REQUIRE(findBuilding(f.state, id) != nullptr);
REQUIRE(f.stock == 0); REQUIRE(f.stock == 0);
} }
@@ -521,7 +522,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
// Process through tick 300 to complete first miner's construction. // Process through tick 300 to complete first miner's construction.
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getAllSites(state_bs).size() == 1); REQUIRE(getAllSites(state_bs).size() == 1);
REQUIRE(getAllSites(state_bs).front().id == id2); REQUIRE(getAllSites(state_bs).front().id == id2);
@@ -553,7 +554,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
Tick tick = 0; Tick tick = 0;
// Construction completes on tick 300; production cycle starts tick 300, // Construction completes on tick 300; production cycle starts tick 300,
// completes on tick 330. Process through tick 330: 331 ticks total. // completes on tick 330. Process through tick 330: 331 ticks total.
runTicks(bs, cfg, state_bs, belts, runTicks(bs, cfg, state_bs, belts, stock,
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1, static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
tick); tick);
@@ -590,7 +591,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
// Cycle 2 starts at tick 331 (completesAt=361). // Cycle 2 starts at tick 331 (completesAt=361).
// Cycle 2 completes at tick 361: deposit item → buffer=2, cycle 3 stalls. // Cycle 2 completes at tick 361: deposit item → buffer=2, cycle 3 stalls.
// Need to process through tick 361: 362 ticks total. // Need to process through tick 361: 362 ticks total.
runTicks(bs, cfg, state_bs, belts, runTicks(bs, cfg, state_bs, belts, stock,
static_cast<int>(secondsToTicks(10.0)) static_cast<int>(secondsToTicks(10.0))
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 2, + 2 * static_cast<int>(secondsToTicks(1.0)) + 2,
tick); tick);
@@ -632,10 +633,10 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
// The queue builds one at a time: miner (10s) completes at tick 300, then // 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. // the smelter (15s) starts and completes at tick 300 + 450 = 750.
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getProductionBuildingCount(state_bs) == 1); REQUIRE(getProductionBuildingCount(state_bs) == 1);
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(15.0)), tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(15.0)), tick);
REQUIRE(getProductionBuildingCount(state_bs) == 2); REQUIRE(getProductionBuildingCount(state_bs) == 2);
// Neither is producing yet: the miner has no recipe selected, and the // Neither is producing yet: the miner has no recipe selected, and the
@@ -643,7 +644,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); REQUIRE(getActiveProductionBuildingCount(state_bs) == 0);
bs.setRecipe(state_bs, minerId, "mine_iron_ore"); bs.setRecipe(state_bs, minerId, "mine_iron_ore");
runTicks(bs, cfg, state_bs, belts, 1, tick); runTicks(bs, cfg, state_bs, belts, stock, 1, tick);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 1); REQUIRE(getActiveProductionBuildingCount(state_bs) == 1);
} }
@@ -671,12 +672,12 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); REQUIRE(getActiveProductionBuildingCount(state_bs) == 0);
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330). // Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getActiveProductionBuildingCount(state_bs) == 1); REQUIRE(getActiveProductionBuildingCount(state_bs) == 1);
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the // Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER). // output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
runTicks(bs, cfg, state_bs, belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
const Building* b = findBuilding(state_bs, id); const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
@@ -714,7 +715,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
// Complete construction (15s → tick 450+1 = 451 ticks). // Complete construction (15s → tick 450+1 = 451 ticks).
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
// Place west-flowing belt at (2,0): belt flows West, delivers to smelter. // Place west-flowing belt at (2,0): belt flows West, delivers to smelter.
belts.placeBelt(QPoint(2, 0), Rotation::West); belts.placeBelt(QPoint(2, 0), Rotation::West);
@@ -751,7 +752,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(); const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
belts.placeBelt(QPoint(2, 0), Rotation::West); belts.placeBelt(QPoint(2, 0), Rotation::West);
belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
@@ -794,7 +795,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0).value(); QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
// Feed scrap via an input belt without ever running production (only pull), so // 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. // the buffer fills and stays full. Try to over-fill it well past the cap.
@@ -837,7 +838,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(); const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
// Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a // Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a
// west-flowing belt at input port (2,0). // west-flowing belt at input port (2,0).
@@ -850,7 +851,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
} }
// iron_ingot recipe cycle is 2s; run to completion. // iron_ingot recipe cycle is 2s; run to completion.
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
const Building* b = findBuilding(state_bs, sid); const Building* b = findBuilding(state_bs, sid);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
@@ -884,7 +885,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(); const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
// Feed 1 iron_ore (iron_ingot needs 2 — incomplete) then 2 copper_ore // Feed 1 iron_ore (iron_ingot needs 2 — incomplete) then 2 copper_ore
// (copper_ingot needs 2 — satisfiable) via the west-flowing input belt. // (copper_ingot needs 2 — satisfiable) via the west-flowing input belt.
@@ -898,7 +899,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
} }
// copper_ingot cycle is 2.5s; run to completion. // copper_ingot cycle is 2.5s; run to completion.
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
const Building* b = findBuilding(state_bs, sid); const Building* b = findBuilding(state_bs, sid);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
@@ -944,13 +945,13 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
Tick tick = 0; Tick tick = 0;
// Construction (10s) + 1 production cycle (1s) + 1 extra tick. // Construction (10s) + 1 production cycle (1s) + 1 extra tick.
runTicks(bs, cfg, state_bs, belts, runTicks(bs, cfg, state_bs, belts, stock,
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1, static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
tick); tick);
// Item should have been pushed onto the belt this tick or a subsequent one. // 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. // Run one more tick to ensure tickBeltPush fires after the deposit tick.
runTicks(bs, cfg, state_bs, belts, 1, tick); runTicks(bs, cfg, state_bs, belts, stock, 1, tick);
const std::optional<Item> item = belts.tryTakeItem(eastPort(QPoint(1, 1))); const std::optional<Item> item = belts.tryTakeItem(eastPort(QPoint(1, 1)));
REQUIRE(item.has_value()); REQUIRE(item.has_value());
@@ -986,7 +987,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p
Tick tick = 0; Tick tick = 0;
// Smelter build (15s) + margin for coupling and a smelt cycle. // Smelter build (15s) + margin for coupling and a smelt cycle.
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(30.0)), tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(30.0)), tick);
const Building* smelter = findBuilding(state_bs, smelterId); const Building* smelter = findBuilding(state_bs, smelterId);
REQUIRE(smelter != nullptr); REQUIRE(smelter != nullptr);
@@ -1026,7 +1027,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
Tick tick = 0; Tick tick = 0;
// Both miners build sequentially (10s each), then the producer runs and jams. // Both miners build sequentially (10s each), then the producer runs and jams.
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(25.0)), tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(25.0)), tick);
const Building* miner = findBuilding(state_bs, minerId); const Building* miner = findBuilding(state_bs, minerId);
const Building* sink = findBuilding(state_bs, sinkId); const Building* sink = findBuilding(state_bs, sinkId);
@@ -1062,7 +1063,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
Tick tick = 0; Tick tick = 0;
// Run until first item is in output buffer. // Run until first item is in output buffer.
runTicks(bs, cfg, state_bs, belts, runTicks(bs, cfg, state_bs, belts, stock,
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1, static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
tick); tick);
@@ -1108,7 +1109,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
// Complete construction (25s). // Complete construction (25s).
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
const Building* b = findBuilding(state_bs, id); const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
@@ -1141,7 +1142,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
// Complete construction (25s). // Complete construction (25s).
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
// Feed 5 scrap into the building via a belt at an input port. // Feed 5 scrap into the building via a belt at an input port.
// Reprocessing plant body (East rotation) = 3×3 at (0,0). // Reprocessing plant body (East rotation) = 3×3 at (0,0).
@@ -1163,7 +1164,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
} }
// Run production cycle (3s = 90 ticks + 1 for the completion tick). // Run production cycle (3s = 90 ticks + 1 for the completion tick).
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(3.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(3.0)) + 1, tick);
const Building* b = findBuilding(state_bs, id); const Building* b = findBuilding(state_bs, id);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
@@ -1240,7 +1241,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(); const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(getAllSites(state_bs).empty()); REQUIRE(getAllSites(state_bs).empty());
const std::optional<BuildingId> result = const std::optional<BuildingId> result =
@@ -1422,7 +1423,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(); const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(findBuilding(state_bs, id) != nullptr); REQUIRE(findBuilding(state_bs, id) != nullptr);
const Building& before = *findBuilding(state_bs, id); const Building& before = *findBuilding(state_bs, id);
@@ -1454,7 +1455,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(); const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, cfg, state_bs, belts, stock, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
bs.rotateInPlace(state_bs, id, Rotation::North); bs.rotateInPlace(state_bs, id, Rotation::North);
@@ -1474,7 +1475,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split
Tick tick = 0; Tick tick = 0;
while (getAllBuildings(f.state).empty() && tick < 100000) while (getAllBuildings(f.state).empty() && tick < 100000)
{ {
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick);
} }
REQUIRE(getAllBuildings(f.state).size() == 1); REQUIRE(getAllBuildings(f.state).size() == 1);
@@ -1519,7 +1520,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
Tick tick = 0; Tick tick = 0;
while (getAllBuildings(f.state).empty() && tick < 100000) while (getAllBuildings(f.state).empty() && tick < 100000)
{ {
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick);
} }
REQUIRE(getAllBuildings(f.state).size() == 1); REQUIRE(getAllBuildings(f.state).size() == 1);
REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter); REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter);
@@ -1674,11 +1675,11 @@ namespace
// Advances the sim until the given site becomes an operational building, or a // Advances the sim until the given site becomes an operational building, or a
// safety cap is reached. // safety cap is reached.
void buildToCompletion(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state, void buildToCompletion(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state,
BeltSystem& belts, BuildingId id, Tick& tick) BeltSystem& belts, int& stock, BuildingId id, Tick& tick)
{ {
for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i) for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i)
{ {
runTicks(bs, cfg, state, belts, 1, tick); runTicks(bs, cfg, state, belts, stock, 1, tick);
} }
} }
} }
@@ -1713,7 +1714,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 BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
const std::vector<Port> sitePorts = getInputPorts(f.state, f.cfg, id); const std::vector<Port> sitePorts = getInputPorts(f.state, f.cfg, id);
buildToCompletion(f.bs, f.cfg, f.state, f.belts, id, tick); buildToCompletion(f.bs, f.cfg, f.state, f.belts, f.stock, id, tick);
REQUIRE(findBuilding(f.state, id) != nullptr); REQUIRE(findBuilding(f.state, id) != nullptr);
const std::vector<Port> builtPorts = getInputPorts(f.state, f.cfg, id); const std::vector<Port> builtPorts = getInputPorts(f.state, f.cfg, id);