An auto-recipe building adopted whatever recipe of its type consumed the
material offered to it, without asking the unlock state. That was the last way
a building could come to run a recipe the player could not have selected: the
selection dialog hides those and the blueprint gate discards them, but a belt
delivering the right material installed one regardless.
Automatic selection now asks isRecipeUnlocked, the same question the dialog
asks, plumbed in beside the isItemUnlocked the output pool already uses. Two
callbacks, two questions, one unlock state -- rather than a second definition of
"a recipe the player may run" written out inside the sim.
A material whose only recipes are locked now selects nothing, and an
unconfigured building has no input buffer, so that material is refused rather
than swallowed: it stays on the belt and the line backs up behind an idle
building. That is the intended failure -- a stalled belt is visible, a building
quietly eating a material the player cannot use is not.
Selection only. A recipe already set goes on producing under
REQ-LOCK-OUTPUT-POOL, which deliberately never tests a single-group recipe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
The rule that decides whether a group may be picked -- every item it yields
unlocked, the group judged whole because its items are produced together
(REQ-LOCK-OUTPUT-POOL) -- was written out inside rollOutputGroup, where nothing
outside the sim could reach it. It moves next to producesItem and
getProducibleItems in RecipesConfig.h, which is where the recipe-level queries
already shared between lib and ui live.
Deliberately narrow: the helper says whether a group is eligible, not whether
eligibility is asked about at all. rollOutputGroup keeps its own reason for not
asking when a recipe has a single group.
No behaviour change; the pool's end-to-end test still covers the rule and a
direct one now pins the judged-whole semantics.
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