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
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
factory data that still live on `Simulation`, reached through callbacks
(`m_allocateBuildingId`, `m_addBuildingBlocks`) held by `BuildingSystem` and
`DeconstructionSystem`. `m_spawnShip` and `m_isItemUnlocked` are genuine cross-domain
reaches into the entity model and the unlock state — and are not candidates for this
struct.
The struct also carries the two counters that used to live on `Simulation` and be reached
through callbacks: `nextBuildingId` (handed out by `allocateBuildingId`) and
`buildingBlocksStock`, which placement spends, deconstruction refunds, and HQ belt
deliveries add to. What remains on `BuildingSystem` is `m_spawnShip` and
`m_isItemUnlocked` — genuine cross-domain reaches into the entity model and the unlock
state, not factory data, and so not candidates for this struct.
## Debris

View File

@@ -39,7 +39,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
, m_arenaConfig(std::move(arenaConfig))
, m_rng(seed)
, m_currentTick(0)
, m_nextBuildingId(1)
, m_beltSystem(1.0)
, m_team1HqEntity(entt::null)
, m_team2HqEntity(entt::null)
@@ -51,7 +50,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig,
m_beltSystem,
[this]() { return allocateBuildingId(); },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
[](const std::string&) -> bool { return true; },
m_rng);
@@ -133,10 +131,6 @@ void ArenaSimulation::computeTeamMaxEhp()
ArenaSimulation::~ArenaSimulation() = default;
BuildingId ArenaSimulation::allocateBuildingId()
{
return m_nextBuildingId++;
}
void ArenaSimulation::placeStructures()
{
@@ -163,7 +157,8 @@ 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(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
}
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
@@ -184,7 +179,8 @@ 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(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
}
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
@@ -238,7 +234,8 @@ void ArenaSimulation::placeStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild,
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)

View File

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

View File

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

View File

@@ -40,7 +40,6 @@ class BuildingSystem
public:
BuildingSystem(const GameConfig& config,
BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId,
std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> spawnShip,
std::function<bool(const std::string&)> isItemUnlocked,
@@ -173,12 +172,12 @@ private:
std::vector<Item> rollOutputGroup(const RecipeDef& recipe);
// 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
// callbacks into what this system cannot reach on its own -- the id counter and the
// block stock, which live on Simulation, and the entity and unlock sides.
// left are the immutable config, the transport layer, the shared RNG, and two
// callbacks into what this system genuinely cannot reach -- the entity model a
// 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;
BeltSystem& m_belts;
std::function<BuildingId()> m_allocateBuildingId;
std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
std::function<bool(const std::string&)> m_isItemUnlocked;

View File

@@ -235,6 +235,11 @@ TunnelTileMap collectTunnelTiles(const FactoryState& state)
return tunnels;
}
BuildingId allocateBuildingId(FactoryState& state)
{
return state.nextBuildingId++;
}
void forEachEmergingItem(const FactoryState& state,
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.
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
// 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

View File

@@ -58,6 +58,12 @@ struct FactoryState
// Seeded from config by BuildingSystem's constructor.
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):
// 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

View File

@@ -41,7 +41,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
, m_seed(seed)
, m_currentTick(0)
, m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds))
, m_nextBuildingId(1)
, m_gameOver(false)
, m_hqProxyEntity(entt::null)
, m_playerStation1Entity(entt::null)
@@ -83,7 +82,6 @@ void Simulation::reset(unsigned int seed)
m_seed = seed;
m_currentTick = 0;
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
m_nextBuildingId = 1;
m_expansionsPurchased = 0;
m_gameOver = false;
m_isWon = false;
@@ -111,7 +109,6 @@ void Simulation::initializeSubsystems()
m_buildingSystem = std::make_unique<BuildingSystem>(
m_config,
m_beltSystem,
[this]() { return allocateBuildingId(); },
[this](const std::string& id, QVector2D pos,
const std::optional<ShipLayoutConfig>& layout) {
if (!isSchematicUnlocked(id))
@@ -356,7 +353,8 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{m_playerStation1Entity});
}
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
allocateBuildingId(m_factoryState));
}
{
const QPoint anchor(psAnchorX, ps2Y);
@@ -373,7 +371,8 @@ void Simulation::placeInitialStructures()
m_admin.addComponent<ModuleOwnerComponent>(wChild,
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.
@@ -428,7 +427,8 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild,
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);
@@ -445,7 +445,8 @@ void Simulation::placeEnemyStationSet(int generation)
m_admin.addComponent<ModuleOwnerComponent>(wChild,
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.
hasher.append(m_currentTick);
hasher.append(m_nextDepartureTick);
hasher.append(m_nextBuildingId);
hasher.append(m_factoryState.nextBuildingId);
hasher.append(m_factoryState.buildingBlocksStock);
hasher.append(m_gameOver);
hasher.append(m_isWon);
@@ -948,7 +949,3 @@ void Simulation::handleEvent(std::shared_ptr<const TracePrintRequestedEvent> eve
PRINT_TRACES();
}
BuildingId Simulation::allocateBuildingId()
{
return m_nextBuildingId++;
}

View File

@@ -169,7 +169,6 @@ private:
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();
// the construction order is load-bearing for determinism, so both paths must
@@ -195,7 +194,6 @@ private:
Tick m_currentTick;
Tick m_nextDepartureTick;
BuildingId m_nextBuildingId;
int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x
bool m_gameOver = false;
bool m_isWon = false;

View File

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

View File

@@ -99,7 +99,6 @@ struct PlacementFixture
FactoryState state = makeFactoryState(cfg);
BeltSystem belts;
std::mt19937 rng{0};
BuildingId nextBuildingId = 1;
BuildingSystem bs;
// 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)
: belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps))
, bs(cfg, belts,
[this]() { return nextBuildingId++; },
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
isItemUnlocked ? std::move(isItemUnlocked)
: std::function<bool(const std::string&)>(

View File

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