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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
@@ -43,6 +42,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
, m_hqProxyEntity(entt::null)
|
, m_hqProxyEntity(entt::null)
|
||||||
, m_playerStation1Entity(entt::null)
|
, m_playerStation1Entity(entt::null)
|
||||||
, m_playerStation2Entity(entt::null)
|
, m_playerStation2Entity(entt::null)
|
||||||
|
, m_unlockState(m_config)
|
||||||
, m_beltSystem(m_config.world.beltSpeed_tps)
|
, m_beltSystem(m_config.world.beltSpeed_tps)
|
||||||
{
|
{
|
||||||
m_currentEnemyStationEntities[0] = entt::null;
|
m_currentEnemyStationEntities[0] = entt::null;
|
||||||
@@ -50,7 +50,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
|
|
||||||
initializeSubsystems();
|
initializeSubsystems();
|
||||||
|
|
||||||
initializeUnlockState();
|
m_unlockState.initializeUnlockState();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
registerForEvents();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
||||||
initializeSubsystems();
|
initializeSubsystems();
|
||||||
|
|
||||||
initializeUnlockState();
|
m_unlockState.initializeUnlockState();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +110,7 @@ void Simulation::initializeSubsystems()
|
|||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||||
[this](const std::string& id, QVector2D pos,
|
[this](const std::string& id, QVector2D pos,
|
||||||
const std::optional<ShipLayoutConfig>& layout) {
|
const std::optional<ShipLayoutConfig>& layout) {
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
if (!isSchematicUnlocked(id))
|
||||||
m_schematicLevels.find(id);
|
|
||||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,55 +129,6 @@ void Simulation::initializeSubsystems()
|
|||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
m_combatSystem = std::make_unique<CombatSystem>(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
|
// tick
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -613,9 +562,9 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
std::vector<const UnlockGroupDef*> pool;
|
std::vector<const UnlockGroupDef*> pool;
|
||||||
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
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 (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
||||||
if (!prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
if (!m_unlockState.prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
||||||
pool.push_back(&group);
|
pool.push_back(&group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,7 +588,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
||||||
std::swap(pool[rollIdx], pool[endIdx]);
|
std::swap(pool[rollIdx], pool[endIdx]);
|
||||||
|
|
||||||
m_pendingSchematicChoices.push_back(makeUnlockOption(*pool[endIdx]));
|
m_pendingSchematicChoices.push_back(m_unlockState.makeUnlockOption(*pool[endIdx]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artifactRolled)
|
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<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
|
||||||
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
|
||||||
std::set<std::string> 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)
|
void Simulation::applySchematicChoice(int choiceIndex)
|
||||||
{
|
{
|
||||||
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
||||||
@@ -709,204 +616,24 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
m_unlockState.awardUnlockGroup(chosen);
|
||||||
// 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_pendingSchematicChoices.clear();
|
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<std::string> Simulation::getUnlockedShipSchematicIds() const
|
|
||||||
{
|
|
||||||
std::set<std::string> ids;
|
|
||||||
for (const auto& [id, state] : m_schematicLevels)
|
|
||||||
{
|
|
||||||
if (state.unlocked) { ids.insert(id); }
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> Simulation::getUnlockedModuleSchematicIds() const
|
|
||||||
{
|
|
||||||
std::set<std::string> ids;
|
|
||||||
for (const auto& [id, state] : m_moduleSchematicLevels)
|
|
||||||
{
|
|
||||||
if (state.unlocked) { ids.insert(id); }
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::prerequisitesSatisfied(const std::vector<std::string>& 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<std::string>& unlockedShipSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
||||||
const std::set<std::string>& 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<std::string> Simulation::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
|
||||||
{
|
|
||||||
std::vector<std::string> 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
|
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
|
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)
|
// Determinism (see docs/replay_design.md)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void Simulation::appendSchematicMap(Hasher& hasher,
|
|
||||||
const std::map<std::string, SchematicState>& levels)
|
|
||||||
{
|
|
||||||
hasher.append(levels.size());
|
|
||||||
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
|
||||||
{
|
|
||||||
hasher.append(entry.first);
|
|
||||||
hasher.append(entry.second.unlocked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Simulation::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
|
||||||
{
|
|
||||||
hasher.append(ids.size());
|
|
||||||
for (const std::string& id : ids)
|
|
||||||
{
|
|
||||||
hasher.append(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long long Simulation::getRngFingerprint() const
|
unsigned long long Simulation::getRngFingerprint() const
|
||||||
{
|
{
|
||||||
return fingerprintRng(m_rng);
|
return fingerprintRng(m_rng);
|
||||||
@@ -937,13 +664,7 @@ unsigned long long Simulation::computeStateChecksum() const
|
|||||||
hasher.append(getNormalGapRemainingTicks());
|
hasher.append(getNormalGapRemainingTicks());
|
||||||
|
|
||||||
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
||||||
appendSchematicMap(hasher, m_schematicLevels);
|
m_unlockState.appendChecksum(hasher);
|
||||||
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
|
||||||
appendSchematicMap(hasher, m_buildingLevels);
|
|
||||||
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
|
||||||
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
|
||||||
appendStringSet(hasher, m_unlockedRecipeIds);
|
|
||||||
appendStringSet(hasher, m_unlockedItemIds);
|
|
||||||
|
|
||||||
// Subsystems contribute their own state.
|
// Subsystems contribute their own state.
|
||||||
m_buildingSystem->appendChecksum(hasher);
|
m_buildingSystem->appendChecksum(hasher);
|
||||||
@@ -1114,37 +835,17 @@ Tick Simulation::getNormalGapRemainingTicks() const
|
|||||||
|
|
||||||
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
return m_unlockState.isSchematicUnlocked(shipId);
|
||||||
m_schematicLevels.find(shipId);
|
|
||||||
if (it == m_schematicLevels.end())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
return m_unlockState.isModuleSchematicUnlocked(moduleId);
|
||||||
m_moduleSchematicLevels.find(moduleId);
|
|
||||||
if (it == m_moduleSchematicLevels.end())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
||||||
{
|
{
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
return m_unlockState.isBuildingUnlocked(type);
|
||||||
if (def == nullptr)
|
|
||||||
{
|
|
||||||
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
|
||||||
m_buildingLevels.find(def->id);
|
|
||||||
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <set>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -22,6 +20,7 @@
|
|||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TracePrintRequestedEvent.h"
|
#include "TracePrintRequestedEvent.h"
|
||||||
|
#include "UnlockState.h"
|
||||||
|
|
||||||
class AiSystem;
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
@@ -203,69 +202,11 @@ private:
|
|||||||
entt::entity m_playerStation2Entity;
|
entt::entity m_playerStation2Entity;
|
||||||
entt::entity m_currentEnemyStationEntities[2];
|
entt::entity m_currentEnemyStationEntities[2];
|
||||||
|
|
||||||
// Schematic unlock state (REQ-DEF-SCHEMATIC-DROP).
|
// Schematic/unlock bookkeeping (REQ-DEF-SCHEMATIC-DROP, REQ-LOCK-EXPLICIT,
|
||||||
struct SchematicState
|
// REQ-LOCK-IMPLICIT, REQ-LOCK-BUILDING, REQ-LOCK-PREREQ). Constructed before
|
||||||
{
|
// initializeSubsystems() runs since BuildingSystem's spawn-gating lambda
|
||||||
bool unlocked;
|
// calls into it (see initializeSubsystems()).
|
||||||
};
|
UnlockState m_unlockState;
|
||||||
std::map<std::string, SchematicState> m_schematicLevels;
|
|
||||||
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
|
||||||
std::map<std::string, SchematicState> m_buildingLevels;
|
|
||||||
|
|
||||||
// Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids.
|
|
||||||
std::set<std::string> 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<std::string> m_grantedShipIds;
|
|
||||||
std::set<std::string> m_grantedModuleIds;
|
|
||||||
std::set<std::string> m_grantedBuildingIds;
|
|
||||||
std::set<std::string> 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<std::string, SchematicState>& levels);
|
|
||||||
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
|
|
||||||
|
|
||||||
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
|
|
||||||
std::set<std::string> m_unlockedRecipeSchematicIds;
|
|
||||||
|
|
||||||
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
|
|
||||||
std::set<std::string> m_unlockedRecipeIds;
|
|
||||||
std::set<std::string> 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<std::string> itemIds;
|
|
||||||
std::set<std::string> recipeIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets.
|
|
||||||
UnlockedSets computeUnlockedSets(const std::set<std::string>& unlockedShipSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedRecipeSchematicIds) const;
|
|
||||||
|
|
||||||
// Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels.
|
|
||||||
std::set<std::string> getUnlockedShipSchematicIds() const;
|
|
||||||
std::set<std::string> getUnlockedModuleSchematicIds() const;
|
|
||||||
|
|
||||||
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
|
|
||||||
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
|
|
||||||
|
|
||||||
// Ids (sorted alphabetically by display name) of the recipes in
|
|
||||||
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
|
||||||
std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
|
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
|
|||||||
Reference in New Issue
Block a user