#include "build_data_persistence.h" #include #include #include #include "../../../../core/ui/busy/busy.h" #include "../../abilities/ability_bucket_catalog.h" #include "../../bounties/bounty_catalog.h" #include "../../cache/internal.h" #include "../../cache/records/validation.h" #include "../../collectibles/collectible_catalog.h" #include "../../constants/investment_constant_catalog.h" #include "../../hash_names/hash_name_catalog.h" #include "../../inventory/buckets/inventory_bucket_catalog.h" #include "../../items/catalysts/exotic_catalyst_catalog.h" #include "../../items/details/item_detail_catalog.h" #include "../../items/socket_plugs/socket_plug_catalog.h" #include "../../material_requirements/material_requirement_catalog.h" #include "../../nodes/node_catalog.h" #include "../../progressions/progression_catalog.h" #include "../../records/record_catalog.h" #include "../../rewards/reward_catalog.h" #include "../../runtime.h" #include "../../scenarios/scenario_catalog.h" #include "../../season_pass/season_pass_catalog.h" #include "../../sobjects/sobject_catalog.h" #include "../../socket_entry_lists/socket_entry_list_catalog.h" #include "../../spawn_sets/spawn_set_catalog.h" #include "../../vendors/vendor_catalog.h" #include "../domain_markers.h" namespace sunrise::state::build_data::runtime::persistence { namespace { Context g_context; /** * Lazily sizes one bounded cache-snapshot bank and exposes all rows. * @param storage Bank held by the caller's context, resized on first use. * @return The whole bank. */ template [[nodiscard]] std::span ensure_scratch(std::vector& storage) noexcept { if (storage.size() != Capacity) { storage.assign(Capacity, Value{}); } return storage; } /** @param value Published runtime constants. @return The packed header form. */ [[nodiscard]] cache::records::InvestmentConstants to_record(const constants::InvestmentConstants& value) noexcept { return {value.lightStatRow, value.weaponPowerStatRow, value.characterStatRows, value.extracted ? std::uint8_t{1} : std::uint8_t{0}}; } /** * Copies all generated catalogs into fixed scratch storage. * @param state Its lock must already be held exclusively. * @param counts Receives every copied row count. * @return True when every snapshot fits and the canonical sort works. */ [[nodiscard]] bool snapshot_domains_locked(Context& state, cache::records::DomainCounts& counts) noexcept { counts = {}; const cache::records::MutableDomains scratch = scratch_domains(state); *scratch.constants = to_record(constants::snapshot()); gameplay::entity_object_types::Fingerprint objectFingerprint{}; return content::snapshot(scratch.named, counts.named) && items::snapshot(scratch.items, counts.items) && collectibles::snapshot(scratch.collectibles, counts.collectibles) && material_requirements::snapshot(scratch.materialRequirementSets, counts.materialRequirementSets) && items::details::snapshot(scratch.itemDetails, counts.itemDetails) && items::socket_plugs::snapshot(scratch.socketPlugRules, counts.socketPlugRules, scratch.socketPlugPools, counts.socketPlugPools, scratch.socketPlugMembers, counts.socketPlugMembers) && items::catalysts::snapshot(scratch.exoticCatalysts, counts.exoticCatalysts) && inventory::buckets::snapshot(scratch.inventoryBuckets, counts.inventoryBuckets) && socket_entry_lists::snapshot(scratch.socketEntryLists, counts.socketEntryLists) && socket_entry_lists::snapshot_entry_tables(scratch.socketEntryTables, counts.socketEntryTables) && abilities::snapshot(scratch.abilityBuckets, counts.abilityBuckets) && progressions::snapshot(scratch.progressions, counts.progressions) && progressions::snapshot_steps(scratch.progressionSteps, counts.progressionSteps) && season_pass::snapshot(scratch.seasonPassRewards, counts.seasonPassRewards) && bounties::snapshot(scratch.bounties, counts.bounties) && rewards::snapshot(scratch.rewardPools, counts.rewardPools) && rewards::snapshot(scratch.rewardEntries, counts.rewardEntries) && rewards::snapshot(scratch.rewardItems, counts.rewardItems) && rewards::snapshot(scratch.rewardInstructions, counts.rewardInstructions) && rewards::snapshot(scratch.rewardModifiers, counts.rewardModifiers) && rewards::snapshot(scratch.rewardSockets, counts.rewardSockets) && records::snapshot(scratch.records, counts.records) && records::snapshot_objectives(scratch.recordObjectives, counts.recordObjectives) && records::snapshot_intervals(scratch.recordIntervals, counts.recordIntervals) && records::snapshot_rewards(scratch.recordRewards, counts.recordRewards) && nodes::snapshot(scratch.nodes, counts.nodes) && sobjects::snapshot(scratch.sobjects, counts.sobjects) && scenarios::snapshot(scratch.scenarios, counts.scenarios) && scenarios::snapshot_groups(scratch.rosterGroups, counts.rosterGroups) && spawn_sets::snapshot(scratch.spawnStems, counts.spawnStems) && spawn_sets::snapshot_hashes(scratch.spawnNameHashes, counts.spawnNameHashes) && spawn_sets::snapshot_points(scratch.spawnPoints, counts.spawnPoints) && hash_names::snapshot(scratch.hashNames, counts.hashNames) && vendors::snapshot_index(scratch.vendorIndex, counts.vendorIndex) && vendors::snapshot_definitions(scratch.vendorDefinitions, counts.vendorDefinitions) && vendors::snapshot_sale_rows(scratch.vendorSaleRows, counts.vendorSaleRows) && vendors::snapshot_installed_rows(scratch.vendorInstalledRows, counts.vendorInstalledRows) && gameplay::entity_position_profiles::snapshot( scratch.positionProfiles, counts.positionProfiles, *scratch.positionFingerprint) && gameplay::entity_object_types::snapshot( scratch.objectTypes, counts.objectTypes, objectFingerprint) && objectFingerprint == *scratch.positionFingerprint && cache::records::canonicalize(scratch, counts); } /** @return True when every required extracted domain is complete in State. */ [[nodiscard]] bool required_domains_ready() noexcept { constants::InvestmentConstants published{}; return runtime::named::ready() && item_definitions_ready() && configured_item_details_ready() && collectible_definitions_ready() && socket_plug_rules_ready() && material_requirement_sets_ready() && inventory_bucket_descriptors_ready() && socket_entry_lists_ready() && ability_buckets_ready() && progression_definitions_ready() && season_pass_ready() && repeatable_bounties_ready() && rewards::ready() && record_definitions_ready() && node_definitions_ready() && sobject_definitions_ready() && scenario_layouts_ready() && spawn_sets_ready() && hash_names_ready() && vendor_catalog_ready() && gameplay::entity_position_profiles::available() && gameplay::entity_object_types::available() && constants::find(published); } } // namespace /** @return The process-wide persistence context, shared by lifecycle and writer code. */ Context& context() noexcept { return g_context; } /** Gives mutable views over every fixed snapshot buffer. */ cache::records::MutableDomains scratch_domains(Context& state) noexcept { const auto named = ensure_scratch( state.namedScratch); const auto items = ensure_scratch( state.itemScratch); const auto collectibles = ensure_scratch(state.collectibleScratch); const auto materialRequirementSets = ensure_scratch( state.materialRequirementSetScratch); const auto itemDetails = ensure_scratch(state.itemDetailScratch); const auto socketPlugRules = ensure_scratch(state.socketPlugRuleScratch); const auto socketPlugPools = ensure_scratch(state.socketPlugPoolScratch); const auto socketPlugMembers = ensure_scratch( state.socketPlugMemberScratch); const auto exoticCatalysts = ensure_scratch( state.exoticCatalystScratch); const auto inventoryBuckets = ensure_scratch( state.inventoryBucketScratch); const auto socketEntryLists = ensure_scratch( state.socketEntryListScratch); const auto socketEntryTables = ensure_scratch( state.socketEntryTableScratch); const auto abilityBuckets = ensure_scratch( state.abilityBucketScratch); const auto progressions = ensure_scratch( state.progressionScratch); const auto recordRows = ensure_scratch(state.recordScratch); const auto recordObjectives = ensure_scratch( state.recordObjectiveScratch); const auto recordIntervals = ensure_scratch(state.recordIntervalScratch); const auto recordRewards = ensure_scratch(state.recordRewardScratch); const auto nodeRows = ensure_scratch(state.nodeScratch); const auto sobjectRows = ensure_scratch(state.sobjectScratch); const auto scenarios = ensure_scratch( state.scenarioScratch); const auto rosterGroups = ensure_scratch( state.rosterGroupScratch); const auto spawnStems = ensure_scratch(state.spawnStemScratch); const auto spawnNameHashes = ensure_scratch( state.spawnNameHashScratch); const auto spawnPoints = ensure_scratch(state.spawnPointScratch); const auto hashNames = ensure_scratch(state.hashNameScratch); const auto vendorIndex = ensure_scratch(state.vendorIndexScratch); const auto vendorDefinitions = ensure_scratch( state.vendorDefinitionScratch); const auto vendorSaleRows = ensure_scratch(state.vendorSaleRowScratch); const auto vendorInstalledRows = ensure_scratch( state.vendorInstalledRowScratch); const auto progressionSteps = ensure_scratch( state.progressionStepScratch); const auto seasonPassRewards = ensure_scratch( state.seasonPassRewardScratch); const auto bountyRows = ensure_scratch(state.bountyScratch); const auto positionProfiles = ensure_scratch( state.positionProfileScratch); return { &state.constantsScratch, named, items, collectibles, materialRequirementSets, itemDetails, socketPlugRules, socketPlugPools, socketPlugMembers, exoticCatalysts, inventoryBuckets, socketEntryLists, socketEntryTables, abilityBuckets, progressions, recordRows, nodeRows, sobjectRows, scenarios, rosterGroups, spawnStems, spawnNameHashes, spawnPoints, hashNames, vendorIndex, vendorDefinitions, vendorSaleRows, vendorInstalledRows, positionProfiles, &state.positionFingerprint, ensure_scratch(state.objectTypeScratch), recordObjectives, recordIntervals, recordRewards, progressionSteps, seasonPassRewards, bountyRows, ensure_scratch(state.rewardPoolsScratch), ensure_scratch(state.rewardEntriesScratch), ensure_scratch(state.rewardItemsScratch), ensure_scratch( state.rewardInstructionsScratch), ensure_scratch(state.rewardModifiersScratch), ensure_scratch( state.rewardSocketsScratch), }; } /** * Releases one transient cache snapshot bank without walking its capacity. * @param storage Bank emptied and handed back to the allocator. */ template static void release_bank(std::vector& storage) noexcept { std::vector{}.swap(storage); } /** Releases transient cache snapshot banks without allocating or walking their capacities. */ void release_scratch_locked(Context& state) noexcept { release_bank(state.namedScratch); release_bank(state.itemScratch); release_bank(state.collectibleScratch); release_bank(state.materialRequirementSetScratch); release_bank(state.itemDetailScratch); release_bank(state.socketPlugRuleScratch); release_bank(state.socketPlugPoolScratch); release_bank(state.socketPlugMemberScratch); release_bank(state.exoticCatalystScratch); release_bank(state.inventoryBucketScratch); release_bank(state.socketEntryListScratch); release_bank(state.socketEntryTableScratch); release_bank(state.abilityBucketScratch); release_bank(state.progressionScratch); release_bank(state.progressionStepScratch); release_bank(state.seasonPassRewardScratch); release_bank(state.bountyScratch); release_bank(state.rewardPoolsScratch); release_bank(state.rewardEntriesScratch); release_bank(state.rewardItemsScratch); release_bank(state.rewardInstructionsScratch); release_bank(state.rewardModifiersScratch); release_bank(state.rewardSocketsScratch); release_bank(state.recordScratch); release_bank(state.recordObjectiveScratch); release_bank(state.recordIntervalScratch); release_bank(state.recordRewardScratch); release_bank(state.nodeScratch); release_bank(state.sobjectScratch); release_bank(state.scenarioScratch); release_bank(state.rosterGroupScratch); release_bank(state.spawnStemScratch); release_bank(state.spawnNameHashScratch); release_bank(state.spawnPointScratch); release_bank(state.hashNameScratch); release_bank(state.vendorIndexScratch); release_bank(state.vendorDefinitionScratch); release_bank(state.vendorSaleRowScratch); release_bank(state.vendorInstalledRowScratch); release_bank(state.positionProfileScratch); release_bank(state.objectTypeScratch); state.positionFingerprint = {}; state.constantsScratch = {}; } /** Clears fixed cache paths, identity, flags, and snapshot storage. */ void clear_locked(Context& state) noexcept { release_scratch_locked(state); state.cacheDirectory = {}; state.cachePath = {}; state.buildIdentity = {}; state.catalystError = items::catalysts::Error::none; state.enabled = false; state.persisted = false; state.replaceStaleCache = false; } /** Gives read-only views over the used rows of one canonical snapshot. */ cache::records::Domains occupied_domains(Context& state, const cache::records::DomainCounts& counts) noexcept { const std::span itemDetails{state.itemDetailScratch.data(), counts.itemDetails}; const std::span socketPlugRules{ state.socketPlugRuleScratch.data(), counts.socketPlugRules}; const std::span socketPlugPools{ state.socketPlugPoolScratch.data(), counts.socketPlugPools}; const std::span socketPlugMembers{ state.socketPlugMemberScratch.data(), counts.socketPlugMembers}; const std::span exoticCatalysts{ state.exoticCatalystScratch.data(), counts.exoticCatalysts}; return { state.constantsScratch, std::span{state.namedScratch.data(), counts.named}, std::span{state.itemScratch.data(), counts.items}, std::span{state.collectibleScratch.data(), counts.collectibles}, std::span{ state.materialRequirementSetScratch.data(), counts.materialRequirementSets}, itemDetails, socketPlugRules, socketPlugPools, socketPlugMembers, exoticCatalysts, std::span{state.inventoryBucketScratch.data(), counts.inventoryBuckets}, std::span{state.socketEntryListScratch.data(), counts.socketEntryLists}, std::span{state.socketEntryTableScratch.data(), counts.socketEntryTables}, std::span{state.abilityBucketScratch.data(), counts.abilityBuckets}, std::span{state.progressionScratch.data(), counts.progressions}, std::span{state.recordScratch.data(), counts.records}, std::span{state.nodeScratch.data(), counts.nodes}, std::span{state.sobjectScratch.data(), counts.sobjects}, std::span{state.scenarioScratch.data(), counts.scenarios}, std::span{state.rosterGroupScratch.data(), counts.rosterGroups}, std::span{state.spawnStemScratch.data(), counts.spawnStems}, std::span{state.spawnNameHashScratch.data(), counts.spawnNameHashes}, std::span{state.spawnPointScratch.data(), counts.spawnPoints}, std::span{state.hashNameScratch.data(), counts.hashNames}, std::span{state.vendorIndexScratch.data(), counts.vendorIndex}, std::span{state.vendorDefinitionScratch.data(), counts.vendorDefinitions}, std::span{state.vendorSaleRowScratch.data(), counts.vendorSaleRows}, std::span{state.vendorInstalledRowScratch.data(), counts.vendorInstalledRows}, std::span{ state.positionProfileScratch.data(), counts.positionProfiles}, state.positionFingerprint, std::span{state.objectTypeScratch.data(), counts.objectTypes}, std::span{state.recordObjectiveScratch.data(), counts.recordObjectives}, std::span{state.recordIntervalScratch.data(), counts.recordIntervals}, std::span{state.recordRewardScratch.data(), counts.recordRewards}, std::span{state.progressionStepScratch.data(), counts.progressionSteps}, std::span{state.seasonPassRewardScratch.data(), counts.seasonPassRewards}, std::span{state.bountyScratch.data(), counts.bounties}, std::span{state.rewardPoolsScratch.data(), counts.rewardPools}, std::span{state.rewardEntriesScratch.data(), counts.rewardEntries}, std::span{state.rewardItemsScratch.data(), counts.rewardItems}, std::span{state.rewardInstructionsScratch.data(), counts.rewardInstructions}, std::span{state.rewardModifiersScratch.data(), counts.rewardModifiers}, std::span{state.rewardSocketsScratch.data(), counts.rewardSockets}, }; } /** Saves one canonical snapshot when all domains required by its build are ready. */ bool persist_if_ready_locked(Context& state, bool catalystRequired) noexcept { if (!required_domains_ready() || (catalystRequired && !exotic_catalysts_ready()) || state.persisted || !state.enabled) { return true; } cache::records::DomainCounts counts{}; // One canonical snapshot keeps header counts and payload rows in the same commit. if (!snapshot_domains_locked(state, counts)) { return false; } // The write flushes every domain to disk once. It is the only stall here, so the overlay // covers just it. core::ui::busy::begin(core::ui::busy::Task::cacheWrite); const bool written = cache::write(state.cacheDirectory.chars.data(), state.cachePath.chars.data(), state.buildIdentity, occupied_domains(state, counts), state.replaceStaleCache ? cache::WriteDisposition::replaceStale : cache::WriteDisposition::createOnly); core::ui::busy::end(core::ui::busy::Task::cacheWrite); if (!written) { return false; } state.persisted = true; state.replaceStaleCache = false; release_scratch_locked(state); return true; } } // namespace sunrise::state::build_data::runtime::persistence namespace sunrise::state::build_data { /** Newly extracted package data replaces the shared cache only after all domains are ready. */ void invalidate_cache() noexcept { auto& state = runtime::persistence::context(); AcquireSRWLockExclusive(&state.lock); state.persisted = false; state.replaceStaleCache = true; ReleaseSRWLockExclusive(&state.lock); } /** @return True when required domains are ready and any safe cache write works. */ bool persist() noexcept { runtime::persistence::Context& state = runtime::persistence::context(); AcquireSRWLockExclusive(&state.lock); const bool requiredReady = runtime::persistence::required_domains_ready(); bool result = false; switch (runtime::persistence::cache_action( requiredReady, exotic_catalysts_ready(), state.catalystError)) { case runtime::persistence::CacheAction::waitForDomains: break; case runtime::persistence::CacheAction::writeRequiredDomains: // Unsupported catalyst facts do not prevent the other build-bound domains from caching. result = runtime::persistence::persist_if_ready_locked(state, false); break; case runtime::persistence::CacheAction::writeCompleteCache: result = runtime::persistence::persist_if_ready_locked(state, true); break; } ReleaseSRWLockExclusive(&state.lock); return result; } } // namespace sunrise::state::build_data