The last system holding a piece of the world as a member. ConstructionSystem and
ProductionSystem take the transport layer as a tick argument; BuildingSystem kept
a BeltSystem& from construction, so the same object arrived two different ways
depending on which system you were reading.
Three methods need it -- deconstruct, cancelDeconstruction and rotateInPlace, all
of which register or unregister a belt tile -- and they now take it after the
state, in the argument order the other systems use. The constructor is down to
the config alone.
CombatSystemTest's fixture kept a BeltSystem only to pass it here, so that goes
too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
A public mutator that erased a building with no refund and no deconstruction
queue, documented as being "used for deaths". Nothing calls it: stations and the
HQ became ECS entities, so a death releases its tiles through
unregisterTileOccupancy instead, and every player-driven removal goes through
deconstruct.
Worth removing rather than leaving inert -- it was a way to take a building out
of the world that skipped the refund and the queue, sitting in public view on the
class whose whole job is that lifecycle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
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
BuildingSystem::appendChecksum took a const FactoryState& and touched no member
of the system it hung off -- the same case as the two item visitors, and the
last const method on the class. Fingerprinting the factory needs the data and
nothing else: no config, no belts, no RNG.
It becomes a free function in FactoryChecksum.h/.cpp, taking its three private
helpers with it, and Simulation calls it directly. BuildingSystem loses its last
non-mutating method and its Hasher dependency; the header is down to placement,
configuration, the tick hooks and the topology mutators.
The fold order is untouched -- the block moved verbatim and the call still sits
between the unlock state and the belts in computeStateChecksum -- so recorded
replays keep verifying. Note that no test would have caught a reordering here:
DeterminismTest compares two runs of the same build, and replays carry no golden
checksum, so the guarantee comes from the move being verbatim. The new header
says so, for whoever adds a FactoryState field next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
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
The global building block stock lived on Simulation while being factory data
through and through: placement spends it, deconstruction refunds it, and blocks
delivered to the HQ by belt add to it. Every system that credited it therefore
held a std::function back into Simulation to do so -- BuildingSystem and
DeconstructionSystem each carried one, and the arena and three test fixtures had
to pass a stub.
It moves into FactoryState, seeded by makeFactoryState from
world.starting_building_blocks, and both callbacks are gone: the HQ's belt intake
and the deconstruction refund now credit the state they are already holding.
BuildingSystem::deconstruct stops returning a refund for its caller to remember
to credit. It had grown asymmetric -- the queued path credits itself through
DeconstructionSystem while the instant path handed a number back -- so it now
credits the site's full cost directly and returns void.
Checksum order is untouched: Simulation still folds the stock at exactly the
point it always did, reading it from the state. The arena no longer discards
refunds into a no-op sink; nothing there reads the stock either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
forEachEmergingItem and forEachIncomingItem took a const FactoryState& and
touched no member of BuildingSystem: they read the state and the free geometry
helpers and nothing else. They are queries wearing a system's uniform, and their
one caller -- the renderer -- reached through getBuildings() to call them,
passing the state back in as an argument.
They move to FactoryQueries beside the rest of the read surface, and the renderer
calls them directly. With that, BuildingSystem answers no queries at all: its
const accessor on Simulation and on ArenaSimulation had no other user, so both
are gone. Nothing outside the command path can now reach the system.
No behaviour changes; the bodies move verbatim apart from two arrows in comments
that were non-ASCII.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
The FactoryState migration finished some time ago -- every system that touches
the factory takes the state as an argument and holds none of it, and the const
surface has become free functions over the struct -- but the comments still
described the state of affairs two refactors back.
FactoryState.h claimed BuildingSystem holds a reference and that passing the
state into the tick methods was the remaining step, gated on ~180 const call
sites that no longer exist. It now says what is true, and names what genuinely
has not moved: the id counter and the block stock, factory data still living on
Simulation behind callbacks.
architecture.md described neither FactoryState nor FactoryQueries at all, so the
Buildings section told a reader that buildings are a plain vector and nothing
about who owns it or how it is read. It gains that section, including why
ConstructionSystem completes a building itself.
BuildingSystem.h had collected ten comment blocks whose declarations had moved
out from under them -- the whole Queries section was four comments and no
functions, and the deconstruction-queue tick comment sat above tickBeltPull,
documenting the wrong function. Those are gone; tickBeltPull gets a comment
describing what it actually does, and the Queries banner now points at where the
queries went. No code changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
The belt card's only content was a button reading "Clear stuck items", which
assumed a state the items need not be in. It now reads "Clear items", and above
it the card lists what the selected tiles hold -- one item chip per type, the
same chip the buffer sections and the HQ's block stock draw -- so a line's
contents can be read before they are removed, and can be read at all: items on a
moving belt are too small and too transient to count by eye, and items inside a
tunnel are drawn nowhere.
BeltSystem gains countItems(tiles), a query of the same kind as
forEachVisualItem: a method rather than exposed tile containers, so the per-tile
representation stays swappable. It walks the same five containers as clearTiles,
in the same order, so the list and the button cannot drift apart.
The tunnel's two ends are now told apart. Items in transit are counted on the
exit they are travelling toward, and a clear removes exactly what the panel
listed for the tile it acts on: the exit discards them, the entry leaves them
travelling. BeltSystemTest's tunnel case splits in two accordingly.
Splitters now aggregate with belts and tunnel ends. Their output filters are
per-object configuration, which an aggregate simply does not show -- a splitter
selected alone still gets them. Both cards share one BeltItemList widget, and
both derive their tiles from collectBeltTiles, so nothing is stated twice.
The mixed count summary loses the clear action it carried: a button acting on
part of a selection is worse than no button, and the tiles can be selected by
themselves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
Implements REQ-MAT-OUTPUT-GROUP. A recipe had two shapes -- outputs produced
together, or outputs of which exactly one happened -- and every rule over them
was written twice, selected by `building == ReprocessingPlant`: sizing a
buffer, deciding whether a cycle fits, resolving what a cycle makes, costing an
item. RecipeDef now holds output groups, each a weight and a list of items, and
a cycle yields exactly one group. One group is the ordinary recipe, so the old
two cases are the same shape with one and with several, and all four rules
collapse to one expression apiece with no building-type test left.
rollReprocessingOutput becomes rollOutputGroup, where a single group returns
without drawing or testing eligibility. That early-out is load-bearing twice
over. Drawing there would consume entropy for every ordinary recipe and shift
every later random outcome; and eligibility must not apply either, since
implicit unlocking is demand-derived, so an ordinary recipe's output can be
producible while nothing yet calls for it -- testing it would stop the building
producing rather than gate a drop. Past the early-out a group is eligible only
when all of its items are unlocked, being produced whole.
Threat follows the recipe's shape rather than the building, and the per-unit
value now divides by the group's amount as well as its odds. That moves no
number today: every item resolved through this path has amount 1, which is why
the threat expectations are untouched.
Config keeps `outputs = [...]` as the single-group form, so only the two
reprocessing recipes change shape. The recipe summary gains "/" between groups
and keeps "+" within one, which also fixes the plant reading as though a cycle
produced all of its items at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x