Просмотр исходного кода

Cache the records domain so claims survive a warm boot

The records domain was built only by the package extraction pass, and that pass is skipped once the
cache is warm. So a claim worked on the boot after a cache rebuild and failed on every boot after
it, with the domain empty and every request refused on record_definition.

Write the domain to the cache the way every other one is written: a disk row, its codec, the
checksum and write, the read and size accounting, the capacity check, the header count, the
validation, a scratch bank, a snapshot, and the install on load.

Two things worth knowing for the next domain added here. Inside the cache the namespace is
cache::records, so an unqualified records:: resolves to the cache itself rather than to the
build_data domain and every reference needs qualifying. And the header's static assertion counts its
uint32_t fields one by one, so a new count field has to be added there too, which is what stops a
header silently gaining a field.

The cache format version moves to 45. Do not roll back past this commit without deleting
build_data.bin: a cache written by a newer version is rejected rather than rebuilt, and the client
reports that as "verify integrity of game files".
Millie 2 недель назад
Родитель
Сommit
bf1292751b

+ 2 - 0
Sunrise/src/state/build_data/build_data_runtime.cpp

@@ -16,6 +16,7 @@
 #include "items/socket_plugs/socket_plug_catalog.h"
 #include "material_requirements/material_requirement_catalog.h"
 #include "progressions/progression_catalog.h"
+#include "records/record_catalog.h"
 #include "runtime.h"
 #include "runtime/build_data_catalog_runtime.h"
 #include "runtime/domain_markers.h"
@@ -108,6 +109,7 @@ bool initialize(void* module, std::uint64_t configuredEquipmentHash) noexcept {
             domains.socketPlugRules, domains.socketPlugPools, domains.socketPlugMembers)
         || !abilities::replace(domains.abilityBuckets)
         || !progressions::replace(domains.progressions)
+        || !records::replace(domains.records)
         // The layouts are what activity message 1 reads. Without them a cache hit makes the
         // other domains ready, the package build skips itself, and every destination falls back.
         || !scenarios::replace(domains.scenarios, domains.rosterGroups)

+ 2 - 0
Sunrise/src/state/build_data/cache/read/cache_file_reader.cpp

@@ -32,6 +32,7 @@ namespace {
            && counts.socketEntryTables <= output.socketEntryTables.size()
            && counts.abilityBuckets <= output.abilityBuckets.size()
            && counts.progressions <= output.progressions.size()
+           && counts.records <= output.records.size()
            && counts.scenarios <= output.scenarios.size()
            && counts.rosterGroups <= output.rosterGroups.size()
            && counts.spawnStems <= output.spawnStems.size()
@@ -60,6 +61,7 @@ namespace {
         header.socketEntryTableCount,
         header.abilityBucketCount,
         header.progressionCount,
+        header.recordCount,
         header.scenarioCount,
         header.rosterGroupCount,
         header.spawnStemCount,

+ 6 - 0
Sunrise/src/state/build_data/cache/read/cache_payload_reader.cpp

@@ -85,6 +85,7 @@ void clear(records::MutableDomains output) noexcept {
               socket_entry_lists::EntryTable{});
     std::fill(output.abilityBuckets.begin(), output.abilityBuckets.end(), abilities::Definition{});
     std::fill(output.progressions.begin(), output.progressions.end(), progressions::Definition{});
+    std::fill(output.records.begin(), output.records.end(), build_data::records::Definition{});
     std::fill(output.scenarios.begin(), output.scenarios.end(), scenarios::Definition{});
     std::fill(output.rosterGroups.begin(), output.rosterGroups.end(), scenarios::RosterGroup{});
     std::fill(output.spawnStems.begin(), output.spawnStems.end(), spawn_sets::Stem{});
@@ -117,6 +118,7 @@ bool expected_size(const records::DomainCounts& counts, std::uint64_t& size) noe
            && add_records(counts.socketEntryTables, sizeof(records::SocketEntryTableRecord), size)
            && add_records(counts.abilityBuckets, sizeof(records::AbilityBucketRecord), size)
            && add_records(counts.progressions, sizeof(records::ProgressionRecord), size)
+           && add_records(counts.records, sizeof(records::RecordDefinitionRecord), size)
            && add_records(counts.scenarios, sizeof(records::ScenarioRecord), size)
            && add_records(counts.rosterGroups, sizeof(records::RosterGroupRecord), size)
            && add_records(counts.spawnStems, sizeof(records::SpawnStemRecord), size)
@@ -175,6 +177,9 @@ bool read_payload(HANDLE file,
     valid = valid
             && read_domain<records::ProgressionRecord>(
                 file, output.progressions.first(counts.progressions), checksum);
+    valid = valid
+            && read_domain<records::RecordDefinitionRecord>(
+                file, output.records.first(counts.records), checksum);
     valid = valid
             && read_domain<records::ScenarioRecord>(
                 file, output.scenarios.first(counts.scenarios), checksum);
@@ -223,6 +228,7 @@ bool read_payload(HANDLE file,
         output.socketEntryTables.first(counts.socketEntryTables),
         output.abilityBuckets.first(counts.abilityBuckets),
         output.progressions.first(counts.progressions),
+        output.records.first(counts.records),
         output.scenarios.first(counts.scenarios),
         output.rosterGroups.first(counts.rosterGroups),
         output.spawnStems.first(counts.spawnStems),

+ 3 - 0
Sunrise/src/state/build_data/cache/records/cache_domain_validation.cpp

@@ -11,6 +11,7 @@
 #include "../../items/socket_plugs/socket_plug_catalog.h"
 #include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/progression_catalog.h"
+#include "../../records/record_catalog.h"
 #include "../../scenarios/scenario_catalog.h"
 #include "../../socket_entry_lists/socket_entry_list_catalog.h"
 #include "../../spawn_sets/spawn_set_catalog.h"
@@ -145,6 +146,7 @@ template <typename Value, typename Less>
            && counts.socketEntryTables <= domains.socketEntryTables.size()
            && counts.abilityBuckets <= domains.abilityBuckets.size()
            && counts.progressions <= domains.progressions.size()
+           && counts.records <= domains.records.size()
            && counts.scenarios <= domains.scenarios.size()
            && counts.rosterGroups <= domains.rosterGroups.size()
            && counts.spawnStems <= domains.spawnStems.size()
@@ -217,6 +219,7 @@ bool valid_domains(Domains domains) noexcept {
         || !abilities::valid(domains.abilityBuckets)
         || !strictly_ordered(domains.abilityBuckets, ability_less)
         || !progressions::valid(domains.progressions)
+        || !build_data::records::valid(domains.records)
         || !scenarios::valid(domains.scenarios, domains.rosterGroups)
         // An empty catalog is complete. It is what a build with no installed spawn set means.
         // Both arrays must be empty together, because a stem names its hashes by range.

+ 23 - 0
Sunrise/src/state/build_data/cache/records/cache_investment_records.cpp

@@ -78,4 +78,27 @@ bool decode(const ProgressionRecord& record, progressions::Definition& value) no
     return true;
 }
 
+/** Encodes one record with its padding zeroed. */
+bool encode(const build_data::records::Definition& value,
+            RecordDefinitionRecord& record) noexcept {
+    record = {
+        value.definitionIndex,
+        value.completionFlagIndex,
+        value.scoreValue,
+        kReservedFieldValue,
+    };
+    return true;
+}
+
+/** Decodes one record after checking its padding. */
+bool decode(const RecordDefinitionRecord& record,
+            build_data::records::Definition& value) noexcept {
+    value = {};
+    if (record.reserved != kReservedFieldValue) {
+        return false;
+    }
+    value = {record.definitionIndex, record.completionFlagIndex, record.scoreValue};
+    return true;
+}
+
 } // namespace sunrise::state::build_data::cache::records

+ 16 - 0
Sunrise/src/state/build_data/cache/records/codec.h

@@ -215,6 +215,22 @@ namespace sunrise::state::build_data::cache::records {
 [[nodiscard]] bool decode(const ProgressionRecord& record,
                           progressions::Definition& value) noexcept;
 
+/**
+ * @param value Runtime row to pack.
+ * @param record Receives the packed disk row.
+ * @return Always true.
+ */
+[[nodiscard]] bool encode(const build_data::records::Definition& value,
+                          RecordDefinitionRecord& record) noexcept;
+
+/**
+ * @param record Packed disk row.
+ * @param value Receives the runtime row.
+ * @return True when the disk row is in standard form.
+ */
+[[nodiscard]] bool decode(const RecordDefinitionRecord& record,
+                          build_data::records::Definition& value) noexcept;
+
 /**
  * @param value Runtime row to pack.
  * @param record Receives the packed disk row.

+ 4 - 0
Sunrise/src/state/build_data/cache/records/domains.h

@@ -13,6 +13,7 @@
 #include "../../items/socket_plugs/definition.h"
 #include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/definition.h"
+#include "../../records/definition.h"
 #include "../../scenarios/definition.h"
 #include "../../socket_entry_lists/definition.h"
 #include "../../spawn_sets/definition.h"
@@ -36,6 +37,7 @@ struct DomainCounts {
     std::size_t socketEntryTables{};
     std::size_t abilityBuckets{};
     std::size_t progressions{};
+    std::size_t records{};
     std::size_t scenarios{};
     std::size_t rosterGroups{};
     std::size_t spawnStems{};
@@ -65,6 +67,7 @@ struct MutableDomains {
     std::span<socket_entry_lists::EntryTable> socketEntryTables;
     std::span<abilities::Definition> abilityBuckets;
     std::span<progressions::Definition> progressions;
+    std::span<build_data::records::Definition> records;
     std::span<scenarios::Definition> scenarios;
     std::span<scenarios::RosterGroup> rosterGroups;
     std::span<spawn_sets::Stem> spawnStems;
@@ -93,6 +96,7 @@ struct Domains {
     std::span<const socket_entry_lists::EntryTable> socketEntryTables;
     std::span<const abilities::Definition> abilityBuckets;
     std::span<const progressions::Definition> progressions;
+    std::span<const build_data::records::Definition> records;
     std::span<const scenarios::Definition> scenarios;
     std::span<const scenarios::RosterGroup> rosterGroups;
     std::span<const spawn_sets::Stem> spawnStems;

+ 14 - 2
Sunrise/src/state/build_data/cache/records/format.h

@@ -16,6 +16,7 @@
 #include "../../items/socket_plugs/definition.h"
 #include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/definition.h"
+#include "../../records/definition.h"
 #include "../../scenarios/definition.h"
 #include "../../spawn_sets/definition.h"
 #include "../../vendors/definition.h"
@@ -29,7 +30,7 @@ inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', '
  * Bump it when a stored shape changes, and when the extraction filling it changes what it writes.
  * A cached row survives a code change, so a corrected walk keeps publishing the old rows.
  */
-inline constexpr std::uint32_t kCacheFormatVersion = 44;
+inline constexpr std::uint32_t kCacheFormatVersion = 45;
 /** Signed -1 on disk means there is no equipment slot. */
 inline constexpr std::int8_t kAbsentEquipmentSlot = -1;
 /** The standard 64-bit FNV-1a offset basis starts the payload checksum. */
@@ -78,6 +79,7 @@ struct Header {
     std::uint32_t socketEntryTableCount{};
     std::uint32_t abilityBucketCount{};
     std::uint32_t progressionCount{};
+    std::uint32_t recordCount{};
     std::uint32_t scenarioCount{};
     std::uint32_t rosterGroupCount{};
     std::uint32_t spawnStemCount{};
@@ -236,6 +238,15 @@ struct ProgressionRecord {
     std::uint8_t reserved{};
 };
 
+/** Disk form of one record and the account flag bank row its claim sets. */
+struct RecordDefinitionRecord {
+    std::uint16_t definitionIndex{};
+    std::uint16_t completionFlagIndex{};
+    std::uint16_t scoreValue{};
+    /** Must be zero, so the packed record row always matches. */
+    std::uint16_t reserved{};
+};
+
 /** Disk form of one dense socket-entry-list definition. */
 struct SocketEntryListRecord {
     std::uint32_t definitionHash{};
@@ -422,7 +433,7 @@ static_assert(sizeof(Prefix) == kCacheMagic.size() + sizeof(std::uint32_t));
 static_assert(sizeof(InvestmentConstants)
               == constants::kCharacterStatRowCount + 2 * sizeof(std::uint8_t));
 static_assert(sizeof(Header)
-              == kCacheMagic.size() + 26 * sizeof(std::uint32_t) + 2 * sizeof(std::uint64_t)
+              == kCacheMagic.size() + 27 * sizeof(std::uint32_t) + 2 * sizeof(std::uint64_t)
                      + sizeof(InvestmentConstants));
 static_assert(sizeof(SpawnPointRecord)
               == spawn_sets::kPositionComponents * sizeof(float) + sizeof(std::uint32_t)
@@ -459,6 +470,7 @@ static_assert(sizeof(RosterGroupRecord)
                      + 2 * scenarios::kRosterSlotCapacity * sizeof(std::uint8_t)
                      + scenarios::kRosterSlotCapacity * sizeof(std::uint16_t));
 static_assert(sizeof(ProgressionRecord) == sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t));
+static_assert(sizeof(RecordDefinitionRecord) == 4 * sizeof(std::uint16_t));
 static_assert(sizeof(AbilityBucketRecord)
               == sizeof(std::uint16_t) + 6 * sizeof(std::uint8_t)
                      + 2 * abilities::kBucketCapacity * sizeof(std::uint8_t)

+ 1 - 0
Sunrise/src/state/build_data/cache/write/cache_file_writer.cpp

@@ -29,6 +29,7 @@ namespace {
            && domains.socketEntryTables.size() <= kMaximumCount
            && domains.abilityBuckets.size() <= kMaximumCount
            && domains.progressions.size() <= kMaximumCount
+           && domains.records.size() <= kMaximumCount
            && domains.scenarios.size() <= kMaximumCount
            && domains.rosterGroups.size() <= kMaximumCount
            && domains.vendorIndex.size() <= kMaximumCount

+ 2 - 0
Sunrise/src/state/build_data/cache/write/cache_payload_writer.cpp

@@ -77,6 +77,7 @@ bool payload_checksum(records::Domains domains, std::uint64_t& checksum) noexcep
            && checksum_domain<records::SocketEntryTableRecord>(domains.socketEntryTables, checksum)
            && checksum_domain<records::AbilityBucketRecord>(domains.abilityBuckets, checksum)
            && checksum_domain<records::ProgressionRecord>(domains.progressions, checksum)
+           && checksum_domain<records::RecordDefinitionRecord>(domains.records, checksum)
            && checksum_domain<records::ScenarioRecord>(domains.scenarios, checksum)
            && checksum_domain<records::RosterGroupRecord>(domains.rosterGroups, checksum)
            && checksum_domain<records::SpawnStemRecord>(domains.spawnStems, checksum)
@@ -106,6 +107,7 @@ bool write_payload(HANDLE file, records::Domains domains) noexcept {
            && write_domain<records::SocketEntryTableRecord>(file, domains.socketEntryTables)
            && write_domain<records::AbilityBucketRecord>(file, domains.abilityBuckets)
            && write_domain<records::ProgressionRecord>(file, domains.progressions)
+           && write_domain<records::RecordDefinitionRecord>(file, domains.records)
            && write_domain<records::ScenarioRecord>(file, domains.scenarios)
            && write_domain<records::RosterGroupRecord>(file, domains.rosterGroups)
            && write_domain<records::SpawnStemRecord>(file, domains.spawnStems)

+ 1 - 0
Sunrise/src/state/build_data/cache/write/temporary/temporary_cache_file.cpp

@@ -119,6 +119,7 @@ enum class WriteStatus {
         static_cast<std::uint32_t>(domains.socketEntryTables.size()),
         static_cast<std::uint32_t>(domains.abilityBuckets.size()),
         static_cast<std::uint32_t>(domains.progressions.size()),
+        static_cast<std::uint32_t>(domains.records.size()),
         static_cast<std::uint32_t>(domains.scenarios.size()),
         static_cast<std::uint32_t>(domains.rosterGroups.size()),
         static_cast<std::uint32_t>(domains.spawnStems.size()),

+ 6 - 0
Sunrise/src/state/build_data/records/record_catalog.cpp

@@ -49,6 +49,12 @@ bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
     return true;
 }
 
+/** Copies every row in native record order. */
+bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_definitions.snapshot(output, count);
+}
+
 /** @return Number of generated record definitions, read under the lock. */
 std::size_t count() noexcept {
     const Lock::Shared guard(g_lock);

+ 8 - 0
Sunrise/src/state/build_data/records/record_catalog.h

@@ -32,6 +32,14 @@ void clear() noexcept;
  */
 [[nodiscard]] bool find(std::uint16_t definitionIndex, Definition& definition) noexcept;
 
+/**
+ * Copies every row in native record order.
+ * @param output Caller-owned fixed row storage.
+ * @param count Receives the copied row count, or zero when output is too small.
+ * @return True when output can hold every row.
+ */
+[[nodiscard]] bool snapshot(std::span<Definition> output, std::size_t& count) noexcept;
+
 /** @return Number of generated record definitions, read under the lock. */
 [[nodiscard]] std::size_t count() noexcept;
 

+ 9 - 1
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp

@@ -18,6 +18,7 @@
 #include "../../items/socket_plugs/socket_plug_catalog.h"
 #include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/progression_catalog.h"
+#include "../../records/record_catalog.h"
 #include "../../runtime.h"
 #include "../../scenarios/scenario_catalog.h"
 #include "../../socket_entry_lists/socket_entry_list_catalog.h"
@@ -81,6 +82,7 @@ to_record(const constants::InvestmentConstants& value) noexcept {
                                                         counts.socketEntryTables)
            && abilities::snapshot(scratch.abilityBuckets, counts.abilityBuckets)
            && progressions::snapshot(scratch.progressions, counts.progressions)
+           && records::snapshot(scratch.records, counts.records)
            && scenarios::snapshot(scratch.scenarios, counts.scenarios)
            && scenarios::snapshot_groups(scratch.rosterGroups, counts.rosterGroups)
            && spawn_sets::snapshot(scratch.spawnStems, counts.spawnStems)
@@ -109,7 +111,8 @@ bool all_domains_ready() noexcept {
            && 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() && scenario_layouts_ready() && spawn_sets_ready()
+           && progression_definitions_ready() && record_definitions_ready()
+           && scenario_layouts_ready() && spawn_sets_ready()
            && hash_names_ready() && constants::find(published);
 }
 
@@ -153,6 +156,8 @@ cache::records::MutableDomains scratch_domains(Context& state) noexcept {
     const auto progressions =
         ensure_scratch<progressions::Definition, progressions::kDefinitionCapacity>(
             state.progressionScratch);
+    const auto recordRows = ensure_scratch<records::Definition, records::kDefinitionCapacity>(
+        state.recordScratch);
     const auto scenarios = ensure_scratch<scenarios::Definition, scenarios::kDefinitionCapacity>(
         state.scenarioScratch);
     const auto rosterGroups =
@@ -192,6 +197,7 @@ cache::records::MutableDomains scratch_domains(Context& state) noexcept {
         socketEntryTables,
         abilityBuckets,
         progressions,
+        recordRows,
         scenarios,
         rosterGroups,
         spawnStems,
@@ -229,6 +235,7 @@ void release_scratch_locked(Context& state) noexcept {
     release_bank(state.socketEntryTableScratch);
     release_bank(state.abilityBucketScratch);
     release_bank(state.progressionScratch);
+    release_bank(state.recordScratch);
     release_bank(state.scenarioScratch);
     release_bank(state.rosterGroupScratch);
     release_bank(state.spawnStemScratch);
@@ -286,6 +293,7 @@ cache::records::Domains occupied_domains(Context& state,
                                                counts.abilityBuckets},
         std::span<const progressions::Definition>{state.progressionScratch.data(),
                                                   counts.progressions},
+        std::span<const records::Definition>{state.recordScratch.data(), counts.records},
         std::span<const scenarios::Definition>{state.scenarioScratch.data(), counts.scenarios},
         std::span<const scenarios::RosterGroup>{state.rosterGroupScratch.data(),
                                                 counts.rosterGroups},

+ 1 - 0
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.h

@@ -42,6 +42,7 @@ struct Context {
     std::vector<socket_entry_lists::EntryTable> socketEntryTableScratch{};
     std::vector<abilities::Definition> abilityBucketScratch{};
     std::vector<progressions::Definition> progressionScratch{};
+    std::vector<records::Definition> recordScratch{};
     std::vector<scenarios::Definition> scenarioScratch{};
     std::vector<scenarios::RosterGroup> rosterGroupScratch{};
     std::vector<spawn_sets::Stem> spawnStemScratch{};