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

@@ -46,9 +46,10 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
, m_finished(false) , m_finished(false)
, m_stopRequested(false) , m_stopRequested(false)
{ {
m_factoryState = makeFactoryState(m_gameConfig);
m_buildingSystem = std::make_unique<BuildingSystem>( m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig, m_gameConfig,
m_factoryState,
m_beltSystem, m_beltSystem,
[this]() { return allocateBuildingId(); }, [this]() { return allocateBuildingId(); },
[](int) {}, [](int) {},
@@ -163,7 +164,7 @@ void ArenaSimulation::placeStructures()
hp, hp, false); hp, hp, false);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR). // Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity); m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
} }
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon. // Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
@@ -184,7 +185,7 @@ void ArenaSimulation::placeStructures()
hp, hp, true); hp, hp, true);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR). // Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity); m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
} }
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy) auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
@@ -238,7 +239,7 @@ void ArenaSimulation::placeStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{stationEntity}); ModuleOwnerComponent{stationEntity});
} }
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
}; };
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations) for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
@@ -393,7 +394,7 @@ void ArenaSimulation::tickDeaths()
for (entt::entity deadEntity : deadStations) for (entt::entity deadEntity : deadStations)
{ {
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity); const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
{ {
std::vector<entt::entity> stationChildren; std::vector<entt::entity> stationChildren;
m_admin.forEach<ModuleOwnerComponent>( m_admin.forEach<ModuleOwnerComponent>(

View File

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

View File

@@ -37,7 +37,6 @@ class BuildingSystem
{ {
public: public:
BuildingSystem(const GameConfig& config, BuildingSystem(const GameConfig& config,
FactoryState& state,
BeltSystem& belts, BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId, std::function<BuildingId()> allocateBuildingId,
std::function<void(int)> addBuildingBlocks, std::function<void(int)> addBuildingBlocks,
@@ -53,7 +52,7 @@ public:
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage // queue. Terrain type (A vs S) is NOT checked here so that tests can stage
// arbitrary layouts; the player-facing entry point // arbitrary layouts; the player-facing entry point
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid. // (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation, std::optional<BuildingId> place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation,
Tick currentTick); Tick currentTick);
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and // Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
@@ -65,7 +64,8 @@ public:
// Sets the current buildable asteroid width in tiles. Grows the left // Sets the current buildable asteroid width in tiles. Grows the left
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK). // placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
// Defaults to world.regions.asteroid_width_tiles at construction. // Defaults to world.regions.asteroid_width_tiles at construction.
void setAsteroidWidth_tiles(int widthTiles) { m_state.asteroidWidth_tiles = widthTiles; } void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const
{ state.asteroidWidth_tiles = widthTiles; }
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT). // Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
// A construction site is removed instantly and the full cost is returned. // A construction site is removed instantly and the full cost is returned.
@@ -73,23 +73,23 @@ public:
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is // (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
// credited later, on completion in tickDeconstruction, so this returns 0 for // credited later, on completion in tickDeconstruction, so this returns 0 for
// it. Returns 0 for unknown ids and for a building already queued. // it. Returns 0 for unknown ids and for a building already queued.
int deconstruct(BuildingId id, Tick currentTick); int deconstruct(FactoryState& state, BuildingId id, Tick currentTick);
// Take a building back out of the deconstruction queue before it is removed // Take a building back out of the deconstruction queue before it is removed
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation // (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
// (re-registering belt/tunnel/splitter tiles); discards deconstruction // (re-registering belt/tunnel/splitter tiles); discards deconstruction
// progress and credits no refund. No-op if the id is not queued. // progress and credits no refund. No-op if the id is not queued.
void cancelDeconstruction(BuildingId id); void cancelDeconstruction(FactoryState& state, BuildingId id);
// True if the building is currently in the deconstruction queue. // True if the building is currently in the deconstruction queue.
// Set the recipe (or schematic id for shipyard) on a building or queued // Set the recipe (or schematic id for shipyard) on a building or queued
// construction site. Clears both buffers on an operational building. // construction site. Clears both buffers on an operational building.
void setRecipe(BuildingId id, const std::string& recipeId); void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId);
// Set the module layout for a shipyard. Cancels in-progress production // Set the module layout for a shipyard. Cancels in-progress production
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD). // (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
void setShipLayout(BuildingId id, const ShipLayoutConfig& layout); void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout);
// Splitter filter configuration for a queued/under-construction Splitter // Splitter filter configuration for a queued/under-construction Splitter
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through // site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
@@ -98,23 +98,23 @@ public:
// output directions (derived from its surface mask) and stored filters, or // output directions (derived from its surface mask) and stored filters, or
// nullopt if the id is not a Splitter site. The stored filters are applied // nullopt if the id is not a Splitter site. The stored filters are applied
// to BeltSystem when the splitter finishes building (tickConstruction). // to BeltSystem when the splitter finishes building (tickConstruction).
void setSiteSplitterFilters(BuildingId id, void setSiteSplitterFilters(FactoryState& state, BuildingId id,
const std::vector<ItemType>& filterA, const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB); const std::vector<ItemType>& filterB);
// -- Tick hooks (called from Simulation::tick in the documented order) --- // -- Tick hooks (called from Simulation::tick in the documented order) ---
void tickConstruction(Tick currentTick); void tickConstruction(FactoryState& state, Tick currentTick);
// 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(Tick currentTick); void tickDeconstruction(FactoryState& state, Tick currentTick);
void tickBeltPull(); void tickBeltPull(FactoryState& state);
void tickProduction(Tick currentTick); void tickProduction(FactoryState& state, Tick currentTick);
void tickShipyardProduction(Tick currentTick); void tickShipyardProduction(FactoryState& state, Tick currentTick);
// Advances each building's virtual output belts, hands finished items off onto // Advances each building's virtual output belts, hands finished items off onto
// the adjacent real belt, and feeds new buffered items into them // the adjacent real belt, and feeds new buffered items into them
// (REQ-MAT-OUTPUT-EMERGE). // (REQ-MAT-OUTPUT-EMERGE).
void tickOutputBelts(); void tickOutputBelts(FactoryState& state);
// -- Queries ------------------------------------------------------------- // -- Queries -------------------------------------------------------------
@@ -134,19 +134,19 @@ public:
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its // virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
// world-space centre (in tile units). Least-progressed first (drawn bottom) so // world-space centre (in tile units). Least-progressed first (drawn bottom) so
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering). // callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
void forEachEmergingItem( void forEachEmergingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const; const std::function<void(const ItemType&, QPointF)>& visit) const;
// Visits every item currently travelling inward on a building input port's // Visits every item currently travelling inward on a building input port's
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its // virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
// world-space centre (in tile units). Least-progressed first (drawn bottom). // world-space centre (in tile units). Least-progressed first (drawn bottom).
void forEachIncomingItem( void forEachIncomingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const; const std::function<void(const ItemType&, QPointF)>& visit) const;
// Rotate an existing building or construction site to newRotation in place. // Rotate an existing building or construction site to newRotation in place.
// For belt-type operational buildings, re-registers with BeltSystem (items // For belt-type operational buildings, re-registers with BeltSystem (items
// currently on the tile are discarded by BeltSystem::removeTile). // currently on the tile are discarded by BeltSystem::removeTile).
void rotateInPlace(BuildingId id, Rotation newRotation); void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation);
// Input-capable adjacent tiles for a building or construction site // Input-capable adjacent tiles for a building or construction site
@@ -155,8 +155,8 @@ public:
// the target. Output-port edges are excluded. Empty for an unknown id. // the target. Output-port edges are excluded. Empty for an unknown id.
// Register / unregister tile occupancy for ECS station entities. // Register / unregister tile occupancy for ECS station entities.
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder); void registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
void unregisterTileOccupancy(const std::vector<QPoint>& cells); void unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells);
// Place one "scrap" item into a SalvageBay's output buffer. // Place one "scrap" item into a SalvageBay's output buffer.
// Returns false if bay not found, wrong type, or output buffer is full. // Returns false if bay not found, wrong type, or output buffer is full.
@@ -164,26 +164,26 @@ public:
// Bypass the construction queue and create a fully-operational Building // Bypass the construction queue and create a fully-operational Building
// immediately. Used for pre-placed structures (HQ, defence stations). // immediately. Used for pre-placed structures (HQ, defence stations).
// surfaceMask comes from the relevant config struct. // surfaceMask comes from the relevant config struct.
BuildingId placeImmediate(BuildingType type, BuildingId placeImmediate(FactoryState& state, BuildingType type,
const std::vector<std::string>& surfaceMask, const std::vector<std::string>& surfaceMask,
QPoint anchor, Rotation rotation); QPoint anchor, Rotation rotation);
// Remove an operational building by id without refund (used for deaths). // Remove an operational building by id without refund (used for deaths).
// Returns true if found and removed. // Returns true if found and removed.
bool removeBuilding(BuildingId id); bool removeBuilding(FactoryState& state, BuildingId id);
// Mutable iteration over all operational buildings. // Mutable iteration over all operational buildings.
void forEachBuilding(std::function<void(Building&)> fn); void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
// -- Determinism --------------------------------------------------------- // -- Determinism ---------------------------------------------------------
// Folds all building, construction-site, and tile-occupancy state into the // Folds all building, construction-site, and tile-occupancy state into the
// hasher in deterministic order (see docs/replay_design.md). // hasher in deterministic order (see docs/replay_design.md).
void appendChecksum(Hasher& hasher) const; void appendChecksum(const FactoryState& state, Hasher& hasher) const;
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(Tick currentTick); 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
@@ -206,7 +206,7 @@ private:
// Attempts to hand an emerging output item straight into a directly adjacent // Attempts to hand an emerging output item straight into a directly adjacent
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE). // building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
// Returns true if the item was accepted onto the consumer's input belt. // Returns true if the item was accepted onto the consumer's input belt.
bool tryDirectCoupleDeposit(BuildingId producerId, bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
const Port& outputPort, const Port& outputPort,
const Item& item); const Item& item);
@@ -233,9 +233,6 @@ private:
const GameConfig& m_config; const GameConfig& m_config;
// The factory's world data — buildings, queued work, tile ownership. Owned by
// Simulation, not by this system (see FactoryState.h).
FactoryState& m_state;
BeltSystem& m_belts; BeltSystem& m_belts;
std::function<BuildingId()> m_allocateBuildingId; std::function<BuildingId()> m_allocateBuildingId;

View File

@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include "Building.h" #include "Building.h"
#include "GameConfig.h"
#include "BuildingGrid.h" #include "BuildingGrid.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "ItemType.h" #include "ItemType.h"
@@ -52,3 +53,14 @@ struct FactoryState
// Seeded from config by BuildingSystem's constructor. // Seeded from config by BuildingSystem's constructor.
int asteroidWidth_tiles = 0; int asteroidWidth_tiles = 0;
}; };
// A fresh factory for a new run: nothing built, and the asteroid bound seeded from
// config. Every owner of a FactoryState creates it this way — the bound has no
// sensible default without the config, so a default-constructed state would refuse
// every placement on the asteroid.
inline FactoryState makeFactoryState(const GameConfig& config)
{
FactoryState state;
state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles;
return state;
}

View File

@@ -50,6 +50,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
{ {
m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null; m_currentEnemyStationEntities[1] = entt::null;
m_factoryState = makeFactoryState(m_config);
initializeSubsystems(); initializeSubsystems();
@@ -97,7 +98,7 @@ void Simulation::reset(unsigned int seed)
m_pendingSchematicChoices.clear(); m_pendingSchematicChoices.clear();
m_admin.clear(); m_admin.clear();
m_factoryState = FactoryState{}; m_factoryState = makeFactoryState(m_config);
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps); m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
initializeSubsystems(); initializeSubsystems();
@@ -109,7 +110,6 @@ void Simulation::initializeSubsystems()
{ {
m_buildingSystem = std::make_unique<BuildingSystem>( m_buildingSystem = std::make_unique<BuildingSystem>(
m_config, m_config,
m_factoryState,
m_beltSystem, m_beltSystem,
[this]() { return allocateBuildingId(); }, [this]() { return allocateBuildingId(); },
[this](int amount) { m_buildingBlocksStock += amount; }, [this](int amount) { m_buildingBlocksStock += amount; },
@@ -153,15 +153,15 @@ void Simulation::apply(const Command& command)
const BuildingId id = *placed; const BuildingId id = *placed;
if (c.recipeId.has_value()) if (c.recipeId.has_value())
{ {
m_buildingSystem->setRecipe(id, *c.recipeId); m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId);
} }
if (c.shipLayout.has_value()) if (c.shipLayout.has_value())
{ {
m_buildingSystem->setShipLayout(id, *c.shipLayout); m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout);
} }
if (c.hasSplitterFilters) if (c.hasSplitterFilters)
{ {
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB); m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB);
} }
break; break;
} }
@@ -174,26 +174,26 @@ void Simulation::apply(const Command& command)
case CommandKind::RotateInPlace: case CommandKind::RotateInPlace:
{ {
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command); const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
m_buildingSystem->rotateInPlace(*c.id, c.newRotation); m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation);
break; break;
} }
case CommandKind::SetRecipe: case CommandKind::SetRecipe:
{ {
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command); const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
m_buildingSystem->setRecipe(*c.id, c.recipeId); m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId);
break; break;
} }
case CommandKind::SetShipLayout: case CommandKind::SetShipLayout:
{ {
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command); const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
m_buildingSystem->setShipLayout(*c.id, c.layout); m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout);
break; break;
} }
case CommandKind::SetSiteSplitterFilters: case CommandKind::SetSiteSplitterFilters:
{ {
const SetSiteSplitterFiltersCommand& c = const SetSiteSplitterFiltersCommand& c =
static_cast<const SetSiteSplitterFiltersCommand&>(command); static_cast<const SetSiteSplitterFiltersCommand&>(command);
m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB); m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB);
break; break;
} }
case CommandKind::SetSplitterFilters: case CommandKind::SetSplitterFilters:
@@ -243,12 +243,12 @@ void Simulation::tick()
m_waveSystem->tickThreatAccumulation(); m_waveSystem->tickThreatAccumulation();
// Construction + production pipeline // Construction + production pipeline
m_buildingSystem->tickConstruction(m_currentTick); m_buildingSystem->tickConstruction(m_factoryState, m_currentTick);
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction m_buildingSystem->tickDeconstruction(m_factoryState, m_currentTick); // parallel to construction
m_buildingSystem->tickBeltPull(); // step 3 m_buildingSystem->tickBeltPull(m_factoryState); // step 3
m_buildingSystem->tickProduction(m_currentTick); // step 4 m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
m_buildingSystem->tickOutputBelts(); // step 5 m_buildingSystem->tickOutputBelts(m_factoryState); // step 5
m_beltSystem.tick(); // step 6 m_beltSystem.tick(); // step 6
// Step 7: ship behavior systems (movement arbitration via intent priority) // Step 7: ship behavior systems (movement arbitration via intent priority)
@@ -306,8 +306,7 @@ void Simulation::placeInitialStructures()
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2; (m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
const float hqHp = const float hqHp =
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0)); static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
m_hqBuildingId = m_buildingSystem->placeImmediate( m_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq,
BuildingType::Hq,
m_config.stations.hq.surfaceMask, m_config.stations.hq.surfaceMask,
QPoint(hqAnchorX, hqAnchorY), QPoint(hqAnchorX, hqAnchorY),
Rotation::East); Rotation::East);
@@ -356,7 +355,7 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation1Entity}); ModuleOwnerComponent{m_playerStation1Entity});
} }
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
} }
{ {
const QPoint anchor(psAnchorX, ps2Y); const QPoint anchor(psAnchorX, ps2Y);
@@ -373,7 +372,7 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation2Entity}); ModuleOwnerComponent{m_playerStation2Entity});
} }
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
} }
// Rally point: center of the player defence stations' X column, world vertical midpoint. // Rally point: center of the player defence stations' X column, world vertical midpoint.
@@ -428,7 +427,7 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_currentEnemyStationEntities[0]}); ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
} }
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
} }
{ {
const QPoint anchor(anchorX, y2); const QPoint anchor(anchorX, y2);
@@ -445,7 +444,7 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_currentEnemyStationEntities[1]}); ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
} }
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
} }
} }
@@ -519,7 +518,7 @@ void Simulation::tickDeathsAndLoot()
{ {
m_debrisSystem->spawn(pos.value, scrap, despawnAt); m_debrisSystem->spawn(pos.value, scrap, despawnAt);
} }
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
{ {
std::vector<entt::entity> stationChildren; std::vector<entt::entity> stationChildren;
m_admin.forEach<ModuleOwnerComponent>( m_admin.forEach<ModuleOwnerComponent>(
@@ -671,7 +670,7 @@ unsigned long long Simulation::computeStateChecksum() const
m_unlockState.appendChecksum(hasher); m_unlockState.appendChecksum(hasher);
// Subsystems contribute their own state. // Subsystems contribute their own state.
m_buildingSystem->appendChecksum(hasher); m_buildingSystem->appendChecksum(m_factoryState, hasher);
m_beltSystem.appendChecksum(hasher); m_beltSystem.appendChecksum(hasher);
// ECS component state. View iteration order is a pure function of the // ECS component state. View iteration order is a pure function of the
@@ -784,7 +783,7 @@ void Simulation::tryExpandAsteroid()
} }
m_buildingBlocksStock -= cost; m_buildingBlocksStock -= cost;
++m_expansionsPurchased; ++m_expansionsPurchased;
m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles()); m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles());
} }
bool Simulation::isGameOver() const bool Simulation::isGameOver() const
@@ -880,17 +879,17 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
return std::nullopt; return std::nullopt;
} }
m_buildingBlocksStock -= cost; m_buildingBlocksStock -= cost;
return m_buildingSystem->place(type, anchor, rotation, m_currentTick); return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick);
} }
void Simulation::deconstruct(BuildingId id) void Simulation::deconstruct(BuildingId id)
{ {
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick); m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick);
} }
void Simulation::cancelDeconstruction(BuildingId id) void Simulation::cancelDeconstruction(BuildingId id)
{ {
m_buildingSystem->cancelDeconstruction(id); m_buildingSystem->cancelDeconstruction(m_factoryState, id);
} }
BuildingSystem& Simulation::getBuildingsMutable() BuildingSystem& Simulation::getBuildingsMutable()

View File

@@ -56,7 +56,7 @@
struct Fixture struct Fixture
{ {
GameConfig cfg; GameConfig cfg;
FactoryState state; FactoryState state = makeFactoryState(cfg);
BeltSystem belts; BeltSystem belts;
BuildingId nextBuildingId; BuildingId nextBuildingId;
int stock; int stock;
@@ -79,7 +79,7 @@ struct Fixture
, nextBuildingId(1) , nextBuildingId(1)
, stock(0) , stock(0)
, rng(42) , rng(42)
, buildings(cfg, state, belts, , buildings(cfg, belts,
[this]() { return nextBuildingId++; }, [this]() { return nextBuildingId++; },
[this](int n) { stock += n; }, [this](int n) { stock += n; },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
@@ -952,12 +952,12 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
{ {
Fixture f; Fixture f;
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0).value(); QPoint(-4, 0), Rotation::East, 0).value();
Tick t = 0; Tick t = 0;
for (int i = 0; i < 500; ++i) for (int i = 0; i < 500; ++i)
{ {
f.buildings.tickConstruction(t++); f.buildings.tickConstruction(f.state, t++);
if (findBuilding(f.state, bayId) != nullptr) if (findBuilding(f.state, bayId) != nullptr)
{ {
break; break;
@@ -987,12 +987,12 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
{ {
Fixture f; Fixture f;
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0).value(); QPoint(-4, 0), Rotation::East, 0).value();
Tick t = 0; Tick t = 0;
for (int i = 0; i < 500; ++i) for (int i = 0; i < 500; ++i)
{ {
f.buildings.tickConstruction(t++); f.buildings.tickConstruction(f.state, t++);
if (findBuilding(f.state, bayId) != nullptr) { break; } if (findBuilding(f.state, bayId) != nullptr) { break; }
} }
const Building* bay = findBuilding(f.state, bayId); const Building* bay = findBuilding(f.state, bayId);

View File

@@ -666,7 +666,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
const ConstructionSite* site = findSite(sim.getFactoryState(), id); const ConstructionSite* site = findSite(sim.getFactoryState(), id);
REQUIRE(site != nullptr); REQUIRE(site != nullptr);
@@ -680,7 +680,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore");
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300. // Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
// Run 301 ticks (0..300) to process the completion tick. // Run 301 ticks (0..300) to process the completion tick.
@@ -726,7 +726,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
const BuildingId id = const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
const Blueprint bp = captureBlueprintFromSelection(sim, { id }); const Blueprint bp = captureBlueprintFromSelection(sim, { id });
@@ -743,7 +743,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
const BuildingId idA = const BuildingId idA =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(idA != kInvalidBuildingId); REQUIRE(idA != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore");
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); } for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr); REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);
@@ -751,7 +751,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
const BuildingId idB = const BuildingId idB =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value(); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
REQUIRE(idB != kInvalidBuildingId); REQUIRE(idB != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore");
REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr); REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB }); const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
@@ -854,7 +854,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(id, layout); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
const ConstructionSite* site = findSite(sim.getFactoryState(), id); const ConstructionSite* site = findSite(sim.getFactoryState(), id);
REQUIRE(site != nullptr); REQUIRE(site != nullptr);
@@ -878,7 +878,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
pm.rotation = Rotation::North; pm.rotation = Rotation::North;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(id, layout); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
// Shipyard construction_time_seconds = 30 in the test config. // Shipyard construction_time_seconds = 30 in the test config.
double constructionTime = 0.0; double constructionTime = 0.0;

View File

@@ -37,8 +37,7 @@ BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
{ {
const BuildingDef* def = findDef(cfg, type); const BuildingDef* def = findDef(cfg, type);
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
return SimulationTestAccess::buildings(sim).placeImmediate( return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
type, def->surfaceMask, anchor, Rotation::East);
} }
const ShipDef* findAvailableSchematic(const GameConfig& cfg) const ShipDef* findAvailableSchematic(const GameConfig& cfg)
@@ -67,7 +66,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
Simulation sim(loadTestConfig(), 7); Simulation sim(loadTestConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0)); const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id); const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value()); REQUIRE(config.has_value());
@@ -103,8 +102,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
REQUIRE(schematic != nullptr); REQUIRE(schematic != nullptr);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0)); const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{}); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{});
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id); const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value()); REQUIRE(config.has_value());
@@ -126,7 +125,7 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr); REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr); REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id); const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value()); REQUIRE(config.has_value());

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@ static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
struct CombatFixture struct CombatFixture
{ {
GameConfig cfg; GameConfig cfg;
FactoryState state; FactoryState state = makeFactoryState(cfg);
std::mt19937 rng; std::mt19937 rng;
EntityAdmin admin; EntityAdmin admin;
BuildingId nextBuildingId; BuildingId nextBuildingId;
@@ -67,7 +67,7 @@ struct CombatFixture
, nextBuildingId(1) , nextBuildingId(1)
, belts(cfg.world.beltSpeed_tps) , belts(cfg.world.beltSpeed_tps)
, ships(cfg, admin) , ships(cfg, admin)
, buildings(cfg, state, belts, , buildings(cfg, belts,
[this]() { return nextBuildingId++; }, [this]() { return nextBuildingId++; },
[](int){}, [](int){},
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},

View File

@@ -47,7 +47,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
const BuildingId id = const BuildingId id =
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore");
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
} }

View File

@@ -62,8 +62,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef) static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
{ {
return SimulationTestAccess::buildings(sim).placeImmediate( return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
BuildingType::Shipyard,
yardDef.surfaceMask, yardDef.surfaceMask,
QPoint(0, 0), QPoint(0, 0),
Rotation::East); Rotation::East);
@@ -73,7 +72,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
const ShipDef& def, const ShipDef& def,
const ShipLayoutConfig& layout) const ShipLayoutConfig& layout)
{ {
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) { SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b) {
if (b.id != yardId) if (b.id != yardId)
{ {
return; return;
@@ -208,7 +207,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
ShipLayoutConfig layout; ShipLayoutConfig layout;
PlacedModule pm; PlacedModule pm;
@@ -217,7 +216,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
const Building* b = findBuilding(sim.getFactoryState(), yardId); const Building* b = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b != nullptr); REQUIRE(b != nullptr);
@@ -237,7 +236,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
// Fill materials and tick to start production. // Fill materials and tick to start production.
ShipLayoutConfig emptyLayout; ShipLayoutConfig emptyLayout;
@@ -256,7 +255,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
const Building* b2 = findBuilding(sim.getFactoryState(), yardId); const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b2 != nullptr); REQUIRE(b2 != nullptr);
@@ -279,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, "interceptor");
// Deliberately no setShipLayout: recipe set, layout left unconfigured. // Deliberately no setShipLayout: recipe set, layout left unconfigured.
// Charge only the base-hull materials (an empty layout adds none). // Charge only the base-hull materials (an empty layout adds none).
@@ -314,7 +313,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
ShipLayoutConfig layout; ShipLayoutConfig layout;
PlacedModule pm; PlacedModule pm;
@@ -322,13 +321,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
pm.position = QPoint(0, 0); pm.position = QPoint(0, 0);
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId); const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b1 != nullptr); REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value()); REQUIRE(b1->shipLayout.has_value());
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"destroyer");
const Building* b2 = findBuilding(sim.getFactoryState(), yardId); const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b2 != nullptr); REQUIRE(b2 != nullptr);
@@ -343,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
ShipLayoutConfig layout; ShipLayoutConfig layout;
PlacedModule pm; PlacedModule pm;
@@ -351,14 +350,14 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
pm.position = QPoint(0, 0); pm.position = QPoint(0, 0);
pm.rotation = Rotation::East; pm.rotation = Rotation::East;
layout.placedModules.push_back(pm); layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout); SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId); const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b1 != nullptr); REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value()); REQUIRE(b1->shipLayout.has_value());
// Re-selecting the same recipe must be a no-op and preserve the layout. // Re-selecting the same recipe must be a no-op and preserve the layout.
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor"); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
const Building* b2 = findBuilding(sim.getFactoryState(), yardId); const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
REQUIRE(b2 != nullptr); REQUIRE(b2 != nullptr);

View File

@@ -58,8 +58,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef) static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
{ {
return SimulationTestAccess::buildings(sim).placeImmediate( return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
BuildingType::Shipyard,
yardDef.surfaceMask, yardDef.surfaceMask,
QPoint(0, 0), QPoint(0, 0),
Rotation::East); Rotation::East);
@@ -75,7 +74,7 @@ static int countShips(Simulation& sim)
static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def) static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def)
{ {
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b)
{ {
if (b.id != yardId) if (b.id != yardId)
{ {
@@ -107,7 +106,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
REQUIRE(yardId != kInvalidBuildingId); REQUIRE(yardId != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
fillMaterials(sim, yardId, *def); fillMaterials(sim, yardId, *def);
// First tick: materials consumed, production cycle starts — no ship yet. // First tick: materials consumed, production cycle starts — no ship yet.
@@ -166,7 +165,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
const int shipsBefore = countShips(sim); const int shipsBefore = countShips(sim);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
// Materials remain at zero (default after setRecipe); no cycle starts. // Materials remain at zero (default after setRecipe); no cycle starts.
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
@@ -188,7 +187,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef); const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id); SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);

View File

@@ -7,6 +7,7 @@
#include "BuildingId.h" #include "BuildingId.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "Rotation.h" #include "Rotation.h"
#include "FactoryState.h"
#include "Simulation.h" #include "Simulation.h"
class BeltSystem; class BeltSystem;
@@ -25,6 +26,7 @@ class BuildingSystem;
struct SimulationTestAccess struct SimulationTestAccess
{ {
static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); } static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
static FactoryState& state(Simulation& sim) { return sim.m_factoryState; }
static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); } static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
static std::optional<BuildingId> place(Simulation& sim, BuildingType type, static std::optional<BuildingId> place(Simulation& sim, BuildingType type,

View File

@@ -1605,8 +1605,8 @@ void GameWorldView::drawPortItems(QPainter& painter)
painter.save(); painter.save();
painter.setClipRegion(clip); painter.setClipRegion(clip);
m_sim->getBuildings().forEachEmergingItem(drawItem); m_sim->getBuildings().forEachEmergingItem(m_sim->getFactoryState(), drawItem);
m_sim->getBuildings().forEachIncomingItem(drawItem); m_sim->getBuildings().forEachIncomingItem(m_sim->getFactoryState(), drawItem);
painter.restore(); painter.restore();
} }