number the buildings from the factory that holds them

The building-id counter was the last piece of factory data living on Simulation
behind a callback: every construction site, every building, and every tile a
station entity claims took its id from a std::function BuildingSystem held, which
the arena and three test fixtures each had to supply.

It moves into FactoryState as nextBuildingId, handed out by
allocateBuildingId(state) in FactoryQueries beside the other operations over the
state. BuildingSystem's callback is gone; so are Simulation::allocateBuildingId
and ArenaSimulation::allocateBuildingId, whose remaining callers now allocate
from the state directly.

Checksum order is untouched: Simulation folds the counter where it always did.

What is left on BuildingSystem is the config, the belts, the RNG, and two
callbacks that reach genuinely outside the factory -- spawning a finished ship
into the entity model, and testing an output group against the unlock state.
Neither is factory data, so this is where the migration stops.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-19 17:31:48 +02:00
parent 780d5e5052
commit e7bfd91054
13 changed files with 43 additions and 48 deletions

View File

@@ -273,12 +273,12 @@ building could run and what its status light shows. A caller therefore depends o
it reads rather than on whichever system happens to tick it — which is what let the UI, the it reads rather than on whichever system happens to tick it — which is what let the UI, the
balancing tool and the tests stop reaching through `BuildingSystem` for const answers. balancing tool and the tests stop reaching through `BuildingSystem` for const answers.
What has not moved yet: the building-id counter and the global building block stock are The struct also carries the two counters that used to live on `Simulation` and be reached
factory data that still live on `Simulation`, reached through callbacks through callbacks: `nextBuildingId` (handed out by `allocateBuildingId`) and
(`m_allocateBuildingId`, `m_addBuildingBlocks`) held by `BuildingSystem` and `buildingBlocksStock`, which placement spends, deconstruction refunds, and HQ belt
`DeconstructionSystem`. `m_spawnShip` and `m_isItemUnlocked` are genuine cross-domain deliveries add to. What remains on `BuildingSystem` is `m_spawnShip` and
reaches into the entity model and the unlock state — and are not candidates for this `m_isItemUnlocked` — genuine cross-domain reaches into the entity model and the unlock
struct. state, not factory data, and so not candidates for this struct.
## Debris ## Debris

View File

@@ -39,7 +39,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
, m_arenaConfig(std::move(arenaConfig)) , m_arenaConfig(std::move(arenaConfig))
, m_rng(seed) , m_rng(seed)
, m_currentTick(0) , m_currentTick(0)
, m_nextBuildingId(1)
, m_beltSystem(1.0) , m_beltSystem(1.0)
, m_team1HqEntity(entt::null) , m_team1HqEntity(entt::null)
, m_team2HqEntity(entt::null) , m_team2HqEntity(entt::null)
@@ -51,7 +50,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
m_buildingSystem = std::make_unique<BuildingSystem>( m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig, m_gameConfig,
m_beltSystem, m_beltSystem,
[this]() { return allocateBuildingId(); },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
m_rng); m_rng);
@@ -133,10 +131,6 @@ void ArenaSimulation::computeTeamMaxEhp()
ArenaSimulation::~ArenaSimulation() = default; ArenaSimulation::~ArenaSimulation() = default;
BuildingId ArenaSimulation::allocateBuildingId()
{
return m_nextBuildingId++;
}
void ArenaSimulation::placeStructures() void ArenaSimulation::placeStructures()
{ {
@@ -163,7 +157,8 @@ void ArenaSimulation::placeStructures()
hp, hp, false); hp, hp, false);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR). // Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity); m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
} }
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon. // Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
@@ -184,7 +179,8 @@ void ArenaSimulation::placeStructures()
hp, hp, true); hp, hp, true);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR). // Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity); m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
} }
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy) auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
@@ -238,7 +234,8 @@ void ArenaSimulation::placeStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{stationEntity}); ModuleOwnerComponent{stationEntity});
} }
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
}; };
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations) for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)

View File

@@ -92,7 +92,6 @@ public:
const EntityAdmin& getAdmin() const; const EntityAdmin& getAdmin() const;
private: private:
BuildingId allocateBuildingId();
void placeStructures(); void placeStructures();
void spawnShips(); void spawnShips();
void computeTeamMaxEhp(); void computeTeamMaxEhp();
@@ -105,7 +104,6 @@ private:
std::mt19937 m_rng; std::mt19937 m_rng;
Tick m_currentTick; Tick m_currentTick;
BuildingId m_nextBuildingId;
EntityAdmin m_admin; EntityAdmin m_admin;
FactoryState m_factoryState; FactoryState m_factoryState;

View File

@@ -27,14 +27,12 @@ bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
BuildingSystem::BuildingSystem(const GameConfig& config, BuildingSystem::BuildingSystem(const GameConfig& config,
BeltSystem& belts, BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId,
std::function<void(const std::string&, QVector2D, std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> spawnShip, const std::optional<ShipLayoutConfig>&)> spawnShip,
std::function<bool(const std::string&)> isItemUnlocked, std::function<bool(const std::string&)> isItemUnlocked,
std::mt19937& rng) std::mt19937& rng)
: m_config(config) : m_config(config)
, m_belts(belts) , m_belts(belts)
, m_allocateBuildingId(std::move(allocateBuildingId))
, m_spawnShip(std::move(spawnShip)) , m_spawnShip(std::move(spawnShip))
, m_isItemUnlocked(std::move(isItemUnlocked)) , m_isItemUnlocked(std::move(isItemUnlocked))
, m_rng(rng) , m_rng(rng)
@@ -121,7 +119,7 @@ std::optional<BuildingId> BuildingSystem::place(FactoryState& state, BuildingTyp
return std::nullopt; return std::nullopt;
} }
const BuildingId id = m_allocateBuildingId(); const BuildingId id = allocateBuildingId(state);
// Record tile occupancy for body cells. // Record tile occupancy for body cells.
for (const QPoint& cell : mask.bodyCells) for (const QPoint& cell : mask.bodyCells)
@@ -839,7 +837,7 @@ BuildingId BuildingSystem::placeImmediate(FactoryState& state, BuildingType type
const std::vector<std::string>& surfaceMask, const std::vector<std::string>& surfaceMask,
QPoint anchor, Rotation rotation) QPoint anchor, Rotation rotation)
{ {
const BuildingId id = m_allocateBuildingId(); const BuildingId id = allocateBuildingId(state);
const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation); const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation);
Building building; Building building;

View File

@@ -40,7 +40,6 @@ class BuildingSystem
public: public:
BuildingSystem(const GameConfig& config, BuildingSystem(const GameConfig& config,
BeltSystem& belts, BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId,
std::function<void(const std::string&, QVector2D, std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> spawnShip, const std::optional<ShipLayoutConfig>&)> spawnShip,
std::function<bool(const std::string&)> isItemUnlocked, std::function<bool(const std::string&)> isItemUnlocked,
@@ -173,12 +172,12 @@ private:
std::vector<Item> rollOutputGroup(const RecipeDef& recipe); std::vector<Item> rollOutputGroup(const RecipeDef& recipe);
// No world data among these: the factory arrives per call (FactoryState.h). What is // No world data among these: the factory arrives per call (FactoryState.h). What is
// left are the immutable config, the transport layer, the shared RNG, and four // left are the immutable config, the transport layer, the shared RNG, and two
// callbacks into what this system cannot reach on its own -- the id counter and the // callbacks into what this system genuinely cannot reach -- the entity model a
// block stock, which live on Simulation, and the entity and unlock sides. // finished ship is spawned into, and the unlock state an output group is tested
// against. Neither is factory data, so neither belongs in the state.
const GameConfig& m_config; const GameConfig& m_config;
BeltSystem& m_belts; BeltSystem& m_belts;
std::function<BuildingId()> m_allocateBuildingId;
std::function<void(const std::string&, QVector2D, std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> m_spawnShip; const std::optional<ShipLayoutConfig>&)> m_spawnShip;
std::function<bool(const std::string&)> m_isItemUnlocked; std::function<bool(const std::string&)> m_isItemUnlocked;

View File

@@ -235,6 +235,11 @@ TunnelTileMap collectTunnelTiles(const FactoryState& state)
return tunnels; return tunnels;
} }
BuildingId allocateBuildingId(FactoryState& state)
{
return state.nextBuildingId++;
}
void forEachEmergingItem(const FactoryState& state, void forEachEmergingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const std::function<void(const ItemType&, QPointF)>& visit)
{ {

View File

@@ -85,6 +85,11 @@ std::vector<BuildingId> buildingsInBox(const FactoryState& state,
// single-cell tile. Shared by the placement preview and the selection highlight. // single-cell tile. Shared by the placement preview and the selection highlight.
TunnelTileMap collectTunnelTiles(const FactoryState& state); TunnelTileMap collectTunnelTiles(const FactoryState& state);
// Hands out the next building id and advances the counter (BuildingId.h). A mutation
// rather than a query, like deliverScrapToSalvageBay above: it is one line over the state
// and belongs to no system, every id-issuing path being a factory path.
BuildingId allocateBuildingId(FactoryState& state);
// Visits every item currently emerging from a building output port on its virtual output // 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 // 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 // units). Least-progressed first (drawn bottom) so callers can paint in visit order

View File

@@ -58,6 +58,12 @@ struct FactoryState
// Seeded from config by BuildingSystem's constructor. // Seeded from config by BuildingSystem's constructor.
int asteroidWidth_tiles = 0; int asteroidWidth_tiles = 0;
// Next id to hand out for a building, a construction site, or a tile claimed by a
// station entity. Strictly increasing and never reused, so an id names one thing for
// the whole run -- which is what lets a command reference a building across a replay
// (docs/replay_design.md). Allocated through allocateBuildingId (FactoryQueries.h).
BuildingId nextBuildingId = 1;
// The global building block stock (REQ-HQ-STARTING-BLOCKS, REQ-HQ-BELT-INPUT): // The global building block stock (REQ-HQ-STARTING-BLOCKS, REQ-HQ-BELT-INPUT):
// what placement spends, what deconstruction refunds, and what blocks delivered to // what placement spends, what deconstruction refunds, and what blocks delivered to
// the HQ add to. Factory data, so it lives with the factory rather than being // the HQ add to. Factory data, so it lives with the factory rather than being

View File

@@ -41,7 +41,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
, m_seed(seed) , m_seed(seed)
, m_currentTick(0) , m_currentTick(0)
, m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds)) , m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds))
, m_nextBuildingId(1)
, m_gameOver(false) , m_gameOver(false)
, m_hqProxyEntity(entt::null) , m_hqProxyEntity(entt::null)
, m_playerStation1Entity(entt::null) , m_playerStation1Entity(entt::null)
@@ -83,7 +82,6 @@ void Simulation::reset(unsigned int seed)
m_seed = seed; m_seed = seed;
m_currentTick = 0; m_currentTick = 0;
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds); m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
m_nextBuildingId = 1;
m_expansionsPurchased = 0; m_expansionsPurchased = 0;
m_gameOver = false; m_gameOver = false;
m_isWon = false; m_isWon = false;
@@ -111,7 +109,6 @@ void Simulation::initializeSubsystems()
m_buildingSystem = std::make_unique<BuildingSystem>( m_buildingSystem = std::make_unique<BuildingSystem>(
m_config, m_config,
m_beltSystem, m_beltSystem,
[this]() { return allocateBuildingId(); },
[this](const std::string& id, QVector2D pos, [this](const std::string& id, QVector2D pos,
const std::optional<ShipLayoutConfig>& layout) { const std::optional<ShipLayoutConfig>& layout) {
if (!isSchematicUnlocked(id)) if (!isSchematicUnlocked(id))
@@ -356,7 +353,8 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation1Entity}); ModuleOwnerComponent{m_playerStation1Entity});
} }
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
} }
{ {
const QPoint anchor(psAnchorX, ps2Y); const QPoint anchor(psAnchorX, ps2Y);
@@ -373,7 +371,8 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation2Entity}); ModuleOwnerComponent{m_playerStation2Entity});
} }
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
} }
// Rally point: center of the player defence stations' X column, world vertical midpoint. // Rally point: center of the player defence stations' X column, world vertical midpoint.
@@ -428,7 +427,8 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_currentEnemyStationEntities[0]}); ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
} }
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
} }
{ {
const QPoint anchor(anchorX, y2); const QPoint anchor(anchorX, y2);
@@ -445,7 +445,8 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild, m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_currentEnemyStationEntities[1]}); ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
} }
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
} }
} }
@@ -653,7 +654,7 @@ unsigned long long Simulation::computeStateChecksum() const
// Top-level scalars. // Top-level scalars.
hasher.append(m_currentTick); hasher.append(m_currentTick);
hasher.append(m_nextDepartureTick); hasher.append(m_nextDepartureTick);
hasher.append(m_nextBuildingId); hasher.append(m_factoryState.nextBuildingId);
hasher.append(m_factoryState.buildingBlocksStock); hasher.append(m_factoryState.buildingBlocksStock);
hasher.append(m_gameOver); hasher.append(m_gameOver);
hasher.append(m_isWon); hasher.append(m_isWon);
@@ -948,7 +949,3 @@ void Simulation::handleEvent(std::shared_ptr<const TracePrintRequestedEvent> eve
PRINT_TRACES(); PRINT_TRACES();
} }
BuildingId Simulation::allocateBuildingId()
{
return m_nextBuildingId++;
}

View File

@@ -169,7 +169,6 @@ private:
void handleEvent(std::shared_ptr<const TracePrintRequestedEvent> event) override; void handleEvent(std::shared_ptr<const TracePrintRequestedEvent> event) override;
BuildingId allocateBuildingId(); // Strictly increasing; never returns kInvalidBuildingId.
// (Re-)create every owned subsystem. Shared by the constructor and reset(); // (Re-)create every owned subsystem. Shared by the constructor and reset();
// the construction order is load-bearing for determinism, so both paths must // the construction order is load-bearing for determinism, so both paths must
@@ -195,7 +194,6 @@ private:
Tick m_currentTick; Tick m_currentTick;
Tick m_nextDepartureTick; Tick m_nextDepartureTick;
BuildingId m_nextBuildingId;
int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x
bool m_gameOver = false; bool m_gameOver = false;
bool m_isWon = false; bool m_isWon = false;

View File

@@ -59,7 +59,6 @@ struct Fixture
GameConfig cfg; GameConfig cfg;
FactoryState state = makeFactoryState(cfg); FactoryState state = makeFactoryState(cfg);
BeltSystem belts; BeltSystem belts;
BuildingId nextBuildingId;
std::mt19937 rng; std::mt19937 rng;
EntityAdmin admin; EntityAdmin admin;
BuildingSystem buildings; BuildingSystem buildings;
@@ -77,10 +76,8 @@ struct Fixture
explicit Fixture() explicit Fixture()
: cfg(loadTestConfig()) : cfg(loadTestConfig())
, belts(cfg.world.beltSpeed_tps) , belts(cfg.world.beltSpeed_tps)
, nextBuildingId(1)
, rng(42) , rng(42)
, buildings(cfg, belts, , buildings(cfg, belts,
[this]() { return nextBuildingId++; },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng) rng)

View File

@@ -99,7 +99,6 @@ struct PlacementFixture
FactoryState state = makeFactoryState(cfg); FactoryState state = makeFactoryState(cfg);
BeltSystem belts; BeltSystem belts;
std::mt19937 rng{0}; std::mt19937 rng{0};
BuildingId nextBuildingId = 1;
BuildingSystem bs; BuildingSystem bs;
// Blocks credited back since the run began. The state is seeded with the configured // Blocks credited back since the run began. The state is seeded with the configured
@@ -118,7 +117,6 @@ struct PlacementFixture
std::function<bool(const std::string&)> isItemUnlocked = nullptr) std::function<bool(const std::string&)> isItemUnlocked = nullptr)
: belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps)) : belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps))
, bs(cfg, belts, , bs(cfg, belts,
[this]() { return nextBuildingId++; },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
isItemUnlocked ? std::move(isItemUnlocked) isItemUnlocked ? std::move(isItemUnlocked)
: std::function<bool(const std::string&)>( : std::function<bool(const std::string&)>(

View File

@@ -55,7 +55,6 @@ struct CombatFixture
FactoryState state = makeFactoryState(cfg); FactoryState state = makeFactoryState(cfg);
std::mt19937 rng; std::mt19937 rng;
EntityAdmin admin; EntityAdmin admin;
BuildingId nextBuildingId;
BeltSystem belts; BeltSystem belts;
ShipSystem ships; ShipSystem ships;
BuildingSystem buildings; BuildingSystem buildings;
@@ -64,11 +63,9 @@ struct CombatFixture
explicit CombatFixture() explicit CombatFixture()
: cfg(loadTestConfig()) : cfg(loadTestConfig())
, rng(42) , rng(42)
, nextBuildingId(1)
, belts(cfg.world.beltSpeed_tps) , belts(cfg.world.beltSpeed_tps)
, ships(cfg, admin) , ships(cfg, admin)
, buildings(cfg, belts, , buildings(cfg, belts,
[this]() { return nextBuildingId++; },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng) rng)