29 Commits

Author SHA1 Message Date
60cc187d92 add BuildingGrid to manage tile occupancy 2026-08-04 18:26:01 +02:00
3990351a16 share BuildingSystem's free functions instead of copying them 2026-08-04 18:24:38 +02:00
bd344e4fbe add FieldSelectionPanel for extracting the ships/stations/debris selection 2026-08-04 18:23:28 +02:00
64c344c3a3 correct the belt subsystem interface description in architecture.md 2026-08-04 18:12:02 +02:00
02c7fed9b4 allow "auto" for named local lambdas and iterator types via claude.md 2026-08-04 18:11:37 +02:00
5d4a975384 cover unlock state in the determinism tests 2026-08-04 18:11:03 +02:00
475df0e5fd extract unlock state from Simulation to UnlockState class 2026-08-04 18:10:43 +02:00
61634f6fd2 move the shared TOML helpers into the utility namespace to avoid name collisions 2026-08-04 18:05:52 +02:00
c8ff7da345 extract load methods into their own files 2026-08-04 18:05:27 +02:00
d664ab54cc extract shared TOML helpers into TomlHelpers.h/.cpp 2026-08-04 18:02:23 +02:00
906000b0e9 drop the dead payloads from the state-change events 2026-08-03 22:02:47 +02:00
d9ef6aa728 make HeaderBar read the tick, artifacts and boss wave from the simulation instead of event 2026-08-03 22:02:19 +02:00
b44a85685e make BlueprintPanel read the block stock from the simulation instead of event 2026-08-03 22:02:05 +02:00
edd1c31785 remove duplicate findModuleDef from ShipLayoutPreview 2026-08-03 22:00:25 +02:00
785ce3ebfe remove duplicate findBuildingDef from BuildingSystem 2026-08-03 21:14:13 +02:00
7017f8b4dc route the win-path restart through ResetCommand 2026-08-03 21:13:43 +02:00
77a842f884 dedupe AttackExecutor and RepairExecutor via executeOrbitAndAssign 2026-08-03 21:13:24 +02:00
d5ba72d554 add ModalPauseScope for the pause-around-modal idiom 2026-08-03 21:11:52 +02:00
83177729e9 extract MainWindow::reloadConfig 2026-08-03 21:11:02 +02:00
a8e933b7f2 drop EntityAdmin::add in favour of addComponent 2026-08-03 21:09:18 +02:00
e02e323cb2 share a single ItemIconCache across the UI 2026-08-03 21:08:19 +02:00
b4e622daa5 extract the shared Centroid helper into ai/Centroid.h 2026-08-03 21:06:06 +02:00
1150985c1f share one loadTestConfig() helper across the tests 2026-08-03 21:05:28 +02:00
594c3b93c5 make HeaderBar read block stock and expansion cost from the simulation 2026-08-03 21:04:43 +02:00
932b57720c dedupe tunnel lookup and key tunnel tiles by QPoint 2026-08-03 21:02:06 +02:00
ca727bef35 extract Simulation::initializeSubsystems to remove duplicate code 2026-08-03 20:59:00 +02:00
553a7e0701 add findShipDef/findModuleDef/findRecipeDef to config structs and re-use them in the rest of the code base 2026-08-03 20:57:31 +02:00
af6828c348 remove duplicate findBuildingDef from GameWorldView 2026-08-03 20:50:03 +02:00
3671e1d7e6 fix splitter filters being lost when rotating in place and add test 2026-08-03 20:49:10 +02:00
57 changed files with 1914 additions and 1950 deletions

View File

@@ -1,6 +1,3 @@
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")

View File

@@ -46,8 +46,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
, m_finished(false)
, m_stopRequested(false)
{
m_factoryState = makeFactoryState(m_gameConfig);
m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig,
m_beltSystem,
@@ -164,7 +162,7 @@ void ArenaSimulation::placeStructures()
hp, hp, false);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
@@ -185,7 +183,7 @@ void ArenaSimulation::placeStructures()
hp, hp, true);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
@@ -239,7 +237,7 @@ void ArenaSimulation::placeStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{stationEntity});
}
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
};
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
@@ -324,13 +322,13 @@ void ArenaSimulation::tick()
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
// Module + combat systems emit their tool beams into a shared buffer.
m_shipSystem->clearMovementIntents();
m_aiSystem->tick(m_admin, m_factoryState);
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
std::vector<BeamFiredEvent> beamFiredEvents;
m_salvagerSystem->tick(m_currentTick, m_factoryState, beamFiredEvents);
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, beamFiredEvents);
m_repairSystem->tick(m_currentTick, beamFiredEvents);
// Combat resolution (tick step 8).
m_combatSystem->tick(m_currentTick, m_admin, beamFiredEvents);
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, beamFiredEvents);
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
@@ -394,7 +392,7 @@ void ArenaSimulation::tickDeaths()
for (entt::entity deadEntity : deadStations)
{
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
{
std::vector<entt::entity> stationChildren;
m_admin.forEach<ModuleOwnerComponent>(
@@ -489,11 +487,6 @@ const ArenaConfig& ArenaSimulation::getArenaConfig() const
return m_arenaConfig;
}
const FactoryState& ArenaSimulation::getFactoryState() const
{
return m_factoryState;
}
const BuildingSystem& ArenaSimulation::getBuildings() const
{
return *m_buildingSystem;

View File

@@ -10,7 +10,6 @@
#include "BalancingConfig.h"
#include "BeltSystem.h"
#include "FactoryState.h"
#include "EntityAdmin.h"
#include "BuildingId.h"
@@ -86,7 +85,6 @@ public:
const ArenaConfig& getArenaConfig() const;
const BuildingSystem& getBuildings() const;
const FactoryState& getFactoryState() const;
const ShipSystem& getShips() const;
const DebrisSystem& getDebrisSystem() const;
EntityAdmin& getAdmin();
@@ -109,7 +107,6 @@ private:
BuildingId m_nextBuildingId;
EntityAdmin m_admin;
FactoryState m_factoryState;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ShipSystem> m_shipSystem;

View File

@@ -1,5 +1,4 @@
#include "ArenaView.h"
#include "FactoryQueries.h"
#include <algorithm>
#include <cmath>
@@ -307,7 +306,7 @@ void ArenaView::drawTiles(QPainter& painter)
void ArenaView::drawBuildings(QPainter& painter)
{
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(b.type);
@@ -340,7 +339,7 @@ void ArenaView::drawBuildings(QPainter& painter)
void ArenaView::drawDebris(QPainter& painter)
{
const float r = getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{
const QPointF center = worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90));

View File

@@ -52,18 +52,3 @@ bool isBeltSubsystemType(BuildingType type)
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit;
}
bool isProductionBuildingType(BuildingType type)
{
switch (type)
{
case BuildingType::Miner:
case BuildingType::Smelter:
case BuildingType::Assembler:
case BuildingType::ReprocessingPlant:
case BuildingType::Shipyard:
return true;
default:
return false;
}
}

View File

@@ -35,10 +35,6 @@ std::string buildingTypeId(BuildingType type);
// they receive, matching against every recipe of their building type.
bool isAutoRecipeBuildingType(BuildingType type);
// Buildings that run a production cycle: Miner, Smelter, Assembler, Reprocessing
// Plant and Shipyard (REQ-UI-DEBUG-OVERLAY counts these).
bool isProductionBuildingType(BuildingType type);
// Belts, splitters, and tunnel ends keep their runtime data in the belt subsystem
// rather than in the Building instance, so placing/removing them must register or
// unregister a tile with BeltSystem.

View File

@@ -20,7 +20,6 @@ SET(HDRS
SET(SRCS
${SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.cpp
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp

View File

@@ -1,57 +0,0 @@
#include "PortGeometry.h"
#include <set>
#include <utility>
std::vector<Port> computeInputPorts(
const std::vector<QPoint>& bodyCells,
const std::vector<Port>& outputPorts)
{
// Build lookup sets for quick membership checks.
std::set<std::pair<int, int>> bodySet;
for (const QPoint& cell : bodyCells)
{
bodySet.insert({cell.x(), cell.y()});
}
std::set<std::pair<int, int>> outputPortTiles;
for (const Port& port : outputPorts)
{
outputPortTiles.insert({port.tile.x(), port.tile.y()});
}
// Neighbour deltas and the corresponding "inward" belt direction.
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = { 0, 0, -1, 1};
const Rotation inward[4] = {
Rotation::East, // neighbour is to the West; belt flows East toward building
Rotation::West, // neighbour is to the East; belt flows West toward building
Rotation::South, // neighbour is above (row-1); belt flows South toward building
Rotation::North // neighbour is below (row+1); belt flows North toward building
};
std::set<std::pair<int, int>> seen;
std::vector<Port> inputPorts;
for (const QPoint& cell : bodyCells)
{
for (int i = 0; i < 4; ++i)
{
const int nx = cell.x() + dx[i];
const int ny = cell.y() + dy[i];
const std::pair<int, int> neighbor = {nx, ny};
if (bodySet.count(neighbor)) { continue; }
if (outputPortTiles.count(neighbor)){ continue; }
if (seen.count(neighbor)) { continue; }
seen.insert(neighbor);
Port port;
port.tile = QPoint(nx, ny);
port.direction = inward[i];
inputPorts.push_back(port);
}
}
return inputPorts;
}

View File

@@ -1,10 +1,7 @@
#pragma once
#include <vector>
#include <QPoint>
#include "Port.h"
#include "Rotation.h"
// Geometry of a building's input/output ports. A Port names the tile *outside* the
@@ -46,10 +43,3 @@ inline QPoint inputBodyTile(QPoint portTile, Rotation inwardDirection)
}
return portTile;
}
// Every belt-facing edge of a footprint that is not already an output port — the
// tiles a belt can feed the building from, with the direction items must flow to
// enter (REQ-MAT-INPUT-PORTS, REQ-BLD-BELT-DRAG). bodyCells and outputPorts are
// in absolute tile coordinates, and so is the result.
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
const std::vector<Port>& outputPorts);

View File

@@ -1,5 +1,4 @@
#include "AiSystem.h"
#include "FactoryQueries.h"
#include <limits>
@@ -43,7 +42,8 @@ AiSystem::AiSystem(const GameConfig& config)
{
}
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
const DebrisSystem& debris)
{
TRACE();
@@ -54,8 +54,8 @@ void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
m_retreatEvaluator.evaluate(admin);
m_attackEvaluator.evaluate(admin);
m_repairEvaluator.evaluate(admin);
m_salvageScrapEvaluator.evaluate(admin);
m_deliverScrapEvaluator.evaluate(admin, state);
m_salvageScrapEvaluator.evaluate(admin, debris);
m_deliverScrapEvaluator.evaluate(admin, buildings);
// Phase 2: pick the highest-scoring behavior per ship.
selectWinningBehaviors(admin);
@@ -68,7 +68,7 @@ void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
m_attackExecutor.execute(admin);
m_repairExecutor.execute(admin);
m_salvageScrapExecutor.execute(admin);
m_deliverScrapExecutor.execute(admin, state);
m_deliverScrapExecutor.execute(admin, buildings);
}
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)

View File

@@ -1,7 +1,5 @@
#pragma once
#include "FactoryQueries.h"
#include "AdvanceEvaluator.h"
#include "AdvanceExecutor.h"
#include "AttackEvaluator.h"
@@ -19,7 +17,9 @@
#include "StandbyEvaluator.h"
#include "StandbyExecutor.h"
class BuildingSystem;
class EntityAdmin;
class DebrisSystem;
struct GameConfig;
// Orchestrates ship-behavior decision-making in three batched phases:
@@ -34,7 +34,7 @@ class AiSystem
public:
explicit AiSystem(const GameConfig& config);
void tick(EntityAdmin& admin, const FactoryState& state);
void tick(EntityAdmin& admin, const BuildingSystem& buildings, const DebrisSystem& debris);
private:
void selectWinningBehaviors(EntityAdmin& admin);

View File

@@ -17,6 +17,7 @@ CombatSystem::CombatSystem(const GameConfig& config)
void CombatSystem::tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& /*buildings*/,
std::vector<BeamFiredEvent>& outBeamFiredEvents)
{
TRACE();

View File

@@ -15,6 +15,7 @@
#include "entt/entity/entity.hpp"
class BuildingSystem;
class EntityAdmin;
class CombatSystem
@@ -24,6 +25,7 @@ public:
void tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents);
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);

View File

@@ -46,13 +46,13 @@ std::optional<int> DebrisSystem::consume(entt::entity entity)
return amount;
}
bool collectOne(EntityAdmin& admin, entt::entity entity)
bool DebrisSystem::collectOne(entt::entity entity)
{
if (!admin.isValid(entity) || !admin.hasAll<DebrisComponent>(entity))
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
{
return false;
}
DebrisComponent& data = admin.get<DebrisComponent>(entity);
DebrisComponent& data = m_admin.get<DebrisComponent>(entity);
if (data.amount <= 0)
{
return false;
@@ -60,18 +60,18 @@ bool collectOne(EntityAdmin& admin, entt::entity entity)
--data.amount;
if (data.amount <= 0)
{
admin.destroy(entity);
m_admin.destroy(entity);
}
return true;
}
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin)
std::vector<DebrisInfo> DebrisSystem::getAllDebrisInfo() const
{
std::vector<DebrisInfo> result;
admin.forEach<DebrisComponent>(
[&result, &admin](entt::entity e, const DebrisComponent& sd)
m_admin.forEach<DebrisComponent>(
[&result, this](entt::entity e, const DebrisComponent& sd)
{
result.push_back(DebrisInfo{e, admin.get<PositionComponent>(e).value, sd.amount});
result.push_back(DebrisInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
});
return result;
}

View File

@@ -38,17 +38,9 @@ public:
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
bool collectOne(entt::entity entity);
// Lightweight snapshot for callers that need to iterate all debris.
std::vector<DebrisInfo> getAllDebrisInfo() const;
private:
EntityAdmin& m_admin;
};
// Debris state read and changed straight off the registry — no system needed.
// Lightweight snapshot for callers that need to iterate all debris.
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin);
// Collects a single scrap unit from the debris: decrements its amount by one,
// destroying the entity once depleted. Returns true if a scrap was collected,
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
bool collectOne(EntityAdmin& admin, entt::entity entity);

View File

@@ -1,5 +1,4 @@
#include "SalvagerSystem.h"
#include "FactoryQueries.h"
#include <vector>
@@ -24,14 +23,14 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
{
}
void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents)
{
TRACE();
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
applyPendingCollections(currentTick);
applyPendingCollections(currentTick, debris);
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(m_admin);
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
// Tick down per-module collection cooldowns.
m_admin.forEach<SalvagerComponent>(
@@ -90,7 +89,7 @@ void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
{
if (!deliver.deliveryBay.has_value()) { return; }
const Building* bay = findBuilding(state, *deliver.deliveryBay);
const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
if (!bay) { return; }
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
@@ -101,14 +100,14 @@ void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
if (cargo.current <= 0) { return; }
if (deliverScrapToSalvageBay(state, *deliver.deliveryBay))
if (buildings.deliverScrapToSalvageBay(*deliver.deliveryBay))
{
--cargo.current;
}
});
}
void SalvagerSystem::applyPendingCollections(Tick currentTick)
void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& debris)
{
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
while (it != m_pendingCollections.end())
@@ -118,7 +117,7 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick)
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
{
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
if (cargo.current < cargo.maxCapacity && collectOne(m_admin, it->debris))
if (cargo.current < cargo.maxCapacity && debris.collectOne(it->debris))
{
++cargo.current;
}

View File

@@ -1,7 +1,5 @@
#pragma once
#include "FactoryQueries.h"
#include <vector>
#include "BeamFiredEvent.h"
@@ -9,7 +7,9 @@
#include "entt/entity/entity.hpp"
class BuildingSystem;
class EntityAdmin;
class DebrisSystem;
// World-mutation system for salvage modules: each module runs a collection cycle
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
@@ -21,7 +21,7 @@ class SalvagerSystem
public:
explicit SalvagerSystem(EntityAdmin& admin);
void tick(Tick currentTick, FactoryState& state,
void tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
std::vector<BeamFiredEvent>& outBeamFiredEvents);
private:
@@ -32,7 +32,7 @@ private:
Tick appliesAt;
};
void applyPendingCollections(Tick currentTick);
void applyPendingCollections(Tick currentTick, DebrisSystem& debris);
EntityAdmin& m_admin;
std::vector<PendingCollection> m_pendingCollections;

View File

@@ -1,5 +1,4 @@
#include "DeliverScrapEvaluator.h"
#include "FactoryQueries.h"
#include <unordered_map>
@@ -13,7 +12,7 @@
#include "PositionComponent.h"
#include "tracing.h"
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const FactoryState& state)
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& buildings)
{
TRACE();
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
@@ -35,7 +34,7 @@ void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const FactoryState& sta
if (!deliver.deliveryBay.has_value())
{
const Building* bay =
findNearestBuilding(state, pos.value, BuildingType::SalvageBay);
buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay);
if (bay) { deliver.deliveryBay = bay->id; }
}

View File

@@ -1,13 +1,12 @@
#pragma once
#include "FactoryQueries.h"
class EntityAdmin;
class BuildingSystem;
// Scores high only when the ship's cargo is full, and assigns the nearest
// SalvageBay as the delivery destination.
class DeliverScrapEvaluator
{
public:
void evaluate(EntityAdmin& admin, const FactoryState& state);
void evaluate(EntityAdmin& admin, const BuildingSystem& buildings);
};

View File

@@ -1,5 +1,4 @@
#include "DeliverScrapExecutor.h"
#include "FactoryQueries.h"
#include <QVector2D>
@@ -13,7 +12,7 @@
#include "SelectedBehaviorComponent.h"
#include "tracing.h"
void DeliverScrapExecutor::execute(EntityAdmin& admin, const FactoryState& state)
void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& buildings)
{
TRACE();
admin.forEach<DeliverScrapBehavior, SelectedBehaviorComponent, PositionComponent,
@@ -27,7 +26,7 @@ void DeliverScrapExecutor::execute(EntityAdmin& admin, const FactoryState& state
QVector2D dest = pos.value;
if (deliver.deliveryBay.has_value())
{
const Building* bay = findBuilding(state, *deliver.deliveryBay);
const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
if (bay)
{
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,

View File

@@ -1,13 +1,12 @@
#pragma once
#include "FactoryQueries.h"
class EntityAdmin;
class BuildingSystem;
// Moves a ship toward its delivery bay when DeliverScrap is the winning
// behavior. Never decrements cargo — SalvagerSystem performs the delivery.
class DeliverScrapExecutor
{
public:
void execute(EntityAdmin& admin, const FactoryState& state);
void execute(EntityAdmin& admin, const BuildingSystem& buildings);
};

View File

@@ -15,11 +15,11 @@
#include "SensorRangeComponent.h"
#include "tracing.h"
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin)
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const DebrisSystem& debris)
{
TRACE();
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(admin);
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,

View File

@@ -1,6 +1,7 @@
#pragma once
class EntityAdmin;
class DebrisSystem;
// When cargo is not full, finds the nearest debris within sensor range and sets
// it as the target, scoring high. Scores inactive when cargo is full or no debris
@@ -8,5 +9,5 @@ class EntityAdmin;
class SalvageScrapEvaluator
{
public:
void evaluate(EntityAdmin& admin);
void evaluate(EntityAdmin& admin, const DebrisSystem& debris);
};

View File

@@ -1,173 +0,0 @@
#include "BuildingBuffers.h"
#include <algorithm>
#include <cassert>
#include "BuildingType.h"
#include "ItemType.h"
#include "ModulesConfig.h"
#include "ShipsConfig.h"
void initBuffers(Building& b, const RecipeDef& recipe)
{
b.inputBuffer.counts.clear();
b.inputBuffer.caps.clear();
for (const RecipeIngredient& ing : recipe.inputs)
{
const ItemType type{ing.item};
b.inputBuffer.counts[type] = 0;
b.inputBuffer.caps[type] = 2 * ing.amount;
}
b.outputBuffer.items.clear();
if (b.type == BuildingType::ReprocessingPlant)
{
// 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
int maxAmount = 0;
for (const RecipeOutput& out : recipe.outputs)
{
if (out.amount > maxAmount)
{
maxAmount = out.amount;
}
}
b.outputBuffer.capacity = maxAmount;
}
else
{
// 2× per-cycle output.
int totalAmount = 0;
for (const RecipeOutput& out : recipe.outputs)
{
totalAmount += out.amount;
}
b.outputBuffer.capacity = 2 * totalAmount;
}
}
void initAutoBuffers(const GameConfig& config, Building& b)
{
b.inputBuffer.counts.clear();
b.inputBuffer.caps.clear();
// Union the inputs of every recipe of this building type; the cap for each
// item is twice the largest per-cycle requirement across those recipes.
// Output capacity follows the same rules as initBuffers: the Reprocessing
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
// other auto buildings hold twice the largest per-cycle output.
int outputCapacity = 0;
for (const RecipeDef& recipe : config.recipes.recipes)
{
if (recipe.building != b.type)
{
continue;
}
for (const RecipeIngredient& ing : recipe.inputs)
{
const ItemType type{ing.item};
b.inputBuffer.counts[type] = 0;
b.inputBuffer.caps[type] =
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
}
if (b.type == BuildingType::ReprocessingPlant)
{
int maxAmount = 0;
for (const RecipeOutput& out : recipe.outputs)
{
maxAmount = std::max(maxAmount, out.amount);
}
outputCapacity = std::max(outputCapacity, maxAmount);
}
else
{
int totalAmount = 0;
for (const RecipeOutput& out : recipe.outputs)
{
totalAmount += out.amount;
}
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
}
}
b.outputBuffer.items.clear();
b.outputBuffer.capacity = outputCapacity;
}
void initShipyardBuffers(const GameConfig& config, Building& b)
{
b.inputBuffer.counts.clear();
b.inputBuffer.caps.clear();
b.outputBuffer.items.clear();
b.outputBuffer.capacity = 0;
const ShipDef* def = config.ships.findShipDef(b.recipeId);
if (!def)
{
return;
}
for (const RecipeIngredient& ing : def->schematic.materials)
{
const ItemType type{ing.item};
b.inputBuffer.counts[type] = 0;
b.inputBuffer.caps[type] = 2 * ing.amount;
}
if (b.shipLayout.has_value())
{
for (const PlacedModule& pm : b.shipLayout->placedModules)
{
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
for (const RecipeIngredient& ing : modDef->materials)
{
const ItemType type{ing.item};
b.inputBuffer.counts.try_emplace(type, 0);
b.inputBuffer.caps[type] += 2 * ing.amount;
}
}
}
}
void initSalvageBayBuffer(const GameConfig& config, Building& b)
{
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
b.outputBuffer.items.clear();
const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::SalvageBay);
b.outputBuffer.capacity =
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
}
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
const Building& building,
const std::vector<ItemType>& splitterFilterA,
const std::vector<ItemType>& splitterFilterB)
{
switch (building.type)
{
case BuildingType::Belt:
belts.placeBelt(building.anchor, building.rotation);
break;
case BuildingType::Splitter:
assert(building.outputPorts.size() >= 2);
belts.placeSplitter(building.anchor,
building.outputPorts[0].direction,
building.outputPorts[1].direction);
belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
break;
case BuildingType::TunnelEntry:
belts.placeTunnelEntry(building.anchor, building.rotation,
config.world.tunnelMaxDistance_tiles);
break;
case BuildingType::TunnelExit:
belts.placeTunnelExit(building.anchor, building.rotation);
break;
default:
break;
}
}

View File

@@ -1,39 +0,0 @@
#pragma once
#include <vector>
#include "BeltSystem.h"
#include "Building.h"
#include "GameConfig.h"
#include "ItemType.h"
#include "RecipesConfig.h"
// Setting a building up when it starts existing or is reconfigured: sizing its
// input/output buffers from what it will produce, and handing belt-like types back
// to BeltSystem. Free functions over the config and the building — they read no
// factory state, so both BuildingSystem and ConstructionSystem can use them.
// Buffers for a building running one known recipe: inputs capped at twice each
// ingredient's per-cycle amount, output at twice the per-cycle total (one cycle's
// max for a Reprocessing Plant, REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
void initBuffers(Building& b, const RecipeDef& recipe);
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant), unioned over
// every recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
void initAutoBuffers(const GameConfig& config, Building& b);
// Buffers for a shipyard: its schematic's materials plus those of every placed
// module (REQ-BLD-SHIPYARD).
void initShipyardBuffers(const GameConfig& config, Building& b);
// The Salvage Bay holds no recipe inputs; its output capacity is config-defined
// (REQ-BLD-SALVAGE-BAY).
void initSalvageBayBuffer(const GameConfig& config, Building& b);
// Registers a belt, splitter or tunnel end with BeltSystem. A splitter's filters
// live in BeltSystem and are lost by removeTile, so they are passed back in
// (REQ-BLD-SPLITTER). No-op for every other building type.
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
const Building& building,
const std::vector<ItemType>& splitterFilterA,
const std::vector<ItemType>& splitterFilterB);

View File

@@ -1,5 +1,4 @@
#include "BuildingConfig.h"
#include "FactoryQueries.h"
#include <algorithm>
#include <climits>
@@ -27,8 +26,8 @@ struct SelectedBuilding
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
{
const Building* building = findBuilding(sim.getFactoryState(), id);
const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id);
const Building* building = sim.getBuildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
if (!building && !site)
{
return std::nullopt;
@@ -53,8 +52,8 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
{
const Building* building = findBuilding(sim.getFactoryState(), id);
const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id);
const Building* building = sim.getBuildings().findBuilding(id);
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
if (!building && !site)
{
return std::nullopt;

File diff suppressed because it is too large Load Diff

View File

@@ -15,11 +15,7 @@
#include "BeltSystem.h"
#include "Building.h"
#include "FactoryState.h"
#include "BuildingBuffers.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h"
#include "ProductionRules.h"
#include "BuildingGrid.h"
#include "BuildingType.h"
#include "BuildingId.h"
#include "GameConfig.h"
@@ -31,6 +27,18 @@
class Hasher;
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
// The simulation owns the classification so it stays in sync with the
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
// color.
enum class ProductionStatus
{
Unconfigured, // no recipe/schematic selected (grey)
Producing, // a production cycle is active (green)
Starved, // idle: a required input is missing / Salvage Bay empty (red)
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
};
// Manages building placement, construction queuing, and the per-tick
// production loop (belt→building pull, production, building→belt push).
// All types including Belt and Splitter are stored as Building instances;
@@ -54,7 +62,7 @@ public:
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
// arbitrary layouts; the player-facing entry point
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
std::optional<BuildingId> place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation,
std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation,
Tick currentTick);
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
@@ -62,12 +70,13 @@ public:
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
// and every cell has 0 <= y < world.height_tiles. There is no right-side
// bound — space extends rightward. Tile occupancy is NOT checked here.
bool isPlacementValid(BuildingType type, QPoint anchor,
Rotation rotation) const;
// Sets the current buildable asteroid width in tiles. Grows the left
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
// Defaults to world.regions.asteroid_width_tiles at construction.
void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const
{ state.asteroidWidth_tiles = widthTiles; }
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
// A construction site is removed instantly and the full cost is returned.
@@ -75,23 +84,24 @@ public:
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
// credited later, on completion in tickDeconstruction, so this returns 0 for
// it. Returns 0 for unknown ids and for a building already queued.
int deconstruct(FactoryState& state, BuildingId id, Tick currentTick);
int deconstruct(BuildingId id, Tick currentTick);
// Take a building back out of the deconstruction queue before it is removed
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
// progress and credits no refund. No-op if the id is not queued.
void cancelDeconstruction(FactoryState& state, BuildingId id);
void cancelDeconstruction(BuildingId id);
// True if the building is currently in the deconstruction queue.
bool isQueuedForDeconstruction(BuildingId id) const;
// Set the recipe (or schematic id for shipyard) on a building or queued
// construction site. Clears both buffers on an operational building.
void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId);
void setRecipe(BuildingId id, const std::string& recipeId);
// Set the module layout for a shipyard. Cancels in-progress production
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout);
void setShipLayout(BuildingId id, const ShipLayoutConfig& layout);
// Splitter filter configuration for a queued/under-construction Splitter
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
@@ -100,94 +110,130 @@ public:
// 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
// to BeltSystem when the splitter finishes building (tickConstruction).
void setSiteSplitterFilters(FactoryState& state, BuildingId id,
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(BuildingId id) const;
void setSiteSplitterFilters(BuildingId id,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB);
// -- Tick hooks (called from Simulation::tick in the documented order) ---
void tickConstruction(Tick currentTick);
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
// time, in parallel with tickConstruction. Removes the front building and
// credits its refund when its timer elapses.
void tickBeltPull(FactoryState& state);
void tickProduction(FactoryState& state, Tick currentTick);
void tickShipyardProduction(FactoryState& state, Tick currentTick);
void tickDeconstruction(Tick currentTick);
void tickBeltPull();
void tickProduction(Tick currentTick);
void tickShipyardProduction(Tick currentTick);
// Advances each building's virtual output belts, hands finished items off onto
// the adjacent real belt, and feeds new buffered items into them
// (REQ-MAT-OUTPUT-EMERGE).
void tickOutputBelts(FactoryState& state);
void tickOutputBelts();
// -- Queries -------------------------------------------------------------
struct BeltTileInfo
{
BuildingId buildingId;
QPoint tile;
BuildingType type; // Belt or Splitter
Rotation directionA; // Belt: its direction; Splitter: first output
Rotation directionB; // Splitter: second output; Belt: same as directionA
};
const Building* findBuilding(BuildingId id) const;
const ConstructionSite* findSite(BuildingId id) const;
std::vector<Building> getAllBuildings() const;
std::vector<ConstructionSite> getAllSites() const;
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
int getProductionBuildingCount() const;
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
// that currently has an active production cycle.
int getActiveProductionBuildingCount() const;
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
// nullopt for building types that show no light (belts, splitters, tunnels,
// HQ, defence stations). The Salvage Bay is a two-state special case:
// Producing while its output buffer holds scrap, Starved when empty.
std::optional<ProductionStatus> getProductionStatus(const Building& building) const;
std::vector<BeltTileInfo> getAllBeltTiles() const;
bool isTileOccupied(QPoint tile) const;
// Visits every item currently emerging from a building output port on 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
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
void forEachEmergingItem(const FactoryState& state,
void forEachEmergingItem(
const std::function<void(const ItemType&, QPointF)>& visit) const;
// 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
// world-space centre (in tile units). Least-progressed first (drawn bottom).
void forEachIncomingItem(const FactoryState& state,
void forEachIncomingItem(
const std::function<void(const ItemType&, QPointF)>& visit) const;
// Returns the entity id of the building or construction site whose footprint
// exactly coincides with the ghost (type, anchor, rot) and is of the same
// building type. Returns nullopt otherwise.
std::optional<BuildingId> findRotateInPlaceTarget(BuildingType type,
QPoint anchor,
Rotation rot) const;
// Rotate an existing building or construction site to newRotation in place.
// For belt-type operational buildings, re-registers with BeltSystem (items
// currently on the tile are discarded by BeltSystem::removeTile).
void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation);
void rotateInPlace(BuildingId id, Rotation newRotation);
// Find nearest operational building of the given type; nullptr if none.
const Building* findNearestBuilding(QVector2D worldPos, BuildingType type) const;
// Input-capable adjacent tiles for a building or construction site
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
// outside adjacent tile and Port.direction is the belt facing that points into
// the target. Output-port edges are excluded. Empty for an unknown id.
std::vector<Port> getInputPorts(BuildingId id) const;
// Register / unregister tile occupancy for ECS station entities.
void registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
void unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells);
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
// Place one "scrap" item into a SalvageBay's output buffer.
// Returns false if bay not found, wrong type, or output buffer is full.
bool deliverScrapToSalvageBay(BuildingId bayId);
// Bypass the construction queue and create a fully-operational Building
// immediately. Used for pre-placed structures (HQ, defence stations).
// surfaceMask comes from the relevant config struct.
BuildingId placeImmediate(FactoryState& state, BuildingType type,
BuildingId placeImmediate(BuildingType type,
const std::vector<std::string>& surfaceMask,
QPoint anchor, Rotation rotation);
// Remove an operational building by id without refund (used for deaths).
// Returns true if found and removed.
bool removeBuilding(FactoryState& state, BuildingId id);
bool removeBuilding(BuildingId id);
// Mutable iteration over all operational buildings.
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
void forEachBuilding(std::function<void(Building&)> fn);
// -- Determinism ---------------------------------------------------------
// Folds all building, construction-site, and tile-occupancy state into the
// hasher in deterministic order (see docs/replay_design.md).
void appendChecksum(const FactoryState& state, Hasher& hasher) const;
void appendChecksum(Hasher& hasher) const;
private:
// Starts the front deconstruction-queue entry's timer if not yet started
// (mirrors how tickConstruction starts a queued construction site).
void startFrontDeconstruction(Tick currentTick);
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
// (on construction completion, or when un-queuing a deconstruction). No-op for
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
void reregisterBeltTile(const Building& building,
const std::vector<ItemType>& splitterFilterA,
const std::vector<ItemType>& splitterFilterB);
Building* findBuildingMutable(BuildingId id);
// True if the consumer would accept `type` at the given input port right now:
// it is a required input (or a building block for the HQ), the reservation-aware
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
@@ -202,7 +248,7 @@ private:
// 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).
// Returns true if the item was accepted onto the consumer's input belt.
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
bool tryDirectCoupleDeposit(BuildingId producerId,
const Port& outputPort,
const Item& item);
@@ -210,22 +256,35 @@ private:
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
// inputs; other buildings offer only their selected recipe. Shared by
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
std::vector<const RecipeDef*> gatherCandidateRecipes(const Building& b) const;
// True if every input of `recipe` is present in `b`'s input buffers in the
// required per-cycle amount (REQ-MAT-CYCLE input check).
bool recipeInputsAvailable(const Building& b,
const RecipeDef& recipe) const;
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
std::map<std::string, int> computeShipyardRequiredMaterials(const Building& b) const;
// True if the building currently has all inputs/materials to start a cycle
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
// the status light (REQ-UI-STATUS-LIGHT).
bool hasInputsToStart(const Building& b) const;
void initBuffers(Building& b, const RecipeDef& recipe) const;
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
// caps span the union of every recipe of the building's type; no player
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
void initAutoBuffers(Building& b) const;
void initShipyardBuffers(Building& b) const;
void initSalvageBayBuffer(Building& b) const;
std::vector<Port> computeInputPorts(const Building& b) const;
// Core input-edge scan shared by operational buildings and construction sites.
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
const std::vector<Port>& outputPorts) const;
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
bool bodyCellsWithinWorldBounds(
const std::vector<QPoint>& bodyCells,
QPoint anchor) const;
const GameConfig& m_config;
BeltSystem& m_belts;
std::function<BuildingId()> m_allocateBuildingId;
std::function<void(int)> m_addBuildingBlocks;
@@ -233,4 +292,25 @@ private:
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
std::function<bool(const std::string&)> m_isItemUnlocked;
std::mt19937& m_rng;
int m_asteroidWidth_tiles;
std::vector<Building> m_buildings;
std::deque<ConstructionSite> m_constructionQueue;
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
// completesAt == 0 means "queued but its timer has not started yet"
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
// here so cancelDeconstruction can restore them on re-registration.
struct DeconstructionEntry
{
BuildingId id = kInvalidBuildingId;
Tick completesAt = 0;
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
};
std::deque<DeconstructionEntry> m_deconstructionQueue;
// The authority on which building owns which tile; every placement and removal
// path claims and releases its body cells here.
BuildingGrid m_grid;
};

View File

@@ -13,13 +13,6 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
@@ -45,12 +38,6 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp

View File

@@ -1,112 +0,0 @@
#include "ConstructionSystem.h"
#include "BuildingBuffers.h"
#include "BuildingType.h"
#include "FactoryQueries.h"
#include "PortGeometry.h"
#include "SurfaceMask.h"
#include "tracing.h"
void ConstructionSystem::tick(FactoryState& state, BeltSystem& belts, Tick currentTick)
{
TRACE();
if (state.constructionQueue.empty())
{
return;
}
ConstructionSite& front = state.constructionQueue.front();
// Guard: if somehow the front site was never started, start it now.
if (front.completesAt == 0)
{
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
if (def)
{
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
}
return;
}
if (currentTick < front.completesAt)
{
return;
}
// Promote construction site to an operational Building.
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
const ParsedSurfaceMask mask = parseSurfaceMask(
def ? def->surfaceMask : std::vector<std::string>{},
front.rotation);
Building building;
building.id = front.id;
building.anchor = front.anchor;
building.footprint = front.footprint;
building.rotation = front.rotation;
building.type = front.type;
building.recipeId = front.recipeId;
building.shipLayout = front.shipLayout;
for (const QPoint& cell : mask.bodyCells)
{
building.bodyCells.push_back(front.anchor + cell);
}
for (const Port& port : mask.outputPorts)
{
Port absPort;
absPort.tile = front.anchor + port.tile;
absPort.direction = port.direction;
building.outputPorts.push_back(absPort);
}
building.emergingItems.resize(building.outputPorts.size());
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
building.incomingItems.assign(building.inputPorts.size(), {});
if (building.type == BuildingType::SalvageBay)
{
initSalvageBayBuffer(m_config, building);
}
else if (isAutoRecipeBuildingType(building.type))
{
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
initAutoBuffers(m_config, building);
}
else if (!building.recipeId.empty())
{
if (building.type == BuildingType::Shipyard)
{
initShipyardBuffers(m_config, building);
}
else
{
const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
if (recipe)
{
initBuffers(building, *recipe);
}
}
}
// Register with BeltSystem before the move (mask/building stays valid). Any
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
reregisterBeltTile(belts, m_config, building, front.splitterFilterA, front.splitterFilterB);
state.buildings.push_back(std::move(building));
state.constructionQueue.pop_front();
// Start next queued site if present.
if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0)
{
const BuildingDef* nextDef =
m_config.buildings.findBuildingDef(state.constructionQueue.front().type);
if (nextDef)
{
state.constructionQueue.front().completesAt =
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
}
}
}

View File

@@ -1,30 +0,0 @@
#pragma once
#include "BeltSystem.h"
#include "FactoryState.h"
#include "GameConfig.h"
#include "Tick.h"
// Advances the construction queue and turns a finished site into an operational
// building (REQ-BLD-CONSTRUCTION). One site is built at a time, in queue order:
// the front site's timer runs, and when it elapses the site becomes a Building —
// its ports and buffers are derived from its definition, its belt tile is handed
// back to BeltSystem, and the next queued site starts.
//
// It completes the building itself rather than handing the finished site back to
// BuildingSystem: everything materialisation needs is either in FactoryState, the
// config, or a free function (see BuildingBuffers.h, PortGeometry.h), so there is
// no intermediate value to pass and no ordering rule between two calls.
//
// Holds only the config; the world it works on arrives per tick, like the other
// systems in lib/ecs/system.
class ConstructionSystem
{
public:
explicit ConstructionSystem(const GameConfig& config) : m_config(config) {}
void tick(FactoryState& state, BeltSystem& belts, Tick currentTick);
private:
const GameConfig& m_config;
};

View File

@@ -1,67 +0,0 @@
#include "DeconstructionSystem.h"
#include <vector>
#include "Building.h"
#include "tracing.h"
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
Tick currentTick)
{
if (state.deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = state.deconstructionQueue.front();
if (front.completesAt == 0)
{
front.completesAt =
currentTick + secondsToTicks(config.world.deconstructionTimeSeconds);
}
}
void DeconstructionSystem::tick(FactoryState& state, Tick currentTick)
{
TRACE();
if (state.deconstructionQueue.empty())
{
return;
}
DeconstructionEntry& front = state.deconstructionQueue.front();
// Guard: if the front entry's timer was never started, start it now.
if (front.completesAt == 0)
{
startFrontDeconstruction(state, m_config, currentTick);
return;
}
if (currentTick < front.completesAt)
{
return;
}
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
// Belt/tunnel/splitter tiles were already unregistered when the building was
// queued (see deconstruct), so only tile occupancy and the record remain.
for (std::vector<Building>::iterator it = state.buildings.begin();
it != state.buildings.end();
++it)
{
if (it->id != front.id) { continue; }
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
state.grid.release(it->bodyCells);
state.buildings.erase(it);
if (def)
{
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
}
break;
}
state.deconstructionQueue.pop_front();
// Start the next queued deconstruction, if any.
startFrontDeconstruction(state, m_config, currentTick);
}

View File

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

View File

@@ -1,181 +0,0 @@
#include "FactoryQueries.h"
#include <limits>
#include "PortGeometry.h"
#include "SurfaceMask.h"
#include "Item.h"
#include "ItemType.h"
const Building* findBuilding(const FactoryState& state, BuildingId id)
{
for (const Building& building : state.buildings)
{
if (building.id == id)
{
return &building;
}
}
return nullptr;
}
Building* findBuilding(FactoryState& state, BuildingId id)
{
for (Building& building : state.buildings)
{
if (building.id == id)
{
return &building;
}
}
return nullptr;
}
const ConstructionSite* findSite(const FactoryState& state, BuildingId id)
{
for (const ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
return &site;
}
}
return nullptr;
}
std::vector<Building> getAllBuildings(const FactoryState& state)
{
return state.buildings;
}
std::vector<ConstructionSite> getAllSites(const FactoryState& state)
{
return std::vector<ConstructionSite>(state.constructionQueue.begin(),
state.constructionQueue.end());
}
int getProductionBuildingCount(const FactoryState& state)
{
int count = 0;
for (const Building& b : state.buildings)
{
if (isProductionBuildingType(b.type)) { ++count; }
}
return count;
}
int getActiveProductionBuildingCount(const FactoryState& state)
{
int count = 0;
for (const Building& b : state.buildings)
{
if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
}
return count;
}
bool isTileOccupied(const FactoryState& state, QPoint tile)
{
return state.grid.isOccupied(tile);
}
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id)
{
const Building* building = findBuilding(state, id);
return building && building->queuedForDeconstruction;
}
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
BuildingType type)
{
const Building* best = nullptr;
float bestDist = std::numeric_limits<float>::max();
for (const Building& b : state.buildings)
{
if (b.type != type)
{
continue;
}
QVector2D center(b.anchor.x() + b.footprint.width() / 2.0f,
b.anchor.y() + b.footprint.height() / 2.0f);
float dist = (center - worldPos).length();
if (dist < bestDist)
{
bestDist = dist;
best = &b;
}
}
return best;
}
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId)
{
Building* bay = findBuilding(state, bayId);
if (!bay || bay->type != BuildingType::SalvageBay)
{
return false;
}
if (bay->queuedForDeconstruction)
{
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
}
// Emerging scrap still counts against the bay's holding capacity
// (REQ-MAT-OUTPUT-EMERGE).
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
{
return false;
}
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
return true;
}
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
BuildingId id)
{
if (const Building* building = findBuilding(state, id))
{
return building->inputPorts;
}
if (const ConstructionSite* site = findSite(state, id))
{
// A site stores no ports; derive its output ports from the mask (absolute)
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
const BuildingDef* def = config.buildings.findBuildingDef(site->type);
if (def == nullptr) { return {}; }
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
std::vector<Port> outputPortsAbsolute;
outputPortsAbsolute.reserve(mask.outputPorts.size());
for (const Port& port : mask.outputPorts)
{
outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
}
return computeInputPorts(site->bodyCells, outputPortsAbsolute);
}
return {};
}
std::optional<BeltSystem::SplitterInfo>
getSiteSplitterInfo(const FactoryState& state, const GameConfig& config, BuildingId id)
{
for (const ConstructionSite& site : state.constructionQueue)
{
if (site.id != id) { continue; }
if (site.type != BuildingType::Splitter) { return std::nullopt; }
const BuildingDef* def = config.buildings.findBuildingDef(site.type);
const ParsedSurfaceMask mask = parseSurfaceMask(
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
if (mask.outputPorts.size() < 2) { return std::nullopt; }
BeltSystem::SplitterInfo info;
info.outputA = mask.outputPorts[0].direction;
info.outputB = mask.outputPorts[1].direction;
info.filterA = site.splitterFilterA;
info.filterB = site.splitterFilterB;
return info;
}
return std::nullopt;
}

View File

@@ -1,71 +0,0 @@
#pragma once
#include <vector>
#include <QPoint>
#include <QVector2D>
#include "Building.h"
#include "BuildingId.h"
#include "BuildingType.h"
#include "BeltSystem.h"
#include "FactoryState.h"
#include "GameConfig.h"
#include "Port.h"
// Queries and operations over the factory's world data that need nothing but that
// data — no config, no belts, no RNG. Free functions rather than BuildingSystem
// methods so that callers depend on the data they read instead of on the system
// that happens to tick it (see FactoryState.h).
//
// Most need nothing but the state. The two at the bottom also take the config,
// because answering them means reading a building definition — but still no belts,
// no RNG and no system.
// The building with the given id, or nullptr when no building has it. Construction
// sites are not buildings yet — use findSite for those.
const Building* findBuilding(const FactoryState& state, BuildingId id);
Building* findBuilding(FactoryState& state, BuildingId id);
// The queued construction site with the given id, or nullptr.
const ConstructionSite* findSite(const FactoryState& state, BuildingId id);
std::vector<Building> getAllBuildings(const FactoryState& state);
std::vector<ConstructionSite> getAllSites(const FactoryState& state);
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
int getProductionBuildingCount(const FactoryState& state);
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above that
// currently has an active production cycle.
int getActiveProductionBuildingCount(const FactoryState& state);
bool isTileOccupied(const FactoryState& state, QPoint tile);
// True while the building is in the deconstruction queue (REQ-BLD-DECON-QUEUE).
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id);
// The nearest building of the given type to a world position, or nullptr when
// none exists. Distance is measured to the building's footprint centre.
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
BuildingType type);
// Hands one scrap to a Salvage Bay's output buffer (REQ-BLD-SALVAGE-BAY). Fails
// if the id is not a Salvage Bay, it is queued for deconstruction
// (REQ-BLD-DECON-QUEUE), or its holding capacity is already taken — emerging
// scrap counts against that capacity (REQ-MAT-OUTPUT-EMERGE).
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId);
// Every belt-facing edge of the building or site with this id (REQ-MAT-INPUT-PORTS,
// REQ-BLD-BELT-DRAG). Empty when the id is unknown. A site has no stored ports, so
// they are derived from its surface mask.
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
BuildingId id);
// The two output directions and stored filters of a queued Splitter site
// (REQ-BLD-SITE-CONFIG), or nullopt if the id is not one. Operational splitters are
// configured through BeltSystem by tile instead.
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(const FactoryState& state,
const GameConfig& config,
BuildingId id);

View File

@@ -1,66 +0,0 @@
#pragma once
#include <deque>
#include <vector>
#include "Building.h"
#include "GameConfig.h"
#include "BuildingGrid.h"
#include "BuildingId.h"
#include "ItemType.h"
#include "Tick.h"
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
// completesAt == 0 means "queued but its timer has not started yet"
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
// here so cancelDeconstruction can restore them on re-registration.
struct DeconstructionEntry
{
BuildingId id = kInvalidBuildingId;
Tick completesAt = 0;
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
};
// The factory's world data: every building, the work queued on them, and the
// tile ownership index. This is the buildings-side counterpart to EntityAdmin —
// data with no behaviour of its own beyond what BuildingGrid encapsulates.
//
// Buildings deliberately stay a plain vector rather than becoming EnTT entities
// (see docs/architecture.md). Separating this data from the systems that operate
// on it is not a step toward putting them in the entity model; it is the same
// data/behaviour split the ecs/system/ classes already follow, where world data
// arrives as a tick argument instead of being owned by the system.
//
// Owned by Simulation (and by ArenaSimulation in the balancing tool), not by the
// systems that operate on it. BuildingSystem holds a reference. The remaining step
// is to pass this into the tick methods instead, so the systems become stateless
// over it — that one is gated on the query surface, which today reaches the data
// through BuildingSystem's ~180 const call sites.
struct FactoryState
{
std::vector<Building> buildings;
std::deque<ConstructionSite> constructionQueue;
std::deque<DeconstructionEntry> deconstructionQueue;
// The authority on which building owns which tile; every placement and removal
// path claims and releases its body cells here.
BuildingGrid grid;
// Current buildable asteroid width, the left bound for placement. Grows as the
// player buys expansions (REQ-EXP-UNLOCK). Deliberately not checksummed: it is
// derived from config and Simulation's expansion count, which is folded already.
// Seeded from config by BuildingSystem's constructor.
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

@@ -1,121 +0,0 @@
#include "PlacementRules.h"
#include "BuildingType.h"
#include "FactoryQueries.h"
#include "SurfaceMask.h"
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,const std::vector<QPoint>& bodyCells,
QPoint anchor)
{
const int heightTiles = config.world.heightTiles;
const int leftEdgeX = -state.asteroidWidth_tiles;
for (const QPoint& cell : bodyCells)
{
const QPoint worldCell = anchor + cell;
if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
{
return false;
}
if (worldCell.x() < leftEdgeX)
{
return false;
}
}
return true;
}
bool isPlacementValid(const FactoryState& state, const GameConfig& config,BuildingType type, QPoint anchor,
Rotation rotation)
{
const BuildingDef* def = config.buildings.findBuildingDef(type);
if (def == nullptr)
{
return false;
}
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
if (!bodyCellsWithinWorldBounds(state, config, mask.bodyCells, anchor))
{
return false;
}
// Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
// (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
for (const QPoint& cell : mask.bodyCells)
{
const QPoint worldCell = anchor + cell;
bool isShipDock = false;
for (const QPoint& dock : mask.shipDockCells)
{
if (dock == cell)
{
isShipDock = true;
break;
}
}
if (isShipDock)
{
if (worldCell.x() < 0)
{
return false;
}
}
else if (worldCell.x() >= 0)
{
return false;
}
}
return true;
}
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, const GameConfig& config,
BuildingType type, QPoint anchor, Rotation rot)
{
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
{
return std::nullopt;
}
const BuildingDef* def = config.buildings.findBuildingDef(type);
if (!def) { return std::nullopt; }
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
if (mask.bodyCells.empty()) { return std::nullopt; }
// All body cells must be occupied by the same entity.
const QPoint firstAbs = anchor + mask.bodyCells[0];
const std::optional<BuildingId> firstOwner = state.grid.findOwner(firstAbs);
if (!firstOwner.has_value()) { return std::nullopt; }
const BuildingId candidateId = *firstOwner;
for (const QPoint& rel : mask.bodyCells)
{
const std::optional<BuildingId> owner = state.grid.findOwner(anchor + rel);
if (!owner.has_value() || *owner != candidateId)
{
return std::nullopt;
}
}
// Verify the candidate is the same building type with the same cell count.
for (const ConstructionSite& site : state.constructionQueue)
{
if (site.id != candidateId) { continue; }
if (site.type != type) { return std::nullopt; }
if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
return candidateId;
}
for (const Building& b : state.buildings)
{
if (b.id != candidateId) { continue; }
if (b.type != type) { return std::nullopt; }
if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
return candidateId;
}
return std::nullopt;
}

View File

@@ -1,38 +0,0 @@
#pragma once
#include <optional>
#include <vector>
#include <QPoint>
#include "BuildingId.h"
#include "BuildingType.h"
#include "FactoryState.h"
#include "GameConfig.h"
#include "Rotation.h"
// Where a building may be placed, and what is already sitting on those tiles.
// Free functions over the factory state and the config — they read no other
// system state, so they do not belong to BuildingSystem.
// True if every body cell lies inside the world: 0 <= y < world.height_tiles and
// x >= the current asteroid left edge (REQ-BLD-PLACE-VALID). Terrain type is not
// checked — see isPlacementValid for the full rule.
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,
const std::vector<QPoint>& bodyCells, QPoint anchor);
// True if the placement satisfies REQ-BLD-PLACE-VALID terrain and world-bounds
// rules: every ship-dock (S) cell sits in space (x >= 0), every other body (A)
// cell sits on the asteroid (x < 0 and x >= the left edge), and every cell has
// 0 <= y < world.height_tiles. There is no right-side bound — space extends
// rightward. Tile occupancy is NOT checked here.
bool isPlacementValid(const FactoryState& state, const GameConfig& config,
BuildingType type, QPoint anchor, Rotation rotation);
// The building or site that a ghost of the given type/anchor/rotation would
// rotate in place rather than replace: same type, same body cells, one owner
// (REQ-BLD-ROTATE-IN-PLACE). Tunnels never qualify.
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state,
const GameConfig& config,
BuildingType type, QPoint anchor,
Rotation rot);

View File

@@ -1,142 +0,0 @@
#include "ProductionRules.h"
#include "BuildingType.h"
#include "ItemType.h"
#include "ModulesConfig.h"
#include "ShipsConfig.h"
std::vector<const RecipeDef*>
gatherCandidateRecipes(const GameConfig& config, const Building& b)
{
std::vector<const RecipeDef*> candidates;
if (isAutoRecipeBuildingType(b.type))
{
for (const RecipeDef& r : config.recipes.recipes)
{
if (r.building == b.type && !r.inputs.empty())
{
candidates.push_back(&r);
}
}
}
else
{
const RecipeDef* recipe = config.recipes.findRecipeDef(b.recipeId, b.type);
if (recipe)
{
candidates.push_back(recipe);
}
}
return candidates;
}
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
{
for (const RecipeIngredient& ing : recipe.inputs)
{
const std::map<ItemType, int>::const_iterator it =
b.inputBuffer.counts.find(ItemType{ing.item});
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
if (have < ing.amount)
{
return false;
}
}
return true;
}
std::map<std::string, int>
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
{
std::map<std::string, int> requiredMaterials;
const ShipDef* shipDef = config.ships.findShipDef(b.recipeId);
if (!shipDef)
{
return requiredMaterials;
}
for (const RecipeIngredient& ing : shipDef->schematic.materials)
{
requiredMaterials[ing.item] += ing.amount;
}
if (b.shipLayout.has_value())
{
for (const PlacedModule& pm : b.shipLayout->placedModules)
{
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
for (const RecipeIngredient& ing : modDef->materials)
{
requiredMaterials[ing.item] += ing.amount;
}
}
}
return requiredMaterials;
}
bool hasInputsToStart(const GameConfig& config, const Building& b)
{
if (b.type == BuildingType::Shipyard)
{
const std::map<std::string, int> required =
computeShipyardRequiredMaterials(config, b);
for (const std::pair<const std::string, int>& req : required)
{
const std::map<ItemType, int>::const_iterator it =
b.inputBuffer.counts.find(ItemType{req.first});
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
if (have < req.second)
{
return false;
}
}
return true;
}
// Recipe buildings: startable if any candidate recipe's inputs are satisfied.
// A Miner recipe has no inputs, so an idle Miner is always startable and its
// only idle reason is a full output buffer.
for (const RecipeDef* recipe : gatherCandidateRecipes(config, b))
{
if (recipeInputsAvailable(b, *recipe))
{
return true;
}
}
return false;
}
std::optional<ProductionStatus>
getProductionStatus(const GameConfig& config, const Building& building)
{
// Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
// "producing" while it holds scrap to push out, and starved when empty.
if (building.type == BuildingType::SalvageBay)
{
return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
: ProductionStatus::Starved;
}
// Only the five recipe/cycle production types show a status light besides the
// Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
if (!isProductionBuildingType(building.type))
{
return std::nullopt;
}
// Grey only applies to player-configured types; auto-recipe buildings
// (Smelter, Reprocessing Plant) always run an implicit recipe.
if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty())
{
return ProductionStatus::Unconfigured;
}
if (building.production.has_value())
{
return ProductionStatus::Producing;
}
// Idle: missing inputs (red) take precedence over a full output buffer
// (yellow). If inputs are present yet the building is idle, the only remaining
// reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE).
return hasInputsToStart(config, building) ? ProductionStatus::Blocked
: ProductionStatus::Starved;
}

View File

@@ -1,47 +0,0 @@
#pragma once
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "Building.h"
#include "GameConfig.h"
#include "RecipesConfig.h"
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
// The simulation owns the classification so it stays in sync with the
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
// color.
enum class ProductionStatus
{
Unconfigured, // no recipe/schematic selected (grey)
Producing, // a production cycle is active (green)
Starved, // idle: a required input is missing / Salvage Bay empty (red)
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
};
// The rules deciding what a building can produce and whether it can start.
// Pure functions of the config and the building itself — they read no factory
// state, so they are free functions rather than BuildingSystem members.
// Recipes this building could run: every recipe of its type for an auto-recipe
// building (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), otherwise just its selected one.
std::vector<const RecipeDef*> gatherCandidateRecipes(const GameConfig& config,
const Building& b);
// True when the building's input buffer holds every ingredient the recipe needs.
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
// Total materials a shipyard needs for its schematic plus its placed modules
// (REQ-BLD-SHIPYARD), keyed by item id.
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
const Building& b);
// True when a production cycle could start right now, ignoring output-buffer space.
bool hasInputsToStart(const GameConfig& config, const Building& b);
// Status light for a building, or nullopt for types that show none — belts,
// splitters, tunnels, HQ and defence stations (REQ-UI-STATUS-LIGHT).
std::optional<ProductionStatus> getProductionStatus(const GameConfig& config,
const Building& building);

View File

@@ -1,10 +1,5 @@
#include "Simulation.h"
#include "FactoryQueries.h"
#include "ConstructionSystem.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h"
#include <algorithm>
#include <cassert>
#include <cmath>
@@ -52,7 +47,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
{
m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null;
m_factoryState = makeFactoryState(m_config);
initializeSubsystems();
@@ -100,7 +94,6 @@ void Simulation::reset(unsigned int seed)
m_pendingSchematicChoices.clear();
m_admin.clear();
m_factoryState = makeFactoryState(m_config);
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
initializeSubsystems();
@@ -125,9 +118,6 @@ void Simulation::initializeSubsystems()
},
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
m_rng);
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
m_deconstructionSystem = std::make_unique<DeconstructionSystem>(
m_config, [this](int amount) { m_buildingBlocksStock += amount; });
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>(m_config);
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
@@ -158,15 +148,15 @@ void Simulation::apply(const Command& command)
const BuildingId id = *placed;
if (c.recipeId.has_value())
{
m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId);
m_buildingSystem->setRecipe(id, *c.recipeId);
}
if (c.shipLayout.has_value())
{
m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout);
m_buildingSystem->setShipLayout(id, *c.shipLayout);
}
if (c.hasSplitterFilters)
{
m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB);
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB);
}
break;
}
@@ -179,26 +169,26 @@ void Simulation::apply(const Command& command)
case CommandKind::RotateInPlace:
{
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation);
m_buildingSystem->rotateInPlace(*c.id, c.newRotation);
break;
}
case CommandKind::SetRecipe:
{
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId);
m_buildingSystem->setRecipe(*c.id, c.recipeId);
break;
}
case CommandKind::SetShipLayout:
{
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout);
m_buildingSystem->setShipLayout(*c.id, c.layout);
break;
}
case CommandKind::SetSiteSplitterFilters:
{
const SetSiteSplitterFiltersCommand& c =
static_cast<const SetSiteSplitterFiltersCommand&>(command);
m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB);
m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB);
break;
}
case CommandKind::SetSplitterFilters:
@@ -248,12 +238,12 @@ void Simulation::tick()
m_waveSystem->tickThreatAccumulation();
// Construction + production pipeline
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction
m_buildingSystem->tickBeltPull(m_factoryState); // step 3
m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
m_buildingSystem->tickOutputBelts(m_factoryState); // step 5
m_buildingSystem->tickConstruction(m_currentTick);
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction
m_buildingSystem->tickBeltPull(); // step 3
m_buildingSystem->tickProduction(m_currentTick); // step 4
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
m_buildingSystem->tickOutputBelts(); // step 5
m_beltSystem.tick(); // step 6
// Step 7: ship behavior systems (movement arbitration via intent priority)
@@ -268,14 +258,15 @@ void Simulation::tick()
m_shipSystem->clearMovementIntents();
// Score-based behavior selection: evaluate, select winner, execute (sets
// movement intent + preferred module targets only — no world mutation).
m_aiSystem->tick(m_admin, m_factoryState);
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
// Module systems perform the world mutation (collection/delivery, healing).
// Each emits its tool beams and applies its own delayed (mid-beam) effects.
m_salvagerSystem->tick(m_currentTick, m_factoryState, m_beamFiredEvents);
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, m_beamFiredEvents);
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
// Step 8: combat resolution
m_combatSystem->tick(m_currentTick, m_admin, m_beamFiredEvents);
m_combatSystem->tick(m_currentTick, m_admin,
*m_buildingSystem, m_beamFiredEvents);
// Step 8b: deferred damage whose impact tick has arrived
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
@@ -311,7 +302,8 @@ void Simulation::placeInitialStructures()
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
const float hqHp =
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
m_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq,
m_hqBuildingId = m_buildingSystem->placeImmediate(
BuildingType::Hq,
m_config.stations.hq.surfaceMask,
QPoint(hqAnchorX, hqAnchorY),
Rotation::East);
@@ -360,7 +352,7 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation1Entity});
}
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
{
const QPoint anchor(psAnchorX, ps2Y);
@@ -377,7 +369,7 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation2Entity});
}
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
// Rally point: center of the player defence stations' X column, world vertical midpoint.
@@ -432,7 +424,7 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
}
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
{
const QPoint anchor(anchorX, y2);
@@ -449,7 +441,7 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
}
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
}
@@ -523,7 +515,7 @@ void Simulation::tickDeathsAndLoot()
{
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
}
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
{
std::vector<entt::entity> stationChildren;
m_admin.forEach<ModuleOwnerComponent>(
@@ -675,7 +667,7 @@ unsigned long long Simulation::computeStateChecksum() const
m_unlockState.appendChecksum(hasher);
// Subsystems contribute their own state.
m_buildingSystem->appendChecksum(m_factoryState, hasher);
m_buildingSystem->appendChecksum(hasher);
m_beltSystem.appendChecksum(hasher);
// ECS component state. View iteration order is a pure function of the
@@ -788,7 +780,7 @@ void Simulation::tryExpandAsteroid()
}
m_buildingBlocksStock -= cost;
++m_expansionsPurchased;
m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles());
m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles());
}
bool Simulation::isGameOver() const
@@ -818,12 +810,12 @@ double Simulation::getThreatAccumulationRate() const
double Simulation::getMaxFactoryProductionThreatRate() const
{
return static_cast<double>(getProductionBuildingCount(m_factoryState));
return static_cast<double>(m_buildingSystem->getProductionBuildingCount());
}
double Simulation::getCurrentFactoryProductionThreatRate() const
{
return static_cast<double>(getActiveProductionBuildingCount(m_factoryState));
return static_cast<double>(m_buildingSystem->getActiveProductionBuildingCount());
}
int Simulation::getBossWaveCounter() const
@@ -865,7 +857,7 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
return std::nullopt;
}
if (!isPlacementValid(m_factoryState, m_config, type, anchor, rotation))
if (!m_buildingSystem->isPlacementValid(type, anchor, rotation))
{
return std::nullopt;
}
@@ -884,17 +876,17 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
return std::nullopt;
}
m_buildingBlocksStock -= cost;
return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick);
return m_buildingSystem->place(type, anchor, rotation, m_currentTick);
}
void Simulation::deconstruct(BuildingId id)
{
m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick);
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick);
}
void Simulation::cancelDeconstruction(BuildingId id)
{
m_buildingSystem->cancelDeconstruction(m_factoryState, id);
m_buildingSystem->cancelDeconstruction(id);
}
BuildingSystem& Simulation::getBuildingsMutable()
@@ -902,11 +894,6 @@ BuildingSystem& Simulation::getBuildingsMutable()
return *m_buildingSystem;
}
const FactoryState& Simulation::getFactoryState() const
{
return m_factoryState;
}
const BuildingSystem& Simulation::getBuildings() const
{
return *m_buildingSystem;

View File

@@ -9,7 +9,6 @@
#include <QPoint>
#include "BeltSystem.h"
#include "FactoryState.h"
#include "EntityAdmin.h"
#include "entt/entity/entity.hpp"
#include "SchematicChoiceOption.h"
@@ -25,8 +24,6 @@
class AiSystem;
class BuildingSystem;
class ConstructionSystem;
class DeconstructionSystem;
struct Command;
class Hasher;
class CombatSystem;
@@ -121,9 +118,6 @@ public:
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
// mutate the factory outside the recorded command path (docs/replay_design.md).
const BuildingSystem& getBuildings() const;
// The factory's world data, for the free queries in FactoryQueries.h.
const FactoryState& getFactoryState() const;
const BeltSystem& getBelts() const;
ShipSystem& getShips();
const ShipSystem& getShips() const;
@@ -215,13 +209,8 @@ private:
UnlockState m_unlockState;
EntityAdmin m_admin;
// The factory's world data. Owned here, not by BuildingSystem, so the systems
// that operate on it can be handed the same state (see FactoryState.h).
FactoryState m_factoryState;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ConstructionSystem> m_constructionSystem;
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;

View File

@@ -1,5 +1,4 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <cmath>
#include <random>
@@ -15,8 +14,6 @@
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "ConstructionSystem.h"
#include "FactoryState.h"
#include "BuildingType.h"
#include "ConfigLoader.h"
#include "DeliverScrapBehavior.h"
@@ -57,14 +54,12 @@
struct Fixture
{
GameConfig cfg;
FactoryState state = makeFactoryState(cfg);
BeltSystem belts;
BuildingId nextBuildingId;
int stock;
std::mt19937 rng;
EntityAdmin admin;
BuildingSystem buildings;
ConstructionSystem construction;
ShipSystem ships;
AiSystem ai;
SalvagerSystem salvager;
@@ -87,7 +82,6 @@ struct Fixture
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
[](const std::string&) -> bool { return true; },
rng)
, construction(cfg)
, ships(cfg, admin)
, ai(cfg)
, salvager(admin)
@@ -101,14 +95,14 @@ struct Fixture
void decide()
{
ships.clearMovementIntents();
ai.tick(admin, state);
ai.tick(admin, buildings, scraps);
}
// World mutation: collection/delivery and healing.
void runModules()
{
beamEvents.clear();
salvager.tick(tick, state, beamEvents);
salvager.tick(tick, scraps, buildings, beamEvents);
repair.tick(tick, beamEvents);
}
@@ -141,7 +135,7 @@ struct Fixture
void salvageTick()
{
beamEvents.clear();
salvager.tick(tick, state, beamEvents);
salvager.tick(tick, scraps, buildings, beamEvents);
++tick;
}
@@ -955,18 +949,18 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
{
Fixture f;
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0).value();
Tick t = 0;
for (int i = 0; i < 500; ++i)
{
f.construction.tick(f.state, f.belts, t++);
if (findBuilding(f.state, bayId) != nullptr)
f.buildings.tickConstruction(t++);
if (f.buildings.findBuilding(bayId) != nullptr)
{
break;
}
}
REQUIRE(findBuilding(f.state, bayId) != nullptr);
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
@@ -990,15 +984,15 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
{
Fixture f;
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0).value();
Tick t = 0;
for (int i = 0; i < 500; ++i)
{
f.construction.tick(f.state, f.belts, t++);
if (findBuilding(f.state, bayId) != nullptr) { break; }
f.buildings.tickConstruction(t++);
if (f.buildings.findBuilding(bayId) != nullptr) { break; }
}
const Building* bay = findBuilding(f.state, bayId);
const Building* bay = f.buildings.findBuilding(bayId);
REQUIRE(bay != nullptr);
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
REQUIRE(bay->outputBuffer.capacity == 20);
@@ -1019,7 +1013,7 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
// One unit handed over from cargo into the bay's output buffer.
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
const Building* bayAfter = findBuilding(f.state, bayId);
const Building* bayAfter = f.buildings.findBuilding(bayId);
REQUIRE(bayAfter != nullptr);
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");

View File

@@ -1,5 +1,4 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
#include <climits>
@@ -530,9 +529,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
REQUIRE(idA != kInvalidBuildingId);
REQUIRE(idB != kInvalidBuildingId);
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetA)); // (-6, 0)
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), cursor)); // center not occupied
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(cursor)); // center not occupied
}
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
@@ -601,7 +600,7 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
REQUIRE_FALSE(id.has_value());
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
REQUIRE(getAllSites(sim.getFactoryState()).empty());
REQUIRE(sim.getBuildings().getAllSites().empty());
}
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
@@ -624,11 +623,11 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 0)));
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 0)));
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 1)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 0)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-2, 0)));
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 1)));
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 1)));
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(QPoint(-2, 1)));
}
// ---------------------------------------------------------------------------
@@ -666,9 +665,9 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
const ConstructionSite* site = sim.getBuildings().findSite(id);
REQUIRE(site != nullptr);
REQUIRE(site->recipeId == "mine_iron_ore");
}
@@ -680,7 +679,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();
REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore");
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
// Run 301 ticks (0..300) to process the completion tick.
@@ -689,7 +688,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
sim.tick();
}
const Building* b = findBuilding(sim.getFactoryState(), id);
const Building* b = sim.getBuildings().findBuilding(id);
REQUIRE(b != nullptr);
REQUIRE(b->recipeId == "mine_copper_ore");
}
@@ -709,8 +708,8 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
@@ -726,7 +725,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
@@ -743,16 +742,16 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
const BuildingId idA =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(idA != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore");
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(idA) != nullptr);
// Building B: place and configure, but leave as a construction site.
const BuildingId idB =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
REQUIRE(idB != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore");
REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
REQUIRE(sim.getBuildings().findSite(idB) != nullptr);
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
@@ -776,7 +775,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
}
@@ -854,9 +853,9 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
const ConstructionSite* site = sim.getBuildings().findSite(id);
REQUIRE(site != nullptr);
REQUIRE(site->shipLayout.has_value());
REQUIRE(site->shipLayout->placedModules.size() == 1);
@@ -878,7 +877,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
pm.rotation = Rotation::North;
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
// Shipyard construction_time_seconds = 30 in the test config.
double constructionTime = 0.0;
@@ -893,7 +892,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
sim.tick();
}
const Building* b = findBuilding(sim.getFactoryState(), id);
const Building* b = sim.getBuildings().findBuilding(id);
REQUIRE(b != nullptr);
REQUIRE(b->shipLayout.has_value());
REQUIRE(b->shipLayout->placedModules.size() == 1);

View File

@@ -1,5 +1,4 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
@@ -37,7 +36,8 @@ BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
{
const BuildingDef* def = findDef(cfg, type);
REQUIRE(def != nullptr);
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
return SimulationTestAccess::buildings(sim).placeImmediate(
type, def->surfaceMask, anchor, Rotation::East);
}
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
@@ -66,7 +66,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
Simulation sim(loadTestConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
@@ -102,8 +102,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
REQUIRE(schematic != nullptr);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{});
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id);
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{});
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
@@ -122,10 +122,10 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "FactoryState.h"
#include "BuildingType.h"
#include "CombatSystem.h"
#include "ConfigLoader.h"
@@ -52,7 +51,6 @@ static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
struct CombatFixture
{
GameConfig cfg;
FactoryState state = makeFactoryState(cfg);
std::mt19937 rng;
EntityAdmin admin;
BuildingId nextBuildingId;
@@ -112,7 +110,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
f.combat.applyPendingDamage(5, f.admin);
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
@@ -145,13 +143,13 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
};
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
REQUIRE_FALSE(enemyFiredIn(events));
f.combat.tick(1, f.admin, events);
f.combat.tick(1, f.admin, f.buildings, events);
REQUIRE_FALSE(enemyFiredIn(events));
f.combat.tick(2, f.admin, events);
f.combat.tick(2, f.admin, f.buildings, events);
REQUIRE(enemyFiredIn(events));
}
@@ -166,7 +164,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
f.wireEnemyTarget(enemy, player);
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
REQUIRE(events.empty());
}
@@ -309,7 +307,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
for (Tick t = 1; t < 5; ++t)
{
@@ -331,7 +329,7 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
f.combat.applyPendingDamage(5, f.admin);
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
@@ -348,7 +346,7 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
f.wireEnemyTarget(enemy, player);
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
f.ships.despawn(player);
@@ -371,7 +369,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<BeamFiredEvent> events;
f.combat.tick(0, f.admin, events);
f.combat.tick(0, f.admin, f.buildings, events);
f.ships.despawn(enemy);
@@ -415,7 +413,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
sim.tick();
const std::vector<DebrisInfo> scraps = getAllDebrisInfo(sim.getAdmin());
const std::vector<DebrisInfo> scraps = sim.getDebrisSystem().getAllDebrisInfo();
REQUIRE(scraps.size() == 1);
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
}

View File

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

View File

@@ -116,16 +116,16 @@ TEST_CASE("DebrisSystem: collectOne depletes one scrap and keeps the debris unti
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
REQUIRE(collectOne(admin, e));
REQUIRE(ss.collectOne(e));
REQUIRE(admin.isValid(e));
REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
REQUIRE(collectOne(admin, e));
REQUIRE(ss.collectOne(e));
REQUIRE(admin.isValid(e));
REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
// Final unit collected: the debris is removed once depleted.
REQUIRE(collectOne(admin, e));
REQUIRE(ss.collectOne(e));
REQUIRE_FALSE(admin.isValid(e));
}
@@ -134,7 +134,7 @@ TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debr
EntityAdmin admin;
DebrisSystem ss(admin);
REQUIRE_FALSE(collectOne(admin, entt::null));
REQUIRE_FALSE(ss.collectOne(entt::null));
}
// ---------------------------------------------------------------------------
@@ -149,7 +149,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo returns all spawned debris", "[debris]
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
REQUIRE(info.size() == 2);
}
@@ -161,7 +161,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo reports each debris entry.s remaining
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
REQUIRE(info.size() == 2);
for (const DebrisInfo& i : info)
{

View File

@@ -1,5 +1,4 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include "Building.h"
#include "BuildingSystem.h"
@@ -62,7 +61,8 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
{
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
return SimulationTestAccess::buildings(sim).placeImmediate(
BuildingType::Shipyard,
yardDef.surfaceMask,
QPoint(0, 0),
Rotation::East);
@@ -72,7 +72,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
const ShipDef& def,
const ShipLayoutConfig& layout)
{
SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b) {
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) {
if (b.id != yardId)
{
return;
@@ -207,7 +207,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
ShipLayoutConfig layout;
PlacedModule pm;
@@ -216,9 +216,9 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b = findBuilding(sim.getFactoryState(), yardId);
const Building* b = sim.getBuildings().findBuilding(yardId);
REQUIRE(b != nullptr);
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
@@ -236,14 +236,14 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
// Fill materials and tick to start production.
ShipLayoutConfig emptyLayout;
fillMaterials(sim, yardId, *def, emptyLayout);
sim.tick();
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->production.has_value());
@@ -255,9 +255,9 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->production.has_value());
}
@@ -278,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, "interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor");
// Deliberately no setShipLayout: recipe set, layout left unconfigured.
// Charge only the base-hull materials (an empty layout adds none).
@@ -313,7 +313,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
ShipLayoutConfig layout;
PlacedModule pm;
@@ -321,15 +321,15 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
pm.position = QPoint(0, 0);
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value());
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"destroyer");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->shipLayout.has_value());
}
@@ -342,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
ShipLayoutConfig layout;
PlacedModule pm;
@@ -350,16 +350,16 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
pm.position = QPoint(0, 0);
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
const Building* b1 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value());
// Re-selecting the same recipe must be a no-op and preserve the layout.
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
const Building* b2 = sim.getBuildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
REQUIRE(b2->shipLayout.has_value());
REQUIRE(b2->shipLayout->placedModules.size() == 1);

View File

@@ -1,5 +1,4 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
@@ -58,7 +57,8 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
{
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
return SimulationTestAccess::buildings(sim).placeImmediate(
BuildingType::Shipyard,
yardDef.surfaceMask,
QPoint(0, 0),
Rotation::East);
@@ -74,7 +74,7 @@ static int countShips(Simulation& sim)
static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def)
{
SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b)
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b)
{
if (b.id != yardId)
{
@@ -106,7 +106,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
const BuildingId yardId = placeShipyard(sim, *yardDef);
REQUIRE(yardId != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
fillMaterials(sim, yardId, *def);
// First tick: materials consumed, production cycle starts — no ship yet.
@@ -165,7 +165,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
const int shipsBefore = countShips(sim);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
// Materials remain at zero (default after setRecipe); no cycle starts.
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
@@ -187,7 +187,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
@@ -214,7 +214,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
// Verify the shipyard production field cleared (i.e. the cycle completed
// and is not still running).
bool productionCleared = false;
for (const Building& b : getAllBuildings(sim.getFactoryState()))
for (const Building& b : sim.getBuildings().getAllBuildings())
{
if (b.id == yardId)
{

View File

@@ -7,7 +7,6 @@
#include "BuildingId.h"
#include "BuildingType.h"
#include "Rotation.h"
#include "FactoryState.h"
#include "Simulation.h"
class BeltSystem;
@@ -26,7 +25,6 @@ class BuildingSystem;
struct SimulationTestAccess
{
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 std::optional<BuildingId> place(Simulation& sim, BuildingType type,

View File

@@ -1,5 +1,4 @@
#include "catch.hpp"
#include "FactoryQueries.h"
#include <random>
@@ -101,7 +100,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
// HQ is still a Building (for belt integration).
int hqCount = 0;
for (const Building& b : getAllBuildings(sim.getFactoryState()))
for (const Building& b : sim.getBuildings().getAllBuildings())
{
if (b.type == BuildingType::Hq) { ++hqCount; }
}
@@ -144,7 +143,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
{
const Simulation sim(loadTestConfig(), 42);
for (const Building& b : getAllBuildings(sim.getFactoryState()))
for (const Building& b : sim.getBuildings().getAllBuildings())
{
if (b.type != BuildingType::Hq) { continue; }
// Rightmost body cell must be at x = -1 (asteroid right edge).

View File

@@ -367,7 +367,7 @@ int FieldSelectionPanel::selectedDebrisScrapTotal() const
{
// Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
int total = 0;
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
{
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
!= m_selectedDebris.end())

View File

@@ -1,7 +1,4 @@
#include "GameWorldView.h"
#include "PlacementRules.h"
#include "FactoryQueries.h"
#include "ProductionRules.h"
#include <algorithm>
#include <cctype>
@@ -580,7 +577,7 @@ QRect GameWorldView::getViewportRect() const
float GameWorldView::getAsteroidLeftEdge() const
{
float leftX = -static_cast<float>(m_sim->getCurrentAsteroidWidth_tiles());
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
@@ -663,7 +660,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
// Terrain and world-bounds validity are owned by the simulation
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
// rotate-in-place check.
if (!isPlacementValid(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot))
if (!m_sim->getBuildings().isPlacementValid(type, anchor, rot))
{
return false;
}
@@ -675,7 +672,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
bool anyOccupied = false;
for (const QPoint& relCell : parsed.bodyCells)
{
if (isTileOccupied(m_sim->getFactoryState(), anchor + relCell))
if (m_sim->getBuildings().isTileOccupied(anchor + relCell))
{
anyOccupied = true;
break;
@@ -684,14 +681,14 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
if (anyOccupied)
{
return findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot).has_value();
return m_sim->getBuildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
}
return true;
}
std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
{
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
@@ -706,7 +703,7 @@ std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
std::optional<BuildingId> GameWorldView::siteAtTile(QPoint tile) const
{
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{
for (const QPoint& cell : s.bodyCells)
{
@@ -728,7 +725,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
const int y1 = std::max(cornerA.y(), cornerB.y());
std::vector<BuildingId> ids;
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
@@ -740,7 +737,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
}
}
}
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{
for (const QPoint& cell : s.bodyCells)
{
@@ -777,7 +774,7 @@ void GameWorldView::pruneDespawnedDebris()
if (m_selectedDebris.empty()) { return; }
std::vector<entt::entity> live;
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
{
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
!= m_selectedDebris.end())
@@ -866,7 +863,8 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
for (const BlueprintBuilding& bb : bp.buildings)
{
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
if (findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, center + bb.offset, bb.rotation).has_value())
if (m_sim->getBuildings().findRotateInPlaceTarget(
bb.type, center + bb.offset, bb.rotation).has_value())
{
continue;
}
@@ -880,7 +878,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
const QPoint anchor = center + bb.offset;
const std::optional<BuildingId> rotateTarget =
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, anchor, bb.rotation);
m_sim->getBuildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
if (rotateTarget.has_value())
{
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
@@ -953,14 +951,14 @@ TunnelTileMap GameWorldView::collectTunnelTiles() const
// single-cell tile, so a just-placed tunnel (not yet constructed) is matchable
// (REQ-BLD-TUNNEL-MODE, REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
TunnelTileMap tunnels;
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
if (b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
{
tunnels[b.anchor] = TunnelTileInfo{b.type, b.rotation};
}
}
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{
if (s.type == BuildingType::TunnelEntry || s.type == BuildingType::TunnelExit)
{
@@ -1016,7 +1014,7 @@ void GameWorldView::placeAtTile(QPoint tile)
}
const std::optional<BuildingId> rotateTarget =
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, m_ghostRotation);
m_sim->getBuildings().findRotateInPlaceTarget(type, tile, m_ghostRotation);
if (rotateTarget.has_value())
{
std::shared_ptr<RotateInPlaceCommand> command =
@@ -1036,7 +1034,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit)
{
if (!isTileOccupied(m_sim->getFactoryState(), tile) && canAfford(type))
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
{
enqueuePlaceBuilding(type, tile, m_ghostRotation);
}
@@ -1063,7 +1061,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
std::optional<BuildingType> targetType;
if (targetId.has_value())
{
if (const Building* building = findBuilding(m_sim->getFactoryState(), *targetId))
if (const Building* building = m_sim->getBuildings().findBuilding(*targetId))
{
targetType = building->type;
}
@@ -1071,7 +1069,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
else if (std::optional<BuildingId> siteId = siteAtTile(cursorTile); siteId.has_value())
{
targetId = siteId;
if (const ConstructionSite* site = findSite(m_sim->getFactoryState(), *siteId))
if (const ConstructionSite* site = m_sim->getBuildings().findSite(*siteId))
{
targetType = site->type;
}
@@ -1080,7 +1078,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
if (targetId.has_value() && targetType.has_value()
&& *targetType != BuildingType::Belt)
{
const std::vector<Port> inputPorts = getInputPorts(m_sim->getFactoryState(), m_sim->getConfig(), *targetId);
const std::vector<Port> inputPorts = m_sim->getBuildings().getInputPorts(*targetId);
std::optional<Port> best;
float bestDistanceSq = 0.0f;
for (const Port& port : inputPorts)
@@ -1124,7 +1122,8 @@ std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath(
{
BeltDragResolved item;
const std::optional<BuildingId> rotateTarget =
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), BuildingType::Belt, entry.tile, entry.rotation);
m_sim->getBuildings().findRotateInPlaceTarget(
BuildingType::Belt, entry.tile, entry.rotation);
if (rotateTarget.has_value())
{
// A tile holding only a belt (or belt site) is re-oriented, no cost.
@@ -1299,7 +1298,7 @@ bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
void GameWorldView::drawBuildings(QPainter& painter)
{
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(b.type);
@@ -1341,7 +1340,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
// building footprints are rectangular, so a corner of the axis-aligned
// bounding box is the true corner.
if (const std::optional<ProductionStatus> status =
getProductionStatus(m_sim->getConfig(), b))
m_sim->getBuildings().getProductionStatus(b))
{
const float px = getTilePx();
const float r = px * 0.18f;
@@ -1366,7 +1365,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
}
painter.setOpacity(0.5);
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(s.type);
@@ -1443,7 +1442,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
// after every building and construction site fill so a belt (or other tile)
// placed directly below the HQ cannot overpaint the bar (REQ-UI-STATUS-LIGHT
// neighbours case, same rationale as the selection highlights below).
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
if (b.type != BuildingType::Hq) { continue; }
const QPointF tl = tileToWidget(b.anchor);
@@ -1474,12 +1473,12 @@ std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
std::optional<QPoint> anchor;
std::optional<QSize> footprint;
if (const Building* b = findBuilding(m_sim->getFactoryState(), id))
if (const Building* b = m_sim->getBuildings().findBuilding(id))
{
anchor = b->anchor;
footprint = b->footprint;
}
else if (const ConstructionSite* s = findSite(m_sim->getFactoryState(), id))
else if (const ConstructionSite* s = m_sim->getBuildings().findSite(id))
{
anchor = s->anchor;
footprint = s->footprint;
@@ -1510,7 +1509,7 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
if (!m_selectedDebris.empty())
{
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
== m_selectedDebris.end()) { continue; }
@@ -1531,13 +1530,13 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
painter.setPen(Qt::NoPen);
painter.setBrush(color);
const BuildingType type = m_copiedConfig->type;
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
if (b.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(b.id);
if (rect.has_value()) { painter.drawRect(*rect); }
}
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{
if (s.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(s.id);
@@ -1572,7 +1571,7 @@ void GameWorldView::drawPortItems(QPainter& painter)
const double margin = kPortMarginTiles * static_cast<double>(getTilePx());
QRegion clip(rect());
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
if (b.type == BuildingType::Belt || b.type == BuildingType::Splitter
|| b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
@@ -1605,8 +1604,8 @@ void GameWorldView::drawPortItems(QPainter& painter)
painter.save();
painter.setClipRegion(clip);
m_sim->getBuildings().forEachEmergingItem(m_sim->getFactoryState(), drawItem);
m_sim->getBuildings().forEachIncomingItem(m_sim->getFactoryState(), drawItem);
m_sim->getBuildings().forEachEmergingItem(drawItem);
m_sim->getBuildings().forEachIncomingItem(drawItem);
painter.restore();
}
@@ -1654,7 +1653,7 @@ void GameWorldView::drawBeltItems(QPainter& painter)
void GameWorldView::drawDebris(QPainter& painter)
{
const float r = getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{
const QPointF center = worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90));
@@ -1983,11 +1982,11 @@ void GameWorldView::drawSelectedTunnelConnections(QPainter& painter)
std::optional<QPoint> anchor;
std::optional<BuildingType> type;
Rotation rotation = Rotation::East;
if (const Building* b = findBuilding(m_sim->getFactoryState(), id))
if (const Building* b = m_sim->getBuildings().findBuilding(id))
{
anchor = b->anchor; type = b->type; rotation = b->rotation;
}
else if (const ConstructionSite* s = findSite(m_sim->getFactoryState(), id))
else if (const ConstructionSite* s = m_sim->getBuildings().findSite(id))
{
anchor = s->anchor; type = s->type; rotation = s->rotation;
}
@@ -2092,7 +2091,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
// Queued for deconstruction: tint every building currently in the
// deconstruction queue, regardless of mode (REQ-BLD-DECON-QUEUE).
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
if (!b.queuedForDeconstruction) { continue; }
for (const QPoint& cell : b.bodyCells)
@@ -2107,12 +2106,12 @@ void GameWorldView::drawOverlays(QPainter& painter)
{
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
{
const Building* b = findBuilding(m_sim->getFactoryState(), id);
const Building* b = m_sim->getBuildings().findBuilding(id);
if (b && b->type == BuildingType::Hq) { continue; }
const std::vector<QPoint>* cells = nullptr;
const ConstructionSite* s = nullptr;
if (b) { cells = &b->bodyCells; }
else if ((s = findSite(m_sim->getFactoryState(), id))) { cells = &s->bodyCells; }
else if ((s = m_sim->getBuildings().findSite(id))) { cells = &s->bodyCells; }
if (cells)
{
for (const QPoint& cell : *cells)
@@ -2124,7 +2123,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
}
else if (m_deconstructMode && m_deconstructHoverBuildingId.has_value())
{
const Building* b = findBuilding(m_sim->getFactoryState(), *m_deconstructHoverBuildingId);
const Building* b = m_sim->getBuildings().findBuilding(*m_deconstructHoverBuildingId);
if (b)
{
for (const QPoint& cell : b->bodyCells)
@@ -2749,7 +2748,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
if (m_deconstructMode)
{
const FactoryState& factory = m_sim->getFactoryState();
const BuildingSystem& buildings = m_sim->getBuildings();
// Split covered ids into construction sites (removed instantly) and
// operational deconstructible buildings (the HQ is protected; player
@@ -2758,12 +2757,12 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
std::vector<BuildingId> operational;
for (BuildingId id : boxIds)
{
if (const Building* b = findBuilding(factory, id))
if (const Building* b = buildings.findBuilding(id))
{
if (b->type == BuildingType::Hq) { continue; }
operational.push_back(id);
}
else if (findSite(factory, id))
else if (buildings.findSite(id))
{
sites.push_back(id);
}
@@ -2785,7 +2784,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
bool allQueued = !operational.empty();
for (BuildingId id : operational)
{
if (!isQueuedForDeconstruction(factory, id)) { allQueued = false; break; }
if (!buildings.isQueuedForDeconstruction(id)) { allQueued = false; break; }
}
for (BuildingId id : operational)
{
@@ -2796,7 +2795,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
command->id = id;
enqueueCommand(command);
}
else if (!isQueuedForDeconstruction(factory, id))
else if (!buildings.isQueuedForDeconstruction(id))
{
std::shared_ptr<DeconstructCommand> command =
std::make_shared<DeconstructCommand>();
@@ -2998,7 +2997,7 @@ void GameWorldView::pasteConfigTo(BuildingId id)
{
// Operational splitters are configured by tile; sites by BuildingId
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
if (const Building* building = findBuilding(m_sim->getFactoryState(), id))
if (const Building* building = m_sim->getBuildings().findBuilding(id))
{
std::shared_ptr<SetSplitterFiltersCommand> command =
std::make_shared<SetSplitterFiltersCommand>();

View File

@@ -1,5 +1,4 @@
#include "MainWindow.h"
#include "FactoryQueries.h"
#include <map>
#include <random>
@@ -278,9 +277,9 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
{
// A construction site has no Building yet; fall back to its site record so
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
const Building* b = findBuilding(m_sim->getFactoryState(), event->shipyardId);
const Building* b = m_sim->getBuildings().findBuilding(event->shipyardId);
const ConstructionSite* s =
b ? nullptr : findSite(m_sim->getFactoryState(), event->shipyardId);
b ? nullptr : m_sim->getBuildings().findSite(event->shipyardId);
if (!b && !s)
{
return;
@@ -305,9 +304,9 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
// A construction site has no Building yet; fall back to its site record so
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
const Building* b = findBuilding(m_sim->getFactoryState(), event->buildingId);
const Building* b = m_sim->getBuildings().findBuilding(event->buildingId);
const ConstructionSite* s =
b ? nullptr : findSite(m_sim->getFactoryState(), event->buildingId);
b ? nullptr : m_sim->getBuildings().findSite(event->buildingId);
if (!b && !s)
{
return;

View File

@@ -1,5 +1,4 @@
#include "SelectedBuildingPanel.h"
#include "FactoryQueries.h"
#include <algorithm>
#include <cctype>
@@ -235,8 +234,8 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
m_singleBuildingId = id;
hideAllWidgets();
const Building* b = findBuilding(m_sim->getFactoryState(), id);
const ConstructionSite* s = b ? nullptr : findSite(m_sim->getFactoryState(), id);
const Building* b = m_sim->getBuildings().findBuilding(id);
const ConstructionSite* s = b ? nullptr : m_sim->getBuildings().findSite(id);
if (!b && !s)
{
buildEmpty();
@@ -314,7 +313,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
std::optional<BeltSystem::SplitterInfo> info;
if (m_singleIsSite)
{
info = getSiteSplitterInfo(m_sim->getFactoryState(), m_sim->getConfig(), id);
info = m_sim->getBuildings().getSiteSplitterInfo(id);
}
else
{
@@ -603,7 +602,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
// category owns the panel there is none: yieldToFieldSelection() has cleared it, so
// this returns immediately and the field panel refreshes itself off the same events.
if (!m_singleBuildingId.has_value()) { return; }
const Building* b = findBuilding(m_sim->getFactoryState(), *m_singleBuildingId);
const Building* b = m_sim->getBuildings().findBuilding(*m_singleBuildingId);
if (b)
{
if (m_titleLabel->text().startsWith(tr("(Building) ")))
@@ -616,7 +615,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
}
return;
}
const ConstructionSite* s = findSite(m_sim->getFactoryState(), *m_singleBuildingId);
const ConstructionSite* s = m_sim->getBuildings().findSite(*m_singleBuildingId);
if (s)
{
// A periodic tick only advances construction progress, so update just the
@@ -650,13 +649,13 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
std::map<BuildingType, int> counts;
for (BuildingId id : ids)
{
const Building* b = findBuilding(m_sim->getFactoryState(), id);
const Building* b = m_sim->getBuildings().findBuilding(id);
if (b)
{
counts[b->type]++;
continue;
}
const ConstructionSite* s = findSite(m_sim->getFactoryState(), id);
const ConstructionSite* s = m_sim->getBuildings().findSite(id);
if (s)
{
counts[s->type]++;
@@ -818,7 +817,7 @@ void SelectedBuildingPanel::onClearBelt()
std::vector<QPoint> tiles;
for (BuildingId id : m_selectedBuildingIds)
{
const Building* b = findBuilding(m_sim->getFactoryState(), id);
const Building* b = m_sim->getBuildings().findBuilding(id);
if (b && isBeltSubsystemType(b->type))
{
for (const QPoint& cell : b->bodyCells)