separate what buildings are from what flows through them

BuildingSystem was four unrelated jobs in one class: building lifecycle,
building configuration, the per-tick material flow, and (until last commit) the
checksum. The flow was the odd one out -- it is what the RNG, the ship spawner
and the unlock test were held for, none of which placement, rotation or
demolition has any business reaching.

Tick steps 3 to 5 move to a new ProductionSystem: tickBeltPull, tickProduction,
tickShipyardProduction, tickOutputBelts, their five private helpers and
rollOutputGroup. The cut is clean in both directions -- nothing in the block
called a topology or configuration method, and nothing outside it called the
helpers -- so the bodies move verbatim; a scripted comparison against the old
file confirms all nine differ only by class qualifier, the m_belts -> belts
rename, and the two added parameters. (Seven em dashes in comments became `--`;
the new file is ASCII, as the guidelines require.)

Belts arrive per tick rather than being held, matching ConstructionSystem, and
only the two methods that touch them take the parameter. BuildingSystem is left
holding the config and the belts, and its constructor takes exactly those two.
The arena constructs no ProductionSystem at all: it stages ships directly and
never runs a factory.

Determinism rests on the RNG stream: step 4's weighted output-group pick is the
factory's only draw, so the four calls must keep their order and position in
Simulation::tick. They do, and the tick-order section now says why, since no
test can catch a reordering here.

913 lines of BuildingSystem.cpp become 444 there and 483 in ProductionSystem.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-20 07:41:20 +02:00
parent 04a52698e8
commit 46649e7dd1
12 changed files with 708 additions and 634 deletions

View File

@@ -17,6 +17,7 @@
#include "Building.h"
#include "BuildingBuffers.h"
#include "BuildingSystem.h"
#include "ProductionSystem.h"
#include "ConstructionSystem.h"
#include "DeconstructionSystem.h"
#include "FactoryState.h"
@@ -57,16 +58,16 @@ static Port westPort(QPoint tile)
}
// Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick.
static void runTicks(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state_bs,
BeltSystem& belts, int n, Tick& tick)
static void runTicks(ProductionSystem& production, const GameConfig& cfg,
FactoryState& state_bs, BeltSystem& belts, int n, Tick& tick)
{
for (int i = 0; i < n; ++i)
{
ConstructionSystem(cfg).tick(state_bs, belts, tick);
DeconstructionSystem(cfg).tick(state_bs, tick);
bs.tickBeltPull(state_bs);
bs.tickProduction(state_bs, tick);
bs.tickOutputBelts(state_bs);
production.tickBeltPull(state_bs, belts);
production.tickProduction(state_bs, tick);
production.tickOutputBelts(state_bs, belts);
belts.tick();
++tick;
}
@@ -99,7 +100,8 @@ struct PlacementFixture
FactoryState state = makeFactoryState(cfg);
BeltSystem belts;
std::mt19937 rng{0};
BuildingSystem bs;
BuildingSystem bs;
ProductionSystem production;
// Blocks credited back since the run began. The state is seeded with the configured
// starting stock (FactoryState.h), so a refund reads as a delta rather than a total.
@@ -116,7 +118,8 @@ struct PlacementFixture
std::optional<double> beltSpeed_tps = std::nullopt,
std::function<bool(const std::string&)> isItemUnlocked = nullptr)
: belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps))
, bs(cfg, belts,
, bs(cfg, belts)
, production(cfg,
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
isItemUnlocked ? std::move(isItemUnlocked)
: std::function<bool(const std::string&)>(
@@ -258,7 +261,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
// Complete construction (1 s).
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(f.belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
REQUIRE(getAllBuildings(f.state).size() == 1);
@@ -327,7 +330,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(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getAllSites(f.state).empty());
REQUIRE(findBuilding(f.state, id) != nullptr);
@@ -342,7 +345,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.cfg, f.state, f.belts, 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
}
REQUIRE(findBuilding(f.state, id) != nullptr);
}
@@ -366,7 +369,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.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
runTicks(f.production, 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.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100);
@@ -390,14 +393,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.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
runTicks(f.production, 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.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100);
// The second drains next.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
REQUIRE(findBuilding(f.state, b) == nullptr);
REQUIRE(f.getRefundedBlocks() == 2 * (15 * f.cfg.world.refundPercentage / 100));
}
@@ -423,7 +426,7 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n
REQUIRE(f.getRefundedBlocks() == 0);
// 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.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
REQUIRE(findBuilding(f.state, id) != nullptr);
REQUIRE(f.getRefundedBlocks() == 0);
}
@@ -482,7 +485,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(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getAllSites(f.state).size() == 1);
REQUIRE(getAllSites(f.state).front().id == id2);
@@ -503,7 +506,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(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
tick);
@@ -529,7 +532,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
// (completesAt=360). Cycle 2 completes at tick 360: deposit item -> 2 items held,
// which fills the buffer (capacity 2), so cycle 3 cannot start.
// Need to process through tick 360: 361 ticks total.
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(10.0))
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 1,
tick);
@@ -558,7 +561,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple
Tick tick = 0;
// Construction completes at tick 300 and cycle 1 starts in that same tick.
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(10.0)) + 1, tick);
const Building* b = findBuilding(f.state, id);
REQUIRE(b != nullptr);
@@ -567,7 +570,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple
// Process up to and including that completion tick: the next cycle is already
// running, due exactly one duration later rather than one duration plus a tick.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
b = findBuilding(f.state, id);
REQUIRE(b->getOutputItemCount() == 1);
REQUIRE(b->production.has_value());
@@ -579,7 +582,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple
building.outputBuffer.items.clear();
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
});
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
b = findBuilding(f.state, id);
REQUIRE(b->production.has_value());
REQUIRE(b->production->completesAt == firstCompletesAt + 2 * cycleTicks);
@@ -603,10 +606,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(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getProductionBuildingCount(f.state) == 1);
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)), tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)), tick);
REQUIRE(getProductionBuildingCount(f.state) == 2);
// Neither is producing yet: the miner has no recipe selected, and the
@@ -614,7 +617,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
REQUIRE(getActiveProductionBuildingCount(f.state) == 0);
f.bs.setRecipe(f.state, minerId, "mine_iron_ore");
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
REQUIRE(getActiveProductionBuildingCount(f.state) == 1);
}
@@ -631,12 +634,12 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
REQUIRE(getActiveProductionBuildingCount(f.state) == 0);
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
REQUIRE(getActiveProductionBuildingCount(f.state) == 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(f.bs, f.cfg, f.state, f.belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
const Building* b = findBuilding(f.state, id);
REQUIRE(b != nullptr);
@@ -663,7 +666,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
// Complete construction (15s → tick 450+1 = 451 ticks).
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
REQUIRE(findBuilding(f.state, sid)->recipeId.empty());
// Place west-flowing belt at (2,0): belt flows West, delivers to smelter.
@@ -671,7 +674,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
const Building* b = findBuilding(f.state, sid);
REQUIRE(b != nullptr);
@@ -697,12 +700,12 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state); // accepts the item onto the input belt at progress 0.0
f.production.tickBeltPull(f.state, f.belts); // accepts the item onto the input belt at progress 0.0
const Building* b = findBuilding(f.state, sid);
REQUIRE(b != nullptr);
@@ -714,7 +717,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
// One more pull tick advances the input belt to the centre; the item arrives.
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
REQUIRE(b->inputBuffer.counts.at(ItemType{"iron_ore"}) == 1);
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
}
@@ -729,7 +732,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.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.
@@ -738,7 +741,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
{
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
}
const Building* b = findBuilding(f.state, id);
@@ -761,7 +764,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.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).
@@ -770,11 +773,11 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
{
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
}
// iron_ingot recipe cycle is 2s; run to completion.
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
const Building* b = findBuilding(f.state, sid);
REQUIRE(b != nullptr);
@@ -797,7 +800,7 @@ TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
// Feed 1 iron_ore, then 2 copper_ore, via the west-flowing input belt.
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
@@ -806,10 +809,10 @@ TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first
{
f.belts.tryPutItem(QPoint(2, 0), makeItem(id));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
}
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
const Building* b = findBuilding(f.state, sid);
REQUIRE(b != nullptr);
@@ -847,13 +850,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(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.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(f.bs, f.cfg, f.state, f.belts, 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
const std::optional<Item> item = f.belts.tryTakeItem(eastPort(QPoint(1, 1)));
REQUIRE(item.has_value());
@@ -878,7 +881,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(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(30.0)), tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(30.0)), tick);
const Building* smelter = findBuilding(f.state, smelterId);
REQUIRE(smelter != nullptr);
@@ -907,7 +910,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(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)), tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)), tick);
const Building* miner = findBuilding(f.state, minerId);
const Building* sink = findBuilding(f.state, sinkId);
@@ -933,7 +936,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
Tick tick = 0;
// Run until first item is in output buffer.
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
tick);
@@ -998,8 +1001,8 @@ TEST_CASE("BuildingSystem: a single-group recipe consumes no randomness", "[buil
advanced.bs.setRecipe(advanced.state, b, "mine_iron_ore");
const int ticks = static_cast<int>(secondsToTicks(10.0)) + 40;
runTicks(quiet.bs, quiet.cfg, quiet.state, quiet.belts, ticks, tickA);
runTicks(advanced.bs, advanced.cfg, advanced.state, advanced.belts, ticks, tickB);
runTicks(quiet.production, quiet.cfg, quiet.state, quiet.belts, ticks, tickA);
runTicks(advanced.production, advanced.cfg, advanced.state, advanced.belts, ticks, tickB);
const Building* minerA = findBuilding(quiet.state, a);
const Building* minerB = findBuilding(advanced.state, b);
@@ -1056,7 +1059,7 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi
Tick tick = 0;
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0).value();
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
f.bs.setRecipe(f.state, id, "reprocessing_cycle");
@@ -1072,7 +1075,7 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi
building.outputBuffer.items.clear();
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
});
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(3.0)) + 1, tick);
for (const Item& item : outputSideItems(*findBuilding(f.state, id)))
@@ -1094,7 +1097,7 @@ TEST_CASE("BuildingSystem: reprocessing plant sizes one output buffer per possib
// Complete construction (25s).
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
// A plant holds no buffers until it has a recipe (REQ-BLD-AUTO-RECIPE); selecting
// one sizes them, exactly as the first scrap offered to it would.
@@ -1124,7 +1127,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
// Feed a full cycle's scrap (5) so only the output side can hold it back.
@@ -1133,7 +1136,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the
{
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
}
// Fill the iron_ingot buffer to its cap and leave the other two empty.
@@ -1146,7 +1149,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the
}
});
runTicks(f.bs, f.cfg, f.state, f.belts, 5, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 5, tick);
const Building* b = findBuilding(f.state, id);
REQUIRE(b != nullptr);
@@ -1171,7 +1174,7 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
// Two cycles' worth of scrap (5 each), which is exactly the input cap.
@@ -1180,13 +1183,13 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding
{
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
}
REQUIRE(findBuilding(f.state, id)->pendingInputCount(ItemType{"scrap"}) == 10);
// No belt carries the output away, so the first cycle's result is still held.
// reprocessing_cycle runs 3s; run through the completion tick.
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(3.0)) + 1, tick);
const Building* b = findBuilding(f.state, id);
@@ -1207,7 +1210,7 @@ static BuildingId buildSmelter(PlacementFixture& f, QPoint anchor, Tick& tick)
{
const BuildingId id =
f.bs.place(f.state, BuildingType::Smelter, anchor, Rotation::East, 0).value();
runTicks(f.bs, f.cfg, f.state, f.belts,
runTicks(f.production, f.cfg, f.state, f.belts,
static_cast<int>(secondsToTicks(15.0)) + 1, tick);
return id;
}
@@ -1241,13 +1244,13 @@ TEST_CASE("BuildingSystem: a set recipe is never replaced by a later material",
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
// Copper ore next: refused, and the recipe stands.
f.belts.tryPutItem(QPoint(2, 0), makeItem("copper_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
const Building* b = findBuilding(f.state, id);
REQUIRE(b->recipeId == "iron_ingot");
@@ -1270,7 +1273,7 @@ TEST_CASE("BuildingSystem: a manually selected recipe is not overridden", "[buil
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
const Building* b = findBuilding(f.state, id);
REQUIRE(b->recipeId == "copper_ingot");
@@ -1291,8 +1294,8 @@ TEST_CASE("BuildingSystem: selecting a different recipe frees a stuck auto-recip
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
runTicks(f.bs, f.cfg, f.state, f.belts, 30, tick);
f.production.tickBeltPull(f.state, f.belts);
runTicks(f.production, f.cfg, f.state, f.belts, 30, tick);
const Building* stuck = findBuilding(f.state, id);
REQUIRE(stuck->recipeId == "iron_ingot");
@@ -1322,7 +1325,7 @@ TEST_CASE("BuildingSystem: selecting (Auto) returns the building to automatic se
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
}
@@ -1340,7 +1343,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
// Complete construction (25s).
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.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).
@@ -1350,7 +1353,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
{
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
f.belts.tick();
f.bs.tickBeltPull(f.state);
f.production.tickBeltPull(f.state, f.belts);
}
// Verify all five scrap were accepted; some may still be travelling inward on
@@ -1362,7 +1365,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
}
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(3.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(3.0)) + 1, tick);
const Building* b = findBuilding(f.state, id);
REQUIRE(b != nullptr);
@@ -1406,7 +1409,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(getAllSites(f.state).empty());
const std::optional<BuildingId> result =
@@ -1806,7 +1809,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(findBuilding(f.state, id) != nullptr);
const Building& before = *findBuilding(f.state, id);
@@ -1827,7 +1830,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
f.bs.rotateInPlace(f.state, id, Rotation::North);
@@ -1847,7 +1850,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.cfg, f.state, f.belts, 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
}
REQUIRE(getAllBuildings(f.state).size() == 1);
@@ -1892,7 +1895,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.cfg, f.state, f.belts, 1, tick);
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
}
REQUIRE(getAllBuildings(f.state).size() == 1);
REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter);
@@ -2199,12 +2202,13 @@ namespace
// Advances the sim until the given site becomes an operational building, or a
// safety cap is reached.
void buildToCompletion(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state,
BeltSystem& belts, BuildingId id, Tick& tick)
void buildToCompletion(ProductionSystem& production, const GameConfig& cfg,
FactoryState& state, BeltSystem& belts, BuildingId id,
Tick& tick)
{
for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i)
{
runTicks(bs, cfg, state, belts, 1, tick);
runTicks(production, cfg, state, belts, 1, tick);
}
}
}
@@ -2239,7 +2243,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.cfg, f.state, f.belts, id, tick);
buildToCompletion(f.production, 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);