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