From 04a52698e8087d49ad82c3388a52cb554ccb6693 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Wed, 19 Aug 2026 21:48:00 +0200 Subject: [PATCH] fingerprint the factory without going through a system 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 Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x --- src/lib/sim/BuildingSystem.cpp | 111 ----------------------------- src/lib/sim/BuildingSystem.h | 7 -- src/lib/sim/CMakeLists.txt | 2 + src/lib/sim/FactoryChecksum.cpp | 120 ++++++++++++++++++++++++++++++++ src/lib/sim/FactoryChecksum.h | 18 +++++ src/lib/sim/Simulation.cpp | 3 +- 6 files changed, 142 insertions(+), 119 deletions(-) create mode 100644 src/lib/sim/FactoryChecksum.cpp create mode 100644 src/lib/sim/FactoryChecksum.h diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 0918c6f..2d07ae0 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -10,7 +10,6 @@ #include "PlacementRules.h" #include "ProductionRules.h" #include "PortGeometry.h" -#include "StateChecksum.h" #include "SurfaceMask.h" #include "tracing.h" @@ -912,113 +911,3 @@ void BuildingSystem::unregisterTileOccupancy(FactoryState& state, const std::vec { state.grid.release(cells); } - -namespace -{ -void appendItems(Hasher& hasher, const std::vector& items) -{ - hasher.append(items.size()); - for (const Item& item : items) - { - hasher.append(item.type.id); - } -} - -// std::map iterates in sorted-id order (ItemType::operator<), so both -// buffer sides hash the same way in every run. -void appendItemCounts(Hasher& hasher, const std::map& counts) -{ - hasher.append(counts.size()); - for (const std::pair& entry : counts) - { - hasher.append(entry.first.id); - hasher.append(entry.second); - } -} - -void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer) -{ - appendItemCounts(hasher, buffer.counts); - appendItemCounts(hasher, buffer.caps); -} -} // namespace - -void BuildingSystem::appendChecksum(const FactoryState& state, Hasher& hasher) const -{ - // state.buildings keeps a stable, deterministic order (append on build, swap-free - // erase aside — both runs perform identical operations, so order matches). - hasher.append(state.buildings.size()); - for (const Building& b : state.buildings) - { - hasher.append(b.id); - hasher.append(b.anchor); - hasher.append(b.footprint.width()); - hasher.append(b.footprint.height()); - hasher.append(b.rotation); - hasher.append(b.type); - hasher.append(b.recipeId); - appendInputBuffer(hasher, b.inputBuffer); - appendItems(hasher, b.outputBuffer.items); - appendItemCounts(hasher, b.outputBuffer.caps); - hasher.append(b.emergingItems.size()); - for (const std::vector& lane : b.emergingItems) - { - hasher.append(lane.size()); - for (const BeltItemSlot& slot : lane) - { - hasher.append(slot.item.type.id); - hasher.append(slot.progress); - } - } - hasher.append(b.incomingItems.size()); - for (const std::vector& lane : b.incomingItems) - { - hasher.append(lane.size()); - for (const BeltItemSlot& slot : lane) - { - hasher.append(slot.item.type.id); - hasher.append(slot.progress); - } - } - hasher.append(b.production.has_value()); - if (b.production.has_value()) - { - hasher.append(b.production->recipeId); - hasher.append(b.production->completesAt); - appendItems(hasher, b.production->chosenOutputs); - } - hasher.append(b.shipLayout.has_value()); - hasher.append(b.queuedForDeconstruction); - } - - hasher.append(state.constructionQueue.size()); - for (const ConstructionSite& s : state.constructionQueue) - { - hasher.append(s.id); - hasher.append(s.anchor); - hasher.append(s.footprint.width()); - hasher.append(s.footprint.height()); - hasher.append(s.rotation); - hasher.append(s.type); - hasher.append(s.recipeId); - hasher.append(s.completesAt); - hasher.append(s.shipLayout.has_value()); - hasher.append(s.splitterFilterA.size()); - for (const ItemType& type : s.splitterFilterA) { hasher.append(type.id); } - hasher.append(s.splitterFilterB.size()); - for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); } - } - - hasher.append(state.deconstructionQueue.size()); - for (const DeconstructionEntry& e : state.deconstructionQueue) - { - hasher.append(e.id); - hasher.append(e.completesAt); - hasher.append(e.splitterFilterA.size()); - for (const ItemType& type : e.splitterFilterA) { hasher.append(type.id); } - hasher.append(e.splitterFilterB.size()); - for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); } - } - - state.grid.appendChecksum(hasher); -} diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index 1d7087d..4e753ad 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -29,8 +29,6 @@ #include "ShipsConfig.h" #include "Tick.h" -class Hasher; - // Manages building placement, construction queuing, and the per-tick // production loop (belt→building pull, production, building→belt push). // All types including Belt and Splitter are stored as Building instances; @@ -134,11 +132,6 @@ public: // Mutable iteration over all operational buildings. void forEachBuilding(FactoryState& state, std::function fn); - // -- Determinism --------------------------------------------------------- - // Folds all building, construction-site, and tile-occupancy state into the - // hasher in deterministic order (see docs/replay_design.md). - void appendChecksum(const FactoryState& state, Hasher& hasher) const; - private: // Selects a recipe for an auto-recipe building that has none, from a material being // offered to it at one of its input ports (REQ-BLD-AUTO-RECIPE). No-op for every diff --git a/src/lib/sim/CMakeLists.txt b/src/lib/sim/CMakeLists.txt index cf1698b..b3fd447 100644 --- a/src/lib/sim/CMakeLists.txt +++ b/src/lib/sim/CMakeLists.txt @@ -17,6 +17,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h + ${CMAKE_CURRENT_SOURCE_DIR}/FactoryChecksum.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h ${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h @@ -48,6 +49,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/FactoryChecksum.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp diff --git a/src/lib/sim/FactoryChecksum.cpp b/src/lib/sim/FactoryChecksum.cpp new file mode 100644 index 0000000..0e50780 --- /dev/null +++ b/src/lib/sim/FactoryChecksum.cpp @@ -0,0 +1,120 @@ +#include "FactoryChecksum.h" + +#include +#include + +#include "BuildingBuffers.h" +#include "FactoryState.h" +#include "Item.h" +#include "ItemType.h" +#include "StateChecksum.h" + +namespace +{ +void appendItems(Hasher& hasher, const std::vector& items) +{ + hasher.append(items.size()); + for (const Item& item : items) + { + hasher.append(item.type.id); + } +} + +// std::map iterates in sorted-id order (ItemType::operator<), so both +// buffer sides hash the same way in every run. +void appendItemCounts(Hasher& hasher, const std::map& counts) +{ + hasher.append(counts.size()); + for (const std::pair& entry : counts) + { + hasher.append(entry.first.id); + hasher.append(entry.second); + } +} + +void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer) +{ + appendItemCounts(hasher, buffer.counts); + appendItemCounts(hasher, buffer.caps); +} +} // namespace + +void appendChecksum(const FactoryState& state, Hasher& hasher) +{ + // state.buildings keeps a stable, deterministic order (append on build, swap-free + // erase aside -- both runs perform identical operations, so order matches). + hasher.append(state.buildings.size()); + for (const Building& b : state.buildings) + { + hasher.append(b.id); + hasher.append(b.anchor); + hasher.append(b.footprint.width()); + hasher.append(b.footprint.height()); + hasher.append(b.rotation); + hasher.append(b.type); + hasher.append(b.recipeId); + appendInputBuffer(hasher, b.inputBuffer); + appendItems(hasher, b.outputBuffer.items); + appendItemCounts(hasher, b.outputBuffer.caps); + hasher.append(b.emergingItems.size()); + for (const std::vector& lane : b.emergingItems) + { + hasher.append(lane.size()); + for (const BeltItemSlot& slot : lane) + { + hasher.append(slot.item.type.id); + hasher.append(slot.progress); + } + } + hasher.append(b.incomingItems.size()); + for (const std::vector& lane : b.incomingItems) + { + hasher.append(lane.size()); + for (const BeltItemSlot& slot : lane) + { + hasher.append(slot.item.type.id); + hasher.append(slot.progress); + } + } + hasher.append(b.production.has_value()); + if (b.production.has_value()) + { + hasher.append(b.production->recipeId); + hasher.append(b.production->completesAt); + appendItems(hasher, b.production->chosenOutputs); + } + hasher.append(b.shipLayout.has_value()); + hasher.append(b.queuedForDeconstruction); + } + + hasher.append(state.constructionQueue.size()); + for (const ConstructionSite& s : state.constructionQueue) + { + hasher.append(s.id); + hasher.append(s.anchor); + hasher.append(s.footprint.width()); + hasher.append(s.footprint.height()); + hasher.append(s.rotation); + hasher.append(s.type); + hasher.append(s.recipeId); + hasher.append(s.completesAt); + hasher.append(s.shipLayout.has_value()); + hasher.append(s.splitterFilterA.size()); + for (const ItemType& type : s.splitterFilterA) { hasher.append(type.id); } + hasher.append(s.splitterFilterB.size()); + for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); } + } + + hasher.append(state.deconstructionQueue.size()); + for (const DeconstructionEntry& e : state.deconstructionQueue) + { + hasher.append(e.id); + hasher.append(e.completesAt); + hasher.append(e.splitterFilterA.size()); + for (const ItemType& type : e.splitterFilterA) { hasher.append(type.id); } + hasher.append(e.splitterFilterB.size()); + for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); } + } + + state.grid.appendChecksum(hasher); +} diff --git a/src/lib/sim/FactoryChecksum.h b/src/lib/sim/FactoryChecksum.h new file mode 100644 index 0000000..c723a6c --- /dev/null +++ b/src/lib/sim/FactoryChecksum.h @@ -0,0 +1,18 @@ +#pragma once + +struct FactoryState; +class Hasher; + +// Folds every building, construction site, deconstruction entry and tile claim into the +// hasher in deterministic order (docs/replay_design.md). +// +// A free function over the state rather than a system method, for the reason the rest of +// the read surface is one (FactoryQueries.h): fingerprinting the factory needs the data +// and nothing else -- no config, no belts, no RNG -- so it should not be reachable only +// through whichever system happens to tick that data. +// +// The order it folds in is load-bearing. Two runs of the same replay must fold identical +// values in an identical sequence, so nothing here may be reordered, and any field added +// to FactoryState that the simulation reads must be appended at the end rather than +// inserted (docs/replay_design.md, DeterminismTest). +void appendChecksum(const FactoryState& state, Hasher& hasher); diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index a347643..ecf5f2b 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -1,5 +1,6 @@ #include "Simulation.h" +#include "FactoryChecksum.h" #include "FactoryQueries.h" #include "ConstructionSystem.h" #include "DeconstructionSystem.h" @@ -672,7 +673,7 @@ unsigned long long Simulation::computeStateChecksum() const m_unlockState.appendChecksum(hasher); // Subsystems contribute their own state. - m_buildingSystem->appendChecksum(m_factoryState, hasher); + appendChecksum(m_factoryState, hasher); m_beltSystem.appendChecksum(hasher); // ECS component state. View iteration order is a pure function of the