From 10ba226af7fa1d55a4537954f5d6fc57486ad2bf Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Tue, 4 Aug 2026 14:10:52 +0200 Subject: [PATCH] wire Simulation to forward schematic/unlock queries to UnlockState Simulation now owns an UnlockState member (constructed before initializeSubsystems(), since BuildingSystem's spawn-gating lambda calls into it via isSchematicUnlocked instead of poking the old m_schematicLevels map directly). The public isXUnlocked accessors become one-line forwards, applySchematicChoice's group-awarding block becomes a single awardUnlockGroup() call, and generateSchematicChoices (which still owns m_rng and must not change call ordering) now reads group state and builds options through UnlockState. The checksum fold at computeStateChecksum's schematic/unlock section had to move together with the containers it reads; UnlockState:: appendChecksum makes the identical seven appendSchematicMap/ appendStringSet calls in the identical order, so the fold is unaffected. Verified via a temporary golden-checksum test case (added, checked, then removed) that tick1/100/999/1999 checksums for seed 12345 are byte-identical to pre-refactor. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG --- src/lib/sim/Simulation.cpp | 327 ++----------------------------------- src/lib/sim/Simulation.h | 71 +------- 2 files changed, 20 insertions(+), 378 deletions(-) diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 27140d2..53275de 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -6,7 +6,6 @@ #include "AiSystem.h" #include "Command.h" -#include "DisplayName.h" #include "BuildingSystem.h" #include "CombatSystem.h" #include "DynamicBodyComponent.h" @@ -43,6 +42,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed) , m_hqProxyEntity(entt::null) , m_playerStation1Entity(entt::null) , m_playerStation2Entity(entt::null) + , m_unlockState(m_config) , m_beltSystem(m_config.world.beltSpeed_tps) { m_currentEnemyStationEntities[0] = entt::null; @@ -50,7 +50,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed) initializeSubsystems(); - initializeUnlockState(); + m_unlockState.initializeUnlockState(); placeInitialStructures(); registerForEvents(); } @@ -97,7 +97,7 @@ void Simulation::reset(unsigned int seed) m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps); initializeSubsystems(); - initializeUnlockState(); + m_unlockState.initializeUnlockState(); placeInitialStructures(); } @@ -110,9 +110,7 @@ void Simulation::initializeSubsystems() [this](int amount) { m_buildingBlocksStock += amount; }, [this](const std::string& id, QVector2D pos, const std::optional& layout) { - const std::map::const_iterator it = - m_schematicLevels.find(id); - if (it == m_schematicLevels.end() || !it->second.unlocked) + if (!isSchematicUnlocked(id)) { return; } @@ -131,55 +129,6 @@ void Simulation::initializeSubsystems() m_combatSystem = std::make_unique(m_config); } -void Simulation::initializeUnlockState() -{ - // Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item - // starts locked iff it is granted by a group. - m_grantedShipIds.clear(); - m_grantedModuleIds.clear(); - m_grantedBuildingIds.clear(); - m_grantedRecipeIds.clear(); - for (const UnlockGroupDef& group : m_config.unlocks.groups) - { - m_grantedShipIds.insert(group.ships.begin(), group.ships.end()); - m_grantedModuleIds.insert(group.modules.begin(), group.modules.end()); - m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end()); - m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end()); - } - - m_awardedUnlockGroupIds.clear(); - - m_schematicLevels.clear(); - for (const ShipDef& def : m_config.ships.ships) - { - SchematicState state; - state.unlocked = (m_grantedShipIds.count(def.id) == 0); - m_schematicLevels[def.id] = state; - } - - m_moduleSchematicLevels.clear(); - for (const ModuleDef& def : m_config.modules.modules) - { - SchematicState state; - state.unlocked = (m_grantedModuleIds.count(def.id) == 0); - m_moduleSchematicLevels[def.id] = state; - } - - m_buildingLevels.clear(); - for (const BuildingDef& def : m_config.buildings.buildings) - { - SchematicState state; - state.unlocked = (m_grantedBuildingIds.count(def.id) == 0); - m_buildingLevels[def.id] = state; - } - - // Gated assembler recipes start locked; unlocked_at_start recipes are handled - // in the REQ-LOCK-IMPLICIT traversal, not tracked here. - m_unlockedRecipeSchematicIds.clear(); - - recomputeUnlocked(); -} - // --------------------------------------------------------------------------- // tick // --------------------------------------------------------------------------- @@ -613,9 +562,9 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel) std::vector pool; for (const UnlockGroupDef& group : m_config.unlocks.groups) { - if (m_awardedUnlockGroupIds.count(group.id) > 0) { continue; } + if (m_unlockState.isUnlockGroupAwarded(group.id)) { continue; } if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; } - if (!prerequisitesSatisfied(group.requiredGroupIds)) { continue; } + if (!m_unlockState.prerequisitesSatisfied(group.requiredGroupIds)) { continue; } pool.push_back(&group); } @@ -639,7 +588,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel) const std::size_t endIdx = pool.size() - 1 - static_cast(i); std::swap(pool[rollIdx], pool[endIdx]); - m_pendingSchematicChoices.push_back(makeUnlockOption(*pool[endIdx])); + m_pendingSchematicChoices.push_back(m_unlockState.makeUnlockOption(*pool[endIdx])); } if (artifactRolled) @@ -651,48 +600,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel) } } -SchematicChoiceOption Simulation::makeUnlockOption(const UnlockGroupDef& group) const -{ - SchematicChoiceOption option; - option.isArtifact = false; - option.unlockGroupId = group.id; - option.displayName = toDisplayName(group.id); - - for (const std::string& id : group.ships) - { - option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)}); - } - for (const std::string& id : group.modules) - { - option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)}); - } - for (const std::string& id : group.buildings) - { - option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)}); - } - for (const std::string& id : group.recipes) - { - option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)}); - } - - // REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by - // awarding this whole group. Seed the hypothetical explicit-unlock sets with - // every grant (ship + module materials via step 1a, recipe outputs via step - // 1b), then diff against the current implicit set. - std::set hypotheticalShipIds = getUnlockedShipSchematicIds(); - std::set hypotheticalModuleIds = getUnlockedModuleSchematicIds(); - std::set hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds; - for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); } - for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); } - for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); } - - const UnlockedSets hypothetical = computeUnlockedSets( - hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds); - option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical); - - return option; -} - void Simulation::applySchematicChoice(int choiceIndex) { assert(choiceIndex >= 0 && choiceIndex < static_cast(m_pendingSchematicChoices.size())); @@ -709,204 +616,24 @@ void Simulation::applySchematicChoice(int choiceIndex) return; } - // Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted - // ship, module, building, and assembler recipe at once. - m_awardedUnlockGroupIds.insert(chosen.unlockGroupId); - for (const GrantedSchematic& grant : chosen.grantedItems) - { - switch (grant.type) - { - case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break; - case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break; - case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break; - case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break; - } - } - - recomputeUnlocked(); + m_unlockState.awardUnlockGroup(chosen); m_pendingSchematicChoices.clear(); } -// --------------------------------------------------------------------------- -// Implicit unlock computation (REQ-LOCK-IMPLICIT) -// --------------------------------------------------------------------------- - -void Simulation::recomputeUnlocked() -{ - const UnlockedSets result = computeUnlockedSets( - getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds); - m_unlockedItemIds = result.itemIds; - m_unlockedRecipeIds = result.recipeIds; -} - -std::set Simulation::getUnlockedShipSchematicIds() const -{ - std::set ids; - for (const auto& [id, state] : m_schematicLevels) - { - if (state.unlocked) { ids.insert(id); } - } - return ids; -} - -std::set Simulation::getUnlockedModuleSchematicIds() const -{ - std::set ids; - for (const auto& [id, state] : m_moduleSchematicLevels) - { - if (state.unlocked) { ids.insert(id); } - } - return ids; -} - -bool Simulation::prerequisitesSatisfied(const std::vector& requiredGroupIds) const -{ - // A prerequisite is satisfied only once the named unlock group has been - // awarded (REQ-LOCK-PREREQ). - for (const std::string& groupId : requiredGroupIds) - { - if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; } - } - return true; -} - -Simulation::UnlockedSets Simulation::computeUnlockedSets( - const std::set& unlockedShipSchematicIds, - const std::set& unlockedModuleSchematicIds, - const std::set& unlockedRecipeSchematicIds) const -{ - UnlockedSets result; - - for (const ShipDef& def : m_config.ships.ships) - { - if (unlockedShipSchematicIds.count(def.id) == 0) { continue; } - for (const RecipeIngredient& mat : def.schematic.materials) - { - result.itemIds.insert(mat.item); - } - } - for (const ModuleDef& def : m_config.modules.modules) - { - if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; } - for (const RecipeIngredient& mat : def.materials) - { - result.itemIds.insert(mat.item); - } - } - for (const RecipeDef& def : m_config.recipes.recipes) - { - // An assembler recipe seeds the base set when it is explicitly available: - // flagged unlocked_at_start (base recipes the graph can't reach), or a - // gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT). - if (def.building == BuildingType::Assembler - && (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0)) - { - for (const RecipeOutput& out : def.outputs) - { - result.itemIds.insert(out.item); - } - } - } - - bool changed = true; - while (changed) - { - changed = false; - for (const RecipeDef& recipe : m_config.recipes.recipes) - { - if (recipe.building != BuildingType::Miner - && recipe.building != BuildingType::Smelter - && recipe.building != BuildingType::Assembler) - { - continue; - } - // Skip a gated assembler recipe (granted by an unlock group) whose - // group has not yet been awarded (REQ-LOCK-IMPLICIT step 2). - if (recipe.building == BuildingType::Assembler - && m_grantedRecipeIds.count(recipe.id) > 0 - && unlockedRecipeSchematicIds.count(recipe.id) == 0) - { - continue; - } - bool producesUnlocked = false; - for (const RecipeOutput& out : recipe.outputs) - { - if (result.itemIds.count(out.item) > 0) - { - producesUnlocked = true; - break; - } - } - if (!producesUnlocked) { continue; } - - if (recipe.building == BuildingType::Miner - || recipe.building == BuildingType::Assembler) - { - result.recipeIds.insert(recipe.id); - } - for (const RecipeIngredient& ing : recipe.inputs) - { - if (result.itemIds.insert(ing.item).second) - { - changed = true; - } - } - } - } - - return result; -} - -std::vector Simulation::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const -{ - std::vector recipeIds; - for (const std::string& recipeId : hypothetical.recipeIds) - { - if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; } - recipeIds.push_back(recipeId); - } - std::sort(recipeIds.begin(), recipeIds.end(), - [](const std::string& lhs, const std::string& rhs) - { - return toDisplayName(lhs) < toDisplayName(rhs); - }); - return recipeIds; -} - bool Simulation::isRecipeUnlocked(const std::string& recipeId) const { - return m_unlockedRecipeIds.count(recipeId) > 0; + return m_unlockState.isRecipeUnlocked(recipeId); } bool Simulation::isItemUnlocked(const std::string& itemId) const { - return m_unlockedItemIds.count(itemId) > 0; + return m_unlockState.isItemUnlocked(itemId); } // --------------------------------------------------------------------------- // Determinism (see docs/replay_design.md) // --------------------------------------------------------------------------- -void Simulation::appendSchematicMap(Hasher& hasher, - const std::map& levels) -{ - hasher.append(levels.size()); - for (const std::pair& entry : levels) - { - hasher.append(entry.first); - hasher.append(entry.second.unlocked); - } -} - -void Simulation::appendStringSet(Hasher& hasher, const std::set& ids) -{ - hasher.append(ids.size()); - for (const std::string& id : ids) - { - hasher.append(id); - } -} - unsigned long long Simulation::getRngFingerprint() const { return fingerprintRng(m_rng); @@ -937,13 +664,7 @@ unsigned long long Simulation::computeStateChecksum() const hasher.append(getNormalGapRemainingTicks()); // Schematic / unlock state (std::map and std::set iterate in sorted order). - appendSchematicMap(hasher, m_schematicLevels); - appendSchematicMap(hasher, m_moduleSchematicLevels); - appendSchematicMap(hasher, m_buildingLevels); - appendStringSet(hasher, m_awardedUnlockGroupIds); - appendStringSet(hasher, m_unlockedRecipeSchematicIds); - appendStringSet(hasher, m_unlockedRecipeIds); - appendStringSet(hasher, m_unlockedItemIds); + m_unlockState.appendChecksum(hasher); // Subsystems contribute their own state. m_buildingSystem->appendChecksum(hasher); @@ -1114,37 +835,17 @@ Tick Simulation::getNormalGapRemainingTicks() const bool Simulation::isSchematicUnlocked(const std::string& shipId) const { - const std::map::const_iterator it = - m_schematicLevels.find(shipId); - if (it == m_schematicLevels.end()) - { - return false; - } - return it->second.unlocked; + return m_unlockState.isSchematicUnlocked(shipId); } bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const { - const std::map::const_iterator it = - m_moduleSchematicLevels.find(moduleId); - if (it == m_moduleSchematicLevels.end()) - { - return false; - } - return it->second.unlocked; + return m_unlockState.isModuleSchematicUnlocked(moduleId); } bool Simulation::isBuildingUnlocked(BuildingType type) const { - const BuildingDef* def = m_config.buildings.findBuildingDef(type); - if (def == nullptr) - { - // Types without a config entry (e.g. HQ, defence stations) are unrestricted. - return true; - } - const std::map::const_iterator it = - m_buildingLevels.find(def->id); - return it == m_buildingLevels.end() ? true : it->second.unlocked; + return m_unlockState.isBuildingUnlocked(type); } std::optional Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation) diff --git a/src/lib/sim/Simulation.h b/src/lib/sim/Simulation.h index bacb185..3c58ec9 100644 --- a/src/lib/sim/Simulation.h +++ b/src/lib/sim/Simulation.h @@ -1,10 +1,8 @@ #pragma once -#include #include #include #include -#include #include #include @@ -22,6 +20,7 @@ #include "Rotation.h" #include "Tick.h" #include "TracePrintRequestedEvent.h" +#include "UnlockState.h" class AiSystem; class BuildingSystem; @@ -203,69 +202,11 @@ private: entt::entity m_playerStation2Entity; entt::entity m_currentEnemyStationEntities[2]; - // Schematic unlock state (REQ-DEF-SCHEMATIC-DROP). - struct SchematicState - { - bool unlocked; - }; - std::map m_schematicLevels; - std::map m_moduleSchematicLevels; - std::map m_buildingLevels; - - // Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids. - std::set m_awardedUnlockGroupIds; - - // Ids granted by some unlock group, per kind — cached from config at init. - // An item starts locked iff it appears in the corresponding set. - std::set m_grantedShipIds; - std::set m_grantedModuleIds; - std::set m_grantedBuildingIds; - std::set m_grantedRecipeIds; - - // Builds the granted-id sets and initializes all per-item unlock maps from - // them (shared by the constructor and reset). Ends with recomputeUnlocked(). - void initializeUnlockState(); - - // Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP). - SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const; - - // Determinism helpers — fold sub-state into the hasher in deterministic order. - static void appendSchematicMap(Hasher& hasher, - const std::map& levels); - static void appendStringSet(Hasher& hasher, const std::set& ids); - - // Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT). - std::set m_unlockedRecipeSchematicIds; - - // Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT). - std::set m_unlockedRecipeIds; - std::set m_unlockedItemIds; - - // Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state. - void recomputeUnlocked(); - - // Result of the REQ-LOCK-IMPLICIT traversal. - struct UnlockedSets - { - std::set itemIds; - std::set recipeIds; - }; - - // Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets. - UnlockedSets computeUnlockedSets(const std::set& unlockedShipSchematicIds, - const std::set& unlockedModuleSchematicIds, - const std::set& unlockedRecipeSchematicIds) const; - - // Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels. - std::set getUnlockedShipSchematicIds() const; - std::set getUnlockedModuleSchematicIds() const; - - // True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ). - bool prerequisitesSatisfied(const std::vector& requiredGroupIds) const; - - // Ids (sorted alphabetically by display name) of the recipes in - // hypothetical.recipeIds that are not yet in m_unlockedRecipeIds. - std::vector computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const; + // Schematic/unlock bookkeeping (REQ-DEF-SCHEMATIC-DROP, REQ-LOCK-EXPLICIT, + // REQ-LOCK-IMPLICIT, REQ-LOCK-BUILDING, REQ-LOCK-PREREQ). Constructed before + // initializeSubsystems() runs since BuildingSystem's spawn-gating lambda + // calls into it (see initializeSubsystems()). + UnlockState m_unlockState; EntityAdmin m_admin; BeltSystem m_beltSystem;