make construction its own system
ConstructionSystem takes over the construction queue: it runs the front site's timer and, when it elapses, builds the Building itself — ports, buffers, belt registration, and starting the next queued site. Simulation::tick calls it directly, in the same position tickConstruction held. No handoff. The earlier sketch had it return the completed site for BuildingSystem to materialise, which put an intermediate value in Simulation and made the two calls correct only when adjacent and ordered. Once the queries and the buffer helpers became free functions there was nothing left on BuildingSystem that materialisation needed, so the system does the whole job and the invariant disappears rather than being documented. Holds only the config; the world arrives per tick, like the systems in lib/ecs/system. Verified with a golden-checksum capture before and after — all four sample ticks identical, which is the check that matters here since this moves a call in the tick order. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
@@ -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<std::string>{},
|
||||
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();
|
||||
|
||||
@@ -104,7 +104,6 @@ public:
|
||||
const std::vector<ItemType>& 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
112
src/lib/sim/ConstructionSystem.cpp
Normal file
112
src/lib/sim/ConstructionSystem.cpp
Normal file
@@ -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<std::string>{},
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
src/lib/sim/ConstructionSystem.h
Normal file
30
src/lib/sim/ConstructionSystem.h
Normal file
@@ -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;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Simulation.h"
|
||||
|
||||
#include "FactoryQueries.h"
|
||||
#include "ConstructionSystem.h"
|
||||
#include "PlacementRules.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -123,6 +124,7 @@ void Simulation::initializeSubsystems()
|
||||
},
|
||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||
m_rng);
|
||||
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||
@@ -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
|
||||
|
||||
@@ -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<BuildingSystem> m_buildingSystem;
|
||||
std::unique_ptr<ConstructionSystem> m_constructionSystem;
|
||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||
std::unique_ptr<AiSystem> m_aiSystem;
|
||||
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
||||
|
||||
@@ -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<ShipLayoutConfig>&) {},
|
||||
[](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);
|
||||
|
||||
@@ -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<int>(secondsToTicks(1.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(0.1)) + 1, tick);
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(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<int>(secondsToTicks(0.1)) + 1, tick);
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(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<int>(secondsToTicks(0.1)) + 2, tick);
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(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<int>(secondsToTicks(0.1)) + 5, tick);
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(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<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(10.0)) + static_cast<int>(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<int>(secondsToTicks(10.0))
|
||||
+ 2 * static_cast<int>(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<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
REQUIRE(getProductionBuildingCount(state_bs) == 1);
|
||||
|
||||
runTicks(bs, state_bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(1.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, 2 * static_cast<int>(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<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(25.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(2.0)) + 2, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(2.5)) + 2, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(10.0)) + static_cast<int>(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> 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<int>(secondsToTicks(30.0)), tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(25.0)), tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(10.0)) + static_cast<int>(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<int>(secondsToTicks(25.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(25.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(3.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(1.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||
REQUIRE(getAllSites(state_bs).empty());
|
||||
|
||||
const std::optional<BuildingId> 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<int>(secondsToTicks(1.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<int>(secondsToTicks(1.0)) + 1, tick);
|
||||
runTicks(bs, cfg, state_bs, belts, static_cast<int>(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<Port> 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<Port> builtPorts = getInputPorts(f.state, f.cfg, id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user