From 71d0dad3f2dc1531174c0461d9ee5d905cefb182 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Tue, 4 Aug 2026 17:22:40 +0200 Subject: [PATCH] give tile occupancy its own class, BuildingGrid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eleven methods maintained m_tileOccupancy by hand — place, deconstruct, removeBuilding, placeImmediate, tickDeconstruction, findRotateInPlaceTarget and tryDirectCoupleDeposit all indexed a raw std::map, BuildingId> directly, so the invariant "occupancy stays in sync with placement" was re-implemented at every call site. They now ask and tell a small owned index instead: occupy / release / isOccupied / findOwner. BuildingGrid is a member of BuildingSystem, not a peer system: it has no per-tick behaviour and nothing outside BuildingSystem touches it. The internal keying stays std::pair rather than moving to QPoint. The checksum folds the entries in map iteration order, so the comparator is part of the determinism contract; changing it is a separate decision, not a side effect of this move. Verified with a golden-checksum capture before and after — all four sample ticks identical. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG --- src/lib/sim/BuildingGrid.cpp | 52 ++++++++++++++++++++++++++++++ src/lib/sim/BuildingGrid.h | 46 +++++++++++++++++++++++++++ src/lib/sim/BuildingSystem.cpp | 58 ++++++++++------------------------ src/lib/sim/BuildingSystem.h | 6 ++-- src/lib/sim/CMakeLists.txt | 2 ++ 5 files changed, 121 insertions(+), 43 deletions(-) create mode 100644 src/lib/sim/BuildingGrid.cpp create mode 100644 src/lib/sim/BuildingGrid.h diff --git a/src/lib/sim/BuildingGrid.cpp b/src/lib/sim/BuildingGrid.cpp new file mode 100644 index 0000000..ff71483 --- /dev/null +++ b/src/lib/sim/BuildingGrid.cpp @@ -0,0 +1,52 @@ +#include "BuildingGrid.h" + +#include "StateChecksum.h" + +void BuildingGrid::occupy(QPoint cell, BuildingId id) +{ + m_owners[{cell.x(), cell.y()}] = id; +} + +void BuildingGrid::occupy(const std::vector& cells, BuildingId id) +{ + for (const QPoint& cell : cells) + { + occupy(cell, id); + } +} + +void BuildingGrid::release(const std::vector& cells) +{ + for (const QPoint& cell : cells) + { + m_owners.erase({cell.x(), cell.y()}); + } +} + +bool BuildingGrid::isOccupied(QPoint tile) const +{ + return m_owners.count({tile.x(), tile.y()}) > 0; +} + +std::optional BuildingGrid::findOwner(QPoint tile) const +{ + const std::map, BuildingId>::const_iterator it = + m_owners.find({tile.x(), tile.y()}); + if (it == m_owners.end()) + { + return std::nullopt; + } + return it->second; +} + +void BuildingGrid::appendChecksum(Hasher& hasher) const +{ + // std::map iterates in sorted key order. + hasher.append(m_owners.size()); + for (const std::pair, BuildingId>& entry : m_owners) + { + hasher.append(entry.first.first); + hasher.append(entry.first.second); + hasher.append(entry.second); + } +} diff --git a/src/lib/sim/BuildingGrid.h b/src/lib/sim/BuildingGrid.h new file mode 100644 index 0000000..9460231 --- /dev/null +++ b/src/lib/sim/BuildingGrid.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "BuildingId.h" + +class Hasher; + +// The authority on which building owns which world tile. +// +// Every building and construction site claims its body cells here when it is placed +// and releases them when it is removed, so the map is the single place that knows +// whether a tile is free. It is a plain index owned by BuildingSystem, not a system: +// it has no per-tick behaviour and nothing outside BuildingSystem touches it. +// +// Keys are deliberately std::pair rather than QPoint: the checksum folds the +// entries in map iteration order (docs/replay_design.md), so the comparator is part of +// the determinism contract and is not changed casually. +class BuildingGrid +{ +public: + // Records absolute body cells as owned by id. Re-occupying a cell overwrites its + // previous owner, matching the placement paths that reserve cells for a site and + // then hand them to the building it becomes. + void occupy(QPoint cell, BuildingId id); + void occupy(const std::vector& cells, BuildingId id); + + // Releases absolute body cells. Cells that are not occupied are ignored. + void release(const std::vector& cells); + + bool isOccupied(QPoint tile) const; + + // The building owning the tile, or nullopt when the tile is free. + std::optional findOwner(QPoint tile) const; + + // Folds the occupancy into the hasher in deterministic order. + void appendChecksum(Hasher& hasher) const; + +private: + std::map, BuildingId> m_owners; +}; diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 5f4909f..647505a 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -308,7 +308,7 @@ std::optional BuildingSystem::place(BuildingType type, QPoint anchor for (const QPoint& cell : mask.bodyCells) { const QPoint absCell = anchor + cell; - m_tileOccupancy[{absCell.x(), absCell.y()}] = id; + m_grid.occupy(absCell, id); } // Build construction site. @@ -412,10 +412,7 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick) if (it->id == id) { const BuildingDef* def = m_config.buildings.findBuildingDef(it->type); - for (const QPoint& cell : it->bodyCells) - { - m_tileOccupancy.erase({cell.x(), cell.y()}); - } + m_grid.release(it->bodyCells); m_constructionQueue.erase(it); if (def) { @@ -799,10 +796,7 @@ void BuildingSystem::tickDeconstruction(Tick currentTick) if (it->id != front.id) { continue; } const BuildingDef* def = m_config.buildings.findBuildingDef(it->type); - for (const QPoint& cell : it->bodyCells) - { - m_tileOccupancy.erase({cell.x(), cell.y()}); - } + m_grid.release(it->bodyCells); m_buildings.erase(it); if (def) { @@ -938,14 +932,13 @@ bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId, const Port& outputPort, const Item& item) { - const std::map, BuildingId>::const_iterator occIt = - m_tileOccupancy.find({outputPort.tile.x(), outputPort.tile.y()}); - if (occIt == m_tileOccupancy.end() || occIt->second == producerId) + const std::optional ownerId = m_grid.findOwner(outputPort.tile); + if (!ownerId.has_value() || *ownerId == producerId) { return false; } - Building* consumer = findBuildingMutable(occIt->second); + Building* consumer = findBuildingMutable(*ownerId); if (!consumer) { return false; // an unbuilt construction site, or not an operational building @@ -1527,7 +1520,7 @@ std::vector BuildingSystem::getAllBeltTiles() cons bool BuildingSystem::isTileOccupied(QPoint tile) const { - return m_tileOccupancy.count({tile.x(), tile.y()}) > 0; + return m_grid.isOccupied(tile); } std::optional BuildingSystem::findRotateInPlaceTarget( @@ -1548,15 +1541,14 @@ std::optional BuildingSystem::findRotateInPlaceTarget( // All body cells must be occupied by the same entity. const QPoint firstAbs = anchor + mask.bodyCells[0]; - const auto firstIt = m_tileOccupancy.find({firstAbs.x(), firstAbs.y()}); - if (firstIt == m_tileOccupancy.end()) { return std::nullopt; } - const BuildingId candidateId = firstIt->second; + const std::optional firstOwner = m_grid.findOwner(firstAbs); + if (!firstOwner.has_value()) { return std::nullopt; } + const BuildingId candidateId = *firstOwner; for (const QPoint& rel : mask.bodyCells) { - const QPoint abs = anchor + rel; - const auto it = m_tileOccupancy.find({abs.x(), abs.y()}); - if (it == m_tileOccupancy.end() || it->second != candidateId) + const std::optional owner = m_grid.findOwner(anchor + rel); + if (!owner.has_value() || *owner != candidateId) { return std::nullopt; } @@ -1716,7 +1708,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type, { const QPoint absCell = anchor + cell; building.bodyCells.push_back(absCell); - m_tileOccupancy[{absCell.x(), absCell.y()}] = id; + m_grid.occupy(absCell, id); } for (const Port& port : mask.outputPorts) { @@ -1751,10 +1743,7 @@ bool BuildingSystem::removeBuilding(BuildingId id) { m_belts.removeTile(it->anchor); } - for (const QPoint& cell : it->bodyCells) - { - m_tileOccupancy.erase({cell.x(), cell.y()}); - } + m_grid.release(it->bodyCells); m_buildings.erase(it); return true; } @@ -1773,18 +1762,12 @@ void BuildingSystem::forEachBuilding(std::function fn) void BuildingSystem::registerTileOccupancy(const std::vector& cells, BuildingId ownerPlaceholder) { - for (const QPoint& cell : cells) - { - m_tileOccupancy[{cell.x(), cell.y()}] = ownerPlaceholder; - } + m_grid.occupy(cells, ownerPlaceholder); } void BuildingSystem::unregisterTileOccupancy(const std::vector& cells) { - for (const QPoint& cell : cells) - { - m_tileOccupancy.erase({cell.x(), cell.y()}); - } + m_grid.release(cells); } namespace @@ -1893,12 +1876,5 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); } } - // std::map iterates in sorted key order. - hasher.append(m_tileOccupancy.size()); - for (const std::pair, BuildingId>& entry : m_tileOccupancy) - { - hasher.append(entry.first.first); - hasher.append(entry.first.second); - hasher.append(entry.second); - } + m_grid.appendChecksum(hasher); } diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index 349c5e7..88cc228 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -15,6 +15,7 @@ #include "BeltSystem.h" #include "Building.h" +#include "BuildingGrid.h" #include "BuildingType.h" #include "BuildingId.h" #include "GameConfig.h" @@ -309,6 +310,7 @@ private: }; std::deque m_deconstructionQueue; - // Maps every occupied body-cell coordinate to the entity that owns it. - std::map, BuildingId> m_tileOccupancy; + // The authority on which building owns which tile; every placement and removal + // path claims and releases its body cells here. + BuildingGrid m_grid; }; diff --git a/src/lib/sim/CMakeLists.txt b/src/lib/sim/CMakeLists.txt index c4ebed0..c0b8fcd 100644 --- a/src/lib/sim/CMakeLists.txt +++ b/src/lib/sim/CMakeLists.txt @@ -12,6 +12,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/Building.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h @@ -36,6 +37,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp