From e7bfd910546102dceeb8977ce202045c7a073507 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Wed, 19 Aug 2026 17:31:48 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x --- docs/architecture.md | 12 ++++++------ src/balancing/ArenaSimulation.cpp | 15 ++++++--------- src/balancing/ArenaSimulation.h | 2 -- src/lib/sim/BuildingSystem.cpp | 6 ++---- src/lib/sim/BuildingSystem.h | 9 ++++----- src/lib/sim/FactoryQueries.cpp | 5 +++++ src/lib/sim/FactoryQueries.h | 5 +++++ src/lib/sim/FactoryState.h | 6 ++++++ src/lib/sim/Simulation.cpp | 21 +++++++++------------ src/lib/sim/Simulation.h | 2 -- src/test/BehaviorSystemTest.cpp | 3 --- src/test/BuildingTest.cpp | 2 -- src/test/CombatSystemTest.cpp | 3 --- 13 files changed, 43 insertions(+), 48 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index aef40a7..2bb4626 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index aefa85a..e2c930d 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -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( m_gameConfig, m_beltSystem, - [this]() { return allocateBuildingId(); }, [](const std::string&, QVector2D, const std::optional&) {}, [](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(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(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(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) diff --git a/src/balancing/ArenaSimulation.h b/src/balancing/ArenaSimulation.h index b6ef73c..e752ff7 100644 --- a/src/balancing/ArenaSimulation.h +++ b/src/balancing/ArenaSimulation.h @@ -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; diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 67b2bdb..0918c6f 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -27,14 +27,12 @@ bool inputLaneEntryFree(const std::vector& lane) BuildingSystem::BuildingSystem(const GameConfig& config, BeltSystem& belts, - std::function allocateBuildingId, std::function&)> spawnShip, std::function 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 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& surfaceMask, QPoint anchor, Rotation rotation) { - const BuildingId id = m_allocateBuildingId(); + const BuildingId id = allocateBuildingId(state); const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation); Building building; diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index d387ea0..1d7087d 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -40,7 +40,6 @@ class BuildingSystem public: BuildingSystem(const GameConfig& config, BeltSystem& belts, - std::function allocateBuildingId, std::function&)> spawnShip, std::function isItemUnlocked, @@ -173,12 +172,12 @@ private: std::vector 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 m_allocateBuildingId; std::function&)> m_spawnShip; std::function m_isItemUnlocked; diff --git a/src/lib/sim/FactoryQueries.cpp b/src/lib/sim/FactoryQueries.cpp index ed1e45a..8d0ab30 100644 --- a/src/lib/sim/FactoryQueries.cpp +++ b/src/lib/sim/FactoryQueries.cpp @@ -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& visit) { diff --git a/src/lib/sim/FactoryQueries.h b/src/lib/sim/FactoryQueries.h index 935cb09..75b712a 100644 --- a/src/lib/sim/FactoryQueries.h +++ b/src/lib/sim/FactoryQueries.h @@ -85,6 +85,11 @@ std::vector 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 diff --git a/src/lib/sim/FactoryState.h b/src/lib/sim/FactoryState.h index 6cac93b..f1a2066 100644 --- a/src/lib/sim/FactoryState.h +++ b/src/lib/sim/FactoryState.h @@ -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 diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 4f7f87f..a347643 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -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( m_config, m_beltSystem, - [this]() { return allocateBuildingId(); }, [this](const std::string& id, QVector2D pos, const std::optional& layout) { if (!isSchematicUnlocked(id)) @@ -356,7 +353,8 @@ void Simulation::placeInitialStructures() m_admin.addComponent(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(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(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(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 eve PRINT_TRACES(); } -BuildingId Simulation::allocateBuildingId() -{ - return m_nextBuildingId++; -} diff --git a/src/lib/sim/Simulation.h b/src/lib/sim/Simulation.h index 81ea6ee..75f9e20 100644 --- a/src/lib/sim/Simulation.h +++ b/src/lib/sim/Simulation.h @@ -169,7 +169,6 @@ private: void handleEvent(std::shared_ptr 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; diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 9291e03..9806023 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -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&) {}, [](const std::string&) -> bool { return true; }, rng) diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 60820f6..f68c20a 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -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 isItemUnlocked = nullptr) : belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps)) , bs(cfg, belts, - [this]() { return nextBuildingId++; }, [](const std::string&, QVector2D, const std::optional&) {}, isItemUnlocked ? std::move(isItemUnlocked) : std::function( diff --git a/src/test/CombatSystemTest.cpp b/src/test/CombatSystemTest.cpp index 2d4c293..27d9e81 100644 --- a/src/test/CombatSystemTest.cpp +++ b/src/test/CombatSystemTest.cpp @@ -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&) {}, [](const std::string&) -> bool { return true; }, rng)