make BuildingSystem stateless: FactoryState becomes a parameter

This commit is contained in:
2026-08-05 06:50:11 +02:00
parent d87d063b10
commit 114a43b205
15 changed files with 401 additions and 395 deletions

View File

@@ -26,7 +26,6 @@ bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
} // namespace
BuildingSystem::BuildingSystem(const GameConfig& config,
FactoryState& state,
BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId,
std::function<void(int)> addBuildingBlocks,
@@ -35,7 +34,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
std::function<bool(const std::string&)> isItemUnlocked,
std::mt19937& rng)
: m_config(config)
, m_state(state)
, m_belts(belts)
, m_allocateBuildingId(std::move(allocateBuildingId))
, m_addBuildingBlocks(std::move(addBuildingBlocks))
@@ -43,7 +41,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
, m_isItemUnlocked(std::move(isItemUnlocked))
, m_rng(rng)
{
m_state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles;
}
// ---------------------------------------------------------------------------
@@ -213,7 +210,7 @@ std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe
// Placement
// ---------------------------------------------------------------------------
std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor,
std::optional<BuildingId> BuildingSystem::place(FactoryState& state, BuildingType type, QPoint anchor,
Rotation rotation, Tick currentTick)
{
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
@@ -221,7 +218,7 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
// Reject placements that fall outside the world (REQ-BLD-PLACE-VALID).
if (!bodyCellsWithinWorldBounds(m_state, m_config, mask.bodyCells, anchor))
if (!bodyCellsWithinWorldBounds(state, m_config, mask.bodyCells, anchor))
{
return std::nullopt;
}
@@ -232,7 +229,7 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
for (const QPoint& cell : mask.bodyCells)
{
const QPoint absCell = anchor + cell;
m_state.grid.occupy(absCell, id);
state.grid.occupy(absCell, id);
}
// Build construction site.
@@ -247,13 +244,13 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
site.bodyCells.push_back(anchor + cell);
}
if (m_state.constructionQueue.empty())
if (state.constructionQueue.empty())
{
site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
}
// else: completesAt remains 0 (queued, not yet started).
m_state.constructionQueue.push_back(std::move(site));
state.constructionQueue.push_back(std::move(site));
return id;
}
@@ -261,19 +258,19 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
// Deconstruct
// ---------------------------------------------------------------------------
int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick currentTick)
{
// Construction site? Removed instantly with the full refund; never queued
// for deconstruction (REQ-BLD-DECONSTRUCT).
for (std::deque<ConstructionSite>::iterator it = m_state.constructionQueue.begin();
it != m_state.constructionQueue.end();
for (std::deque<ConstructionSite>::iterator it = state.constructionQueue.begin();
it != state.constructionQueue.end();
++it)
{
if (it->id == id)
{
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
m_state.grid.release(it->bodyCells);
m_state.constructionQueue.erase(it);
state.grid.release(it->bodyCells);
state.constructionQueue.erase(it);
if (def)
{
return def->cost;
@@ -285,7 +282,7 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
// Operational building? Append it to the deconstruction queue rather than
// removing it now; the partial refund is credited on completion in
// tickDeconstruction (REQ-BLD-DECON-QUEUE).
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
if (building.id != id) { continue; }
if (building.queuedForDeconstruction) { return 0; } // already queued
@@ -313,11 +310,11 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
m_belts.removeTile(building.anchor);
}
const bool wasEmpty = m_state.deconstructionQueue.empty();
m_state.deconstructionQueue.push_back(std::move(entry));
const bool wasEmpty = state.deconstructionQueue.empty();
state.deconstructionQueue.push_back(std::move(entry));
if (wasEmpty)
{
startFrontDeconstruction(currentTick);
startFrontDeconstruction(state, currentTick);
}
return 0;
}
@@ -325,10 +322,10 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
return 0;
}
void BuildingSystem::startFrontDeconstruction(Tick currentTick)
void BuildingSystem::startFrontDeconstruction(FactoryState& state, Tick currentTick)
{
if (m_state.deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = m_state.deconstructionQueue.front();
if (state.deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = state.deconstructionQueue.front();
if (front.completesAt == 0)
{
front.completesAt =
@@ -340,10 +337,10 @@ void BuildingSystem::startFrontDeconstruction(Tick currentTick)
// Set recipe
// ---------------------------------------------------------------------------
void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId)
{
// Construction site: store recipe for when building completes.
for (ConstructionSite& site : m_state.constructionQueue)
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
@@ -366,7 +363,7 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
}
// Operational building: clear buffers and re-init.
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
if (building.id == id)
{
@@ -416,9 +413,9 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
}
}
void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout)
void BuildingSystem::setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout)
{
for (ConstructionSite& site : m_state.constructionQueue)
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
@@ -427,7 +424,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
}
}
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
if (building.id == id)
{
@@ -451,11 +448,11 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
}
}
void BuildingSystem::setSiteSplitterFilters(BuildingId id,
void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB)
{
for (ConstructionSite& site : m_state.constructionQueue)
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id && site.type == BuildingType::Splitter)
{
@@ -470,15 +467,15 @@ void BuildingSystem::setSiteSplitterFilters(BuildingId id,
// Tick hooks
// ---------------------------------------------------------------------------
void BuildingSystem::tickConstruction(Tick currentTick)
void BuildingSystem::tickConstruction(FactoryState& state, Tick currentTick)
{
TRACE();
if (m_state.constructionQueue.empty())
if (state.constructionQueue.empty())
{
return;
}
ConstructionSite& front = m_state.constructionQueue.front();
ConstructionSite& front = state.constructionQueue.front();
// Guard: if somehow the front site was never started, start it now.
if (front.completesAt == 0)
@@ -556,18 +553,18 @@ void BuildingSystem::tickConstruction(Tick currentTick)
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
reregisterBeltTile(building, front.splitterFilterA, front.splitterFilterB);
m_state.buildings.push_back(std::move(building));
state.buildings.push_back(std::move(building));
m_state.constructionQueue.pop_front();
state.constructionQueue.pop_front();
// Start next queued site if present.
if (!m_state.constructionQueue.empty() && m_state.constructionQueue.front().completesAt == 0)
if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0)
{
const BuildingDef* nextDef =
m_config.buildings.findBuildingDef(m_state.constructionQueue.front().type);
m_config.buildings.findBuildingDef(state.constructionQueue.front().type);
if (nextDef)
{
m_state.constructionQueue.front().completesAt =
state.constructionQueue.front().completesAt =
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
}
}
@@ -601,20 +598,20 @@ void BuildingSystem::reregisterBeltTile(const Building& building,
}
}
void BuildingSystem::tickDeconstruction(Tick currentTick)
void BuildingSystem::tickDeconstruction(FactoryState& state, Tick currentTick)
{
TRACE();
if (m_state.deconstructionQueue.empty())
if (state.deconstructionQueue.empty())
{
return;
}
DeconstructionEntry& front = m_state.deconstructionQueue.front();
DeconstructionEntry& front = state.deconstructionQueue.front();
// Guard: if the front entry's timer was never started, start it now.
if (front.completesAt == 0)
{
startFrontDeconstruction(currentTick);
startFrontDeconstruction(state, currentTick);
return;
}
@@ -626,15 +623,15 @@ void BuildingSystem::tickDeconstruction(Tick currentTick)
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
// Belt/tunnel/splitter tiles were already unregistered when the building was
// queued (see deconstruct), so only tile occupancy and the record remain.
for (std::vector<Building>::iterator it = m_state.buildings.begin();
it != m_state.buildings.end();
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);
m_state.grid.release(it->bodyCells);
m_state.buildings.erase(it);
state.grid.release(it->bodyCells);
state.buildings.erase(it);
if (def)
{
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
@@ -642,16 +639,16 @@ void BuildingSystem::tickDeconstruction(Tick currentTick)
break;
}
m_state.deconstructionQueue.pop_front();
state.deconstructionQueue.pop_front();
// Start the next queued deconstruction, if any.
startFrontDeconstruction(currentTick);
startFrontDeconstruction(state, currentTick);
}
void BuildingSystem::cancelDeconstruction(BuildingId id)
void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id)
{
for (std::deque<DeconstructionEntry>::iterator it = m_state.deconstructionQueue.begin();
it != m_state.deconstructionQueue.end();
for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin();
it != state.deconstructionQueue.end();
++it)
{
if (it->id != id) { continue; }
@@ -659,27 +656,27 @@ void BuildingSystem::cancelDeconstruction(BuildingId id)
// Resume operation: clear the flag and re-register belt/tunnel/splitter
// tiles that were unregistered at enqueue (which re-pairs tunnels,
// REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund.
if (Building* building = findBuilding(m_state, id))
if (Building* building = findBuilding(state, id))
{
building->queuedForDeconstruction = false;
reregisterBeltTile(*building, it->splitterFilterA, it->splitterFilterB);
}
m_state.deconstructionQueue.erase(it);
state.deconstructionQueue.erase(it);
// If the running front was removed, the new front (completesAt == 0) has
// its timer started by the next tickDeconstruction guard.
return;
}
}
void BuildingSystem::tickBeltPull()
void BuildingSystem::tickBeltPull(FactoryState& state)
{
TRACE();
// Same per-tick step as the belts, so items travel inward at belt speed
// (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
const double progressPerTick = m_belts.getProgressPerTick_tpt();
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
@@ -759,17 +756,17 @@ void BuildingSystem::depositToInputBelt(Building& consumer,
consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
}
bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
bool BuildingSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
const Port& outputPort,
const Item& item)
{
const std::optional<BuildingId> ownerId = m_state.grid.findOwner(outputPort.tile);
const std::optional<BuildingId> ownerId = state.grid.findOwner(outputPort.tile);
if (!ownerId.has_value() || *ownerId == producerId)
{
return false;
}
Building* consumer = findBuilding(m_state, *ownerId);
Building* consumer = findBuilding(state, *ownerId);
if (!consumer)
{
return false; // an unbuilt construction site, or not an operational building
@@ -794,10 +791,10 @@ bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
return false;
}
void BuildingSystem::tickProduction(Tick currentTick)
void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
{
TRACE();
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
@@ -897,10 +894,10 @@ void BuildingSystem::tickProduction(Tick currentTick)
}
}
void BuildingSystem::tickShipyardProduction(Tick currentTick)
void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTick)
{
TRACE();
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
@@ -993,14 +990,14 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
}
}
void BuildingSystem::tickOutputBelts()
void BuildingSystem::tickOutputBelts(FactoryState& state)
{
TRACE();
// Use BeltSystem's own per-tick step so emerging items travel at exactly the
// same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
const double progressPerTick = m_belts.getProgressPerTick_tpt();
for (Building& building : m_state.buildings)
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
@@ -1023,7 +1020,7 @@ void BuildingSystem::tickOutputBelts()
{
const Item item = lane.front().item;
if (m_belts.tryPutItem(port.tile, item, port.direction)
|| tryDirectCoupleDeposit(building.id, port, item))
|| tryDirectCoupleDeposit(state, building.id, port, item))
{
lane.erase(lane.begin());
}
@@ -1043,10 +1040,10 @@ void BuildingSystem::tickOutputBelts()
}
}
void BuildingSystem::forEachEmergingItem(
void BuildingSystem::forEachEmergingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const
{
for (const Building& building : m_state.buildings)
for (const Building& building : state.buildings)
{
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
{
@@ -1065,10 +1062,10 @@ void BuildingSystem::forEachEmergingItem(
}
}
void BuildingSystem::forEachIncomingItem(
void BuildingSystem::forEachIncomingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const
{
for (const Building& building : m_state.buildings)
for (const Building& building : state.buildings)
{
for (std::size_t p = 0; p < building.inputPorts.size(); ++p)
{
@@ -1096,10 +1093,10 @@ void BuildingSystem::forEachIncomingItem(
void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
void BuildingSystem::rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation)
{
// Construction site path — just update rotation; no ports to recompute.
for (ConstructionSite& site : m_state.constructionQueue)
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
@@ -1109,7 +1106,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
}
// Operational building path.
for (Building& b : m_state.buildings)
for (Building& b : state.buildings)
{
if (b.id != id) { continue; }
@@ -1161,7 +1158,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
}
}
BuildingId BuildingSystem::placeImmediate(BuildingType type,
BuildingId BuildingSystem::placeImmediate(FactoryState& state, BuildingType type,
const std::vector<std::string>& surfaceMask,
QPoint anchor, Rotation rotation)
{
@@ -1179,7 +1176,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
{
const QPoint absCell = anchor + cell;
building.bodyCells.push_back(absCell);
m_state.grid.occupy(absCell, id);
state.grid.occupy(absCell, id);
}
for (const Port& port : mask.outputPorts)
{
@@ -1197,14 +1194,14 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
initSalvageBayBuffer(building);
}
m_state.buildings.push_back(std::move(building));
state.buildings.push_back(std::move(building));
return id;
}
bool BuildingSystem::removeBuilding(BuildingId id)
bool BuildingSystem::removeBuilding(FactoryState& state, BuildingId id)
{
for (std::vector<Building>::iterator it = m_state.buildings.begin();
it != m_state.buildings.end();
for (std::vector<Building>::iterator it = state.buildings.begin();
it != state.buildings.end();
++it)
{
if (it->id == id)
@@ -1214,31 +1211,31 @@ bool BuildingSystem::removeBuilding(BuildingId id)
{
m_belts.removeTile(it->anchor);
}
m_state.grid.release(it->bodyCells);
m_state.buildings.erase(it);
state.grid.release(it->bodyCells);
state.buildings.erase(it);
return true;
}
}
return false;
}
void BuildingSystem::forEachBuilding(std::function<void(Building&)> fn)
void BuildingSystem::forEachBuilding(FactoryState& state, std::function<void(Building&)> fn)
{
for (Building& b : m_state.buildings)
for (Building& b : state.buildings)
{
fn(b);
}
}
void BuildingSystem::registerTileOccupancy(const std::vector<QPoint>& cells,
void BuildingSystem::registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells,
BuildingId ownerPlaceholder)
{
m_state.grid.occupy(cells, ownerPlaceholder);
state.grid.occupy(cells, ownerPlaceholder);
}
void BuildingSystem::unregisterTileOccupancy(const std::vector<QPoint>& cells)
void BuildingSystem::unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells)
{
m_state.grid.release(cells);
state.grid.release(cells);
}
namespace
@@ -1270,12 +1267,12 @@ void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer)
}
} // namespace
void BuildingSystem::appendChecksum(Hasher& hasher) const
void BuildingSystem::appendChecksum(const FactoryState& state, Hasher& hasher) const
{
// m_state.buildings keeps a stable, deterministic order (append on build, swap-free
// state.buildings keeps a stable, deterministic order (append on build, swap-free
// erase aside — both runs perform identical operations, so order matches).
hasher.append(m_state.buildings.size());
for (const Building& b : m_state.buildings)
hasher.append(state.buildings.size());
for (const Building& b : state.buildings)
{
hasher.append(b.id);
hasher.append(b.anchor);
@@ -1318,8 +1315,8 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
hasher.append(b.queuedForDeconstruction);
}
hasher.append(m_state.constructionQueue.size());
for (const ConstructionSite& s : m_state.constructionQueue)
hasher.append(state.constructionQueue.size());
for (const ConstructionSite& s : state.constructionQueue)
{
hasher.append(s.id);
hasher.append(s.anchor);
@@ -1336,8 +1333,8 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
}
hasher.append(m_state.deconstructionQueue.size());
for (const DeconstructionEntry& e : m_state.deconstructionQueue)
hasher.append(state.deconstructionQueue.size());
for (const DeconstructionEntry& e : state.deconstructionQueue)
{
hasher.append(e.id);
hasher.append(e.completesAt);
@@ -1347,5 +1344,5 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); }
}
m_state.grid.appendChecksum(hasher);
state.grid.appendChecksum(hasher);
}