separate what buildings are from what flows through them

BuildingSystem was four unrelated jobs in one class: building lifecycle,
building configuration, the per-tick material flow, and (until last commit) the
checksum. The flow was the odd one out -- it is what the RNG, the ship spawner
and the unlock test were held for, none of which placement, rotation or
demolition has any business reaching.

Tick steps 3 to 5 move to a new ProductionSystem: tickBeltPull, tickProduction,
tickShipyardProduction, tickOutputBelts, their five private helpers and
rollOutputGroup. The cut is clean in both directions -- nothing in the block
called a topology or configuration method, and nothing outside it called the
helpers -- so the bodies move verbatim; a scripted comparison against the old
file confirms all nine differ only by class qualifier, the m_belts -> belts
rename, and the two added parameters. (Seven em dashes in comments became `--`;
the new file is ASCII, as the guidelines require.)

Belts arrive per tick rather than being held, matching ConstructionSystem, and
only the two methods that touch them take the parameter. BuildingSystem is left
holding the config and the belts, and its constructor takes exactly those two.
The arena constructs no ProductionSystem at all: it stages ships directly and
never runs a factory.

Determinism rests on the RNG stream: step 4's weighted output-group pick is the
factory's only draw, so the four calls must keep their order and position in
Simulation::tick. They do, and the tick-order section now says why, since no
test can catch a reordering here.

913 lines of BuildingSystem.cpp become 444 there and 483 in ProductionSystem.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-20 07:41:20 +02:00
parent 04a52698e8
commit 46649e7dd1
12 changed files with 708 additions and 634 deletions

View File

@@ -109,9 +109,11 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
1. **Wave scheduler** — advance wave timer; on trigger, compute wave composition per REQ-WAV-TRIGGER and schedule spawn times across REQ-WAV-SPAWN-DURATION; spawn any enemy ships whose scheduled time has arrived this tick.
2. **Threat accumulation** — add `max(0, threat_rate_formula(t))` × tick_dt to threat level (REQ-WAV-THREAT-RATE).
3. **Belt → building pull** — buildings drain eligible items from adjacent belt tiles into per-material input buffers (REQ-MAT-INPUT-PORTS).
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output.
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
3. **Belt → building pull** — buildings drain eligible items from adjacent belt tiles into per-material input buffers (REQ-MAT-INPUT-PORTS). `ProductionSystem::tickBeltPull`.
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output. `ProductionSystem::tickProduction`, then `tickShipyardProduction` for the shipyard's ship (REQ-BLD-SHIPYARD).
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT). `ProductionSystem::tickOutputBelts`.
Steps 35 are one system and must stay adjacent and in this order: an item arriving in step 3 is consumable in step 4, and an item produced in step 4 starts travelling in step 5. Step 4 is also the only place the factory draws from the RNG (an output group's weighted pick, REQ-LOCK-OUTPUT-POOL), so moving these calls relative to any other draw invalidates recorded replays.
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration.
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle.
@@ -241,6 +243,7 @@ struct Building {
- The uniform "input buffer → production timer → output buffer" pattern across miner, smelter, assembler, reprocessing plant, and shipyard is driven by the recipe config, not by a class hierarchy.
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
- **What buildings are is separate from what flows through them.** `BuildingSystem` places, demolishes, rotates and configures; `ProductionSystem` runs intake, production cycles and output (tick steps 35). The split follows what each needs: only the flow side draws from the RNG, tests unlock state, and spawns a ship into the entity model, so only it holds those. `BuildingSystem` holds the config and the belts and nothing else.
### Factory State and Queries
@@ -259,8 +262,8 @@ struct FactoryState {
};
```
Every system that touches the factory — `BuildingSystem`, `ConstructionSystem`,
`DeconstructionSystem` — takes it as an argument and holds none of it, the same shape the
Every system that touches the factory — `BuildingSystem`, `ProductionSystem`,
`ConstructionSystem`, `DeconstructionSystem` — takes it as an argument and holds none of it, the same shape the
`lib/ecs/system` classes have, where the world arrives per tick. This is why
`ConstructionSystem` can complete a building itself instead of handing the finished site
back to `BuildingSystem`: with the state in the argument there is no owner to route