Bläddra i källkod

fix(investment): restore catalyst presentation state

Read all positive catalyst effect conditions from build data. Publish legacy objectives and socket the active effect plug. Cache format 51 stores the new fields. This restores gold borders without changing the settings schema.
y9522 2 veckor sedan
förälder
incheckning
79190010be
24 ändrade filer med 1018 tillägg och 52 borttagningar
  1. 1 0
      Sunrise/Sunrise.vcxproj
  2. 7 0
      Sunrise/src/client/content/items/packages/internal.h
  3. 147 0
      Sunrise/src/client/content/items/packages/package_catalyst_condition_reader.cpp
  4. 21 2
      Sunrise/src/client/content/items/packages/package_item_build.cpp
  5. 21 0
      Sunrise/src/client/content/items/packages/package_item_rows.cpp
  6. 102 0
      Sunrise/src/client/content/items/packages/package_socket_plug_build.cpp
  7. 47 0
      Sunrise/src/client/content/items/packages/package_socket_plug_build.h
  8. 19 0
      Sunrise/src/middleware/content/packages/tables/definition_index_table.h
  9. 3 0
      Sunrise/src/middleware/datagen/family4/account/account_encoder.cpp
  10. 6 5
      Sunrise/src/server/web_service/web_service_runtime.cpp
  11. 4 1
      Sunrise/src/state/build_data/cache/records/cache_detail_links.cpp
  12. 2 2
      Sunrise/src/state/build_data/cache/records/cache_domain_validation.cpp
  13. 27 6
      Sunrise/src/state/build_data/cache/records/cache_exotic_catalyst_record_codec.cpp
  14. 15 3
      Sunrise/src/state/build_data/cache/records/format.h
  15. 91 0
      Sunrise/src/state/build_data/items/catalysts/definition.h
  16. 8 0
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_build_data_runtime.cpp
  17. 252 19
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp
  18. 19 0
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.h
  19. 178 7
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp
  20. 18 0
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h
  21. 15 0
      Sunrise/src/state/build_data/runtime.h
  22. 1 1
      Sunrise/src/state/investment/investment.h
  23. 6 2
      Sunrise/src/state/runtime/runtime.h
  24. 8 4
      Sunrise/src/state/runtime/state_runtime.cpp

+ 1 - 0
Sunrise/Sunrise.vcxproj

@@ -1020,6 +1020,7 @@
     <ClCompile Include="src\client\content\items\packages\package_collectible_build.cpp" />
     <ClCompile Include="src\client\content\items\packages\package_material_requirement_build.cpp" />
     <ClCompile Include="src\client\content\items\packages\package_detail_build.cpp" />
+    <ClCompile Include="src\client\content\items\packages\package_catalyst_condition_reader.cpp" />
     <ClCompile Include="src\client\content\items\packages\package_socket_plug_build.cpp" />
     <ClCompile Include="src\client\content\items\packages\package_ability_build.cpp" />
     <ClCompile Include="src\client\content\items\packages\package_subclass_build.cpp" />

+ 7 - 0
Sunrise/src/client/content/items/packages/internal.h

@@ -62,6 +62,13 @@ struct Storage {
     std::vector<std::byte> definition{};
     /** Shared reusable/randomized plug-set table read from investment-root slot 51. */
     std::vector<std::byte> plugSetTable{};
+    /** Dense item-indexed catalyst completion expressions for this package pass. */
+    std::vector<state::build_data::items::catalysts::CompletionCondition>
+        catalystCompletionConditions{};
+    /** Dense socket-type-indexed acquired-state gates for this package pass. */
+    std::vector<state::build_data::items::catalysts::AcquisitionGate> catalystAcquisitionGates{};
+    /** Dense native objective completion values used by legacy catalyst progress items. */
+    std::vector<std::int32_t> catalystObjectiveValues{};
     /** Compact 0..3 special plug-category code of every dense installed item row. */
     std::array<std::uint8_t, state::build_data::items::kDefinitionCapacity> specialPlugCategories{};
     /** Inventory routing rows held until the paired bucket-definition table is resolved. */

+ 147 - 0
Sunrise/src/client/content/items/packages/package_catalyst_condition_reader.cpp

@@ -0,0 +1,147 @@
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+
+#include "../../../../middleware/content/packages/tables/definition_index_table.h"
+#include "package_socket_plug_build.h"
+
+namespace sunrise::client::content::items::packages {
+namespace {
+
+/** Native postfix opcodes for flag, value, literal, and greater-than-or-equal. */
+constexpr std::uint32_t kFlagOpcode = 1;
+constexpr std::uint32_t kValueOpcode = 10;
+constexpr std::uint32_t kLiteralOpcode = 11;
+constexpr std::uint32_t kGreaterEqualOpcode = 14;
+/** Each postfix token is an opcode and one 32-bit operand. */
+constexpr std::size_t kExpressionTokenSize = 8;
+constexpr std::size_t kValueExpressionTokenCount = 3;
+
+/** Adds one unique positive flag term to a bounded condition. */
+[[nodiscard]] bool append_flag(catalysts::CompletionRequirements& output,
+                               std::uint16_t definitionIndex) noexcept {
+    const auto end = output.flags.begin() + output.flagCount;
+    if (std::find(output.flags.begin(), end, definitionIndex) != end) {
+        return true;
+    }
+    if (output.flagCount >= output.flags.size()) {
+        return false;
+    }
+    output.flags[output.flagCount++] = definitionIndex;
+    return true;
+}
+
+/** Adds or raises one unique signed-value minimum in a bounded condition. */
+[[nodiscard]] bool upsert_value(catalysts::CompletionRequirements& output,
+                                std::uint16_t index,
+                                std::int32_t minimum) noexcept {
+    for (std::size_t row = 0; row < output.valueCount; ++row) {
+        if (output.values[row].index == index) {
+            output.values[row].minimum = (std::max)(output.values[row].minimum, minimum);
+            return true;
+        }
+    }
+    if (output.valueCount >= output.values.size()) {
+        return false;
+    }
+    output.values[output.valueCount++] = {index, minimum};
+    return true;
+}
+
+/** Marks a conflicting or over-capacity native rule and clears its partial operands. */
+void mark_ambiguous(catalysts::CompletionCondition& output) noexcept {
+    output.completion = {};
+    output.objectiveDefinitionIndex = catalysts::kUnavailableObjectiveIndex;
+    output.state = catalysts::CompletionConditionState::ambiguous;
+}
+
+} // namespace
+
+void read_catalyst_completion_condition(std::span<const std::byte> definition,
+                                        std::uint16_t itemDefinitionIndex,
+                                        catalysts::CompletionCondition& output) noexcept {
+    output = {};
+    output.itemDefinitionIndex = itemDefinitionIndex;
+    output.objectiveDefinitionIndex = catalysts::kUnavailableObjectiveIndex;
+    for (std::size_t descriptor = 0; descriptor + 2 * sizeof(std::uint64_t) <= definition.size();
+         descriptor += sizeof(std::uint64_t)) {
+        tables::Array expression{};
+        if (!tables::find_array_at(definition, descriptor, expression)) {
+            continue;
+        }
+        if (expression.elementClass == tables::kObjectiveReferenceArrayClass
+            && expression.count == 1 && expression.dataOffset <= definition.size()
+            && definition.size() - expression.dataOffset >= 2 * sizeof(std::uint32_t)) {
+            std::array<std::uint32_t, 2> reference{};
+            std::memcpy(reference.data(),
+                        definition.data() + expression.dataOffset,
+                        sizeof reference);
+            if (reference[0] < catalysts::kUnavailableObjectiveIndex
+                && reference[1] == tables::kObjectiveReferenceRowClass) {
+                const auto objective = static_cast<std::uint16_t>(reference[0]);
+                if (output.objectiveDefinitionIndex == catalysts::kUnavailableObjectiveIndex) {
+                    output.objectiveDefinitionIndex = objective;
+                } else if (output.objectiveDefinitionIndex != objective) {
+                    mark_ambiguous(output);
+                    return;
+                }
+            }
+            continue;
+        }
+        if (expression.elementClass != tables::kInvestmentExpressionRowClass
+            || expression.dataOffset > definition.size()
+            || expression.count
+                   > (definition.size() - expression.dataOffset) / kExpressionTokenSize) {
+            continue;
+        }
+        for (std::size_t token = 0; token < expression.count; ++token) {
+            std::array<std::uint32_t, 2> current{};
+            std::memcpy(current.data(),
+                        definition.data() + expression.dataOffset
+                            + token * kExpressionTokenSize,
+                        sizeof current);
+            if (current[0] == kFlagOpcode
+                && current[1] < state::build_data::items::kDefinitionCapacity
+                && !append_flag(output.completion, static_cast<std::uint16_t>(current[1]))) {
+                mark_ambiguous(output);
+                return;
+            }
+            if (current[0] != kValueOpcode
+                || token + kValueExpressionTokenCount > expression.count) {
+                continue;
+            }
+            std::array<std::uint32_t, kValueExpressionTokenCount * 2> tokens{};
+            std::memcpy(tokens.data(),
+                        definition.data() + expression.dataOffset
+                            + token * kExpressionTokenSize,
+                        sizeof tokens);
+            if (tokens[2] != kLiteralOpcode || tokens[4] != kGreaterEqualOpcode
+                || tokens[5] != UINT32_MAX
+                || tokens[1] >= catalysts::kUnavailableCompletionValueIndex || tokens[3] == 0
+                || tokens[3]
+                       > static_cast<std::uint32_t>(
+                           (std::numeric_limits<std::int32_t>::max)())) {
+                continue;
+            }
+            if (!upsert_value(output.completion,
+                              static_cast<std::uint16_t>(tokens[1]),
+                              static_cast<std::int32_t>(tokens[3]))) {
+                mark_ambiguous(output);
+                return;
+            }
+        }
+    }
+    std::sort(output.completion.flags.begin(),
+              output.completion.flags.begin() + output.completion.flagCount);
+    std::sort(output.completion.values.begin(),
+              output.completion.values.begin() + output.completion.valueCount,
+              [](const catalysts::CompletionValue& left,
+                 const catalysts::CompletionValue& right) { return left.index < right.index; });
+    if (output.completion.flagCount != 0 || output.completion.valueCount != 0
+        || output.objectiveDefinitionIndex != catalysts::kUnavailableObjectiveIndex) {
+        output.state = catalysts::CompletionConditionState::present;
+    }
+}
+
+} // namespace sunrise::client::content::items::packages

+ 21 - 2
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -23,6 +23,7 @@
 #include "../../spawn_sets/spawn_set_build.h"
 #include "build.h"
 #include "internal.h"
+#include "package_socket_plug_build.h"
 
 namespace sunrise::client::content::items::packages {
 namespace {
@@ -41,7 +42,8 @@ namespace {
            && state::build_data::progression_definitions_ready()
            && state::build_data::scenario_layouts_ready() && state::build_data::spawn_sets_ready()
            && state::build_data::hash_names_ready()
-           && state::build_data::investment_constants_ready();
+           && state::build_data::investment_constants_ready()
+           && state::build_data::exotic_catalysts_ready();
 }
 
 /** @return True when every item and investment-root domain is published. */
@@ -56,7 +58,8 @@ namespace {
            && state::build_data::ability_buckets_ready()
            && state::build_data::socket_entry_buckets_ready()
            && state::build_data::progression_definitions_ready()
-           && state::build_data::investment_constants_ready();
+           && state::build_data::investment_constants_ready()
+           && state::build_data::exotic_catalysts_ready();
 }
 
 } // namespace
@@ -137,6 +140,22 @@ bool build() noexcept {
                     continue;
                 }
             }
+            if (!state::build_data::exotic_catalysts_ready()) {
+                reason = "catalyst_gates";
+                if (!read_catalyst_acquisition_gates(source,
+                                                     storage.scratch,
+                                                     std::span<const std::byte>{storage.root},
+                                                     storage.child,
+                                                     storage.catalystAcquisitionGates)
+                    || !read_catalyst_objective_values(
+                        source,
+                        storage.scratch,
+                        std::span<const std::byte>{storage.root},
+                        storage.child,
+                        storage.catalystObjectiveValues)) {
+                    continue;
+                }
+            }
             reason = "buckets";
             if (!build_buckets(source, storage, std::span<const std::byte>{storage.root})) {
                 continue;

+ 21 - 0
Sunrise/src/client/content/items/packages/package_item_rows.cpp

@@ -55,6 +55,15 @@ bool build_item_rows(const reader::Source& source,
     if (retainDetails && storage.details.size() != kDetailCapacity) {
         storage.details.assign(kDetailCapacity, build_details::Definition{});
     }
+    if (needCatalysts) {
+        storage.catalystCompletionConditions.assign(
+            static_cast<std::size_t>(table.count),
+            state::build_data::items::catalysts::CompletionCondition{});
+        for (std::size_t item = 0; item < storage.catalystCompletionConditions.size(); ++item) {
+            storage.catalystCompletionConditions[item].itemDefinitionIndex =
+                static_cast<std::uint16_t>(item);
+        }
+    }
     const bool detailStorageReady = !retainDetails || storage.details.size() == kDetailCapacity;
     const std::span<const std::byte> container{storage.child};
     reason = "rows";
@@ -147,6 +156,13 @@ bool build_item_rows(const reader::Source& source,
             if (retainDetails) {
                 storage.details[builtDetailCount++] = detail;
             }
+            if (needCatalysts
+                && detail.definitionIndex < storage.catalystCompletionConditions.size()) {
+                read_catalyst_completion_condition(
+                    std::span<const std::byte>{storage.definition},
+                    detail.definitionIndex,
+                    storage.catalystCompletionConditions[detail.definitionIndex]);
+            }
             if (needSocketRows) {
                 (void)socketPlugBuild.append(item,
                                              std::span<const std::byte>{storage.definition},
@@ -171,6 +187,9 @@ bool build_item_rows(const reader::Source& source,
             socketPlugBuild.rules(),
             socketPlugBuild.pools(),
             socketPlugBuild.members(),
+            storage.catalystCompletionConditions,
+            storage.catalystAcquisitionGates,
+            storage.catalystObjectiveValues,
         };
         bool catalystBuilt = false;
         if (published && needCatalysts) {
@@ -178,6 +197,7 @@ bool build_item_rows(const reader::Source& source,
             catalystBuilt = state::build_data::derive_exotic_catalysts(
                 catalystSource, catalystRows, catalystCount, catalystReport);
             report_catalyst_catalog(catalystReport, catalystBuilt);
+            published = catalystBuilt;
         }
         if (published && needSocketPlugs) {
             const std::size_t rules = socketPlugBuild.rule_count();
@@ -227,6 +247,7 @@ bool build_item_rows(const reader::Source& source,
     return published && state::build_data::item_definitions_ready()
            && state::build_data::configured_item_details_ready()
            && state::build_data::socket_plug_rules_ready()
+           && state::build_data::exotic_catalysts_ready()
            && state::build_data::ability_buckets_ready();
 }
 

+ 102 - 0
Sunrise/src/client/content/items/packages/package_socket_plug_build.cpp

@@ -1,8 +1,10 @@
 #include "package_socket_plug_build.h"
 
 #include <algorithm>
+#include <cstring>
 #include <limits>
 
+#include "../../../../middleware/content/packages/tables/definition_index_table.h"
 #include "../../../../state/build_data/runtime.h"
 
 namespace sunrise::client::content::items::packages {
@@ -25,6 +27,20 @@ constexpr std::uint16_t kTrackerSocketType = 518;
 /** FNV-1a constants make pool fingerprints stable and cheap. */
 constexpr std::uint64_t kHashOffsetBasis = 14695981039346656037ULL;
 constexpr std::uint64_t kHashPrime = 1099511628211ULL;
+/** One acquired-state rule starts with kind 1 and then its item-definition index. */
+constexpr std::uint32_t kAcquisitionRuleKind = 1;
+constexpr std::size_t kAcquisitionRuleSize = 8;
+
+/** Reads one trivially copied scalar from a bounded package blob. */
+template <typename Value>
+[[nodiscard]] bool
+read(std::span<const std::byte> blob, std::size_t offset, Value& value) noexcept {
+    if (offset > blob.size() || blob.size() - offset < sizeof value) {
+        return false;
+    }
+    std::memcpy(&value, blob.data() + offset, sizeof value);
+    return true;
+}
 
 /** Visitor adapter that appends one list member to a bounded lane candidate. */
 struct VisitorContext {
@@ -45,6 +61,92 @@ struct VisitorContext {
 
 } // namespace
 
+bool read_catalyst_acquisition_gates(const reader::Source& source,
+                                     reader::Scratch& scratch,
+                                     std::span<const std::byte> root,
+                                     std::vector<std::byte>& blob,
+                                     std::vector<catalysts::AcquisitionGate>& output) noexcept {
+    output.clear();
+    std::uint32_t tableTag = 0;
+    tables::Array table{};
+    if (!tables::slot_tag(root, tables::kSocketTypeTableSlot, tableTag) || tableTag == 0
+        || !reader::read_tag(source, scratch, tableTag, blob)
+        || !tables::find_array_at(blob, tables::kTableArrayDescriptor, table)
+        || table.elementClass != tables::kSocketTypeTableClass || table.count == 0
+        || table.count > (std::numeric_limits<std::uint16_t>::max)()) {
+        return false;
+    }
+    const std::uint64_t tableSize = table.count * tables::kSocketTypeRowStride;
+    if (tableSize > blob.size() || table.dataOffset > blob.size() - tableSize) {
+        return false;
+    }
+
+    output.resize(static_cast<std::size_t>(table.count));
+    const std::span<const std::byte> bytes{blob};
+    for (std::size_t index = 0; index < output.size(); ++index) {
+        catalysts::AcquisitionGate& gate = output[index];
+        gate.socketType = static_cast<std::uint16_t>(index);
+        const std::size_t row = table.dataOffset + index * tables::kSocketTypeRowStride;
+        tables::Array rules{};
+        if (!tables::find_array_at(bytes, row + tables::kSocketTypeAcquisitionDescriptor, rules)) {
+            continue;
+        }
+        if (rules.count != 1 || rules.elementClass != tables::kInvestmentExpressionRowClass
+            || rules.dataOffset > bytes.size()
+            || bytes.size() - rules.dataOffset < kAcquisitionRuleSize) {
+            gate.state = catalysts::AcquisitionState::ambiguous;
+            continue;
+        }
+        std::uint32_t kind = 0;
+        std::uint32_t definitionIndex = 0;
+        if (!read(bytes, rules.dataOffset, kind)
+            || !read(bytes, rules.dataOffset + sizeof kind, definitionIndex)
+            || kind != kAcquisitionRuleKind
+            || definitionIndex >= state::build_data::items::kDefinitionCapacity) {
+            gate.state = catalysts::AcquisitionState::ambiguous;
+            continue;
+        }
+        gate.definitionIndex = static_cast<std::uint16_t>(definitionIndex);
+        gate.state = catalysts::AcquisitionState::present;
+    }
+    return true;
+}
+
+bool read_catalyst_objective_values(const reader::Source& source,
+                                    reader::Scratch& scratch,
+                                    std::span<const std::byte> root,
+                                    std::vector<std::byte>& blob,
+                                    std::vector<std::int32_t>& output) noexcept {
+    output.clear();
+    std::uint32_t tableTag = 0;
+    std::uint32_t tableClass = 0;
+    tables::Array table{};
+    if (!tables::slot_tag(root, tables::kObjectiveTableSlot, tableTag) || tableTag == 0
+        || !reader::read_tag(source, scratch, tableTag, blob, tableClass)
+        || tableClass != tables::kObjectiveTableClass
+        || !tables::find_array_at(blob, tables::kTableArrayDescriptor, table)
+        || table.elementClass != tables::kObjectiveRowClass || table.count == 0
+        || table.count > catalysts::kUnavailableObjectiveIndex) {
+        return false;
+    }
+    const std::uint64_t tableSize = table.count * tables::kObjectiveRowStride;
+    if (tableSize > blob.size() || table.dataOffset > blob.size() - tableSize) {
+        return false;
+    }
+
+    output.resize(static_cast<std::size_t>(table.count));
+    const std::span<const std::byte> bytes{blob};
+    for (std::size_t index = 0; index < output.size(); ++index) {
+        const std::size_t offset = table.dataOffset + index * tables::kObjectiveRowStride
+                                   + tables::kObjectiveCompletionValueOffset;
+        if (!read(bytes, offset, output[index])) {
+            output.clear();
+            return false;
+        }
+    }
+    return true;
+}
+
 /** Returns the compact 1-based code of one native category-expansion family. */
 std::uint8_t special_plug_category(std::uint32_t categoryHash) noexcept {
     for (std::size_t index = 0; index < kExpandableCategories.size(); ++index) {

+ 47 - 0
Sunrise/src/client/content/items/packages/package_socket_plug_build.h

@@ -6,14 +6,61 @@
 #include <span>
 #include <vector>
 
+#include "../../../../middleware/content/packages/reader/reader.h"
 #include "../../../../middleware/content/packages/tables/items.h"
+#include "../../../../state/build_data/items/catalysts/definition.h"
 #include "../../../../state/build_data/items/item_catalog.h"
 #include "../../../../state/build_data/items/socket_plugs/definition.h"
 
 namespace sunrise::client::content::items::packages {
 
+namespace reader = middleware::content::packages::reader;
 namespace tables = middleware::content::packages::tables;
 namespace socket_plugs = state::build_data::items::socket_plugs;
+namespace catalysts = state::build_data::items::catalysts;
+
+/**
+ * Reads the dense socket-type table and extracts each type's acquired-state gate.
+ * @param source Installed package source.
+ * @param scratch Shared package reader scratch.
+ * @param root Investment root bytes.
+ * @param blob Scratch storage for the socket-type table.
+ * @param output Receives one row per native socket type.
+ * @return True when the fixed table shape and every row extent are valid.
+ */
+[[nodiscard]] bool
+read_catalyst_acquisition_gates(const reader::Source& source,
+                                reader::Scratch& scratch,
+                                std::span<const std::byte> root,
+                                std::vector<std::byte>& blob,
+                                std::vector<catalysts::AcquisitionGate>& output) noexcept;
+
+/**
+ * Reads the dense objective table's build-defined completion values.
+ * @param source Installed package source.
+ * @param scratch Shared package reader scratch.
+ * @param root Investment root bytes.
+ * @param blob Scratch storage for the objective table.
+ * @param output Receives one completion value per native objective index.
+ * @return True when the table class, row class, count, and fixed rows are valid.
+ */
+[[nodiscard]] bool
+read_catalyst_objective_values(const reader::Source& source,
+                               reader::Scratch& scratch,
+                               std::span<const std::byte> root,
+                               std::vector<std::byte>& blob,
+                               std::vector<std::int32_t>& output) noexcept;
+
+/**
+ * Finds every positive flag and `value >= literal` term plus one objective reference.
+ * Duplicate terms are folded. Conflicting objectives or fixed-capacity overflow fail closed.
+ * @param definition Complete installed definition of the catalyst effect item.
+ * @param itemDefinitionIndex Native index of the catalyst effect item.
+ * @param output Receives the unique condition or its absent or ambiguous state.
+ */
+void read_catalyst_completion_condition(std::span<const std::byte> definition,
+                                        std::uint16_t itemDefinitionIndex,
+                                        catalysts::CompletionCondition& output) noexcept;
 
 /** Fixed-size, heap-backed interning state for one installed package pass. */
 class SocketPlugBuild final {

+ 19 - 0
Sunrise/src/middleware/content/packages/tables/definition_index_table.h

@@ -183,6 +183,25 @@ slot_tag(std::span<const std::byte> blob, std::size_t index, std::uint32_t& tag)
 inline constexpr std::size_t kItemTableSlot = 48;
 /** The investment root holds the shared reusable/randomized plug-set table at this slot. */
 inline constexpr std::size_t kPlugSetTableSlot = 51;
+/** The investment root holds the dense ordinary socket-type table at this slot. */
+inline constexpr std::size_t kSocketTypeTableSlot = 94;
+/** Element class and row size of the installed ordinary socket-type table. */
+inline constexpr std::uint32_t kSocketTypeTableClass = 0x80807ABBU;
+inline constexpr std::size_t kSocketTypeRowStride = 96;
+/** One socket type points to its acquired-state rule array from this row offset. */
+inline constexpr std::size_t kSocketTypeAcquisitionDescriptor = 16;
+/** Acquired-state and item-completion arrays carry this postfix expression-row class. */
+inline constexpr std::uint32_t kInvestmentExpressionRowClass = 0x80807D31U;
+/** Catalyst progress items reference objective rows through this one-row array. */
+inline constexpr std::uint32_t kObjectiveReferenceArrayClass = 0x808087B1U;
+inline constexpr std::uint32_t kObjectiveReferenceRowClass = 0x808077E3U;
+/** The investment root holds the dense objective definition table at this slot. */
+inline constexpr std::size_t kObjectiveTableSlot = 58;
+/** Installed objective table and row layout. */
+inline constexpr std::uint32_t kObjectiveTableClass = 0x8080775BU;
+inline constexpr std::uint32_t kObjectiveRowClass = 0x8080775FU;
+inline constexpr std::size_t kObjectiveRowStride = 0xA0;
+inline constexpr std::size_t kObjectiveCompletionValueOffset = 0x30;
 /** The investment root holds the socket entry list table at this slot. */
 inline constexpr std::size_t kSocketEntryListTableSlot = 97;
 /** The globals container names the investment root as its first child. */

+ 3 - 0
Sunrise/src/middleware/datagen/family4/account/account_encoder.cpp

@@ -95,6 +95,9 @@ bool encode(const state::AccountState& state, std::span<std::byte> output) noexc
     object.acquiredFlags = unlocks.accountFlags;
     object.profileUnlockFlags = unlocks.profileFlags;
     object.objectiveValues = unlocks.objectiveValues;
+    if (!state::build_data::complete_exotic_catalyst_objectives(object.objectiveValues)) {
+        return false;
+    }
     for (layout::CharacterUnlockBlock& block : object.characterUnlocks) {
         block.flags = unlocks.characterFlags;
     }

+ 6 - 5
Sunrise/src/server/web_service/web_service_runtime.cpp

@@ -237,9 +237,10 @@ bool consume(std::span<const std::byte> request,
     note_reports(message);
 
     if (message.opcode == messages::opcode205::kOpcode) {
-        const auto investment = state::investment_snapshot();
-        return messages::opcode205::encode_response(
-                   message, investment, next_family5_clock(), response, written)
+        state::InvestmentState investment{};
+        return (state::investment_snapshot(investment)
+                && messages::opcode205::encode_response(
+                    message, investment, next_family5_clock(), response, written))
                || encode_echo(message, response, written);
     }
 
@@ -251,8 +252,8 @@ bool consume(std::span<const std::byte> request,
         if (!bootstrap.hasPrimarySoid) {
             bootstrap.primarySoid = state::account_snapshot().primarySoid;
         }
-        const auto investment = state::investment_snapshot();
-        if (!parsed
+        state::InvestmentState investment{};
+        if (!parsed || !state::investment_snapshot(investment)
             || !messages::opcode503::encode_response(
                 message, bootstrap, investment, next_family5_clock(), response, written)) {
             return encode_echo(message, response, written);

+ 4 - 1
Sunrise/src/state/build_data/cache/records/cache_detail_links.cpp

@@ -128,8 +128,11 @@ bool valid_exotic_catalyst_links(const BuildIdentity& build, Domains domains) no
         domains.socketPlugRules,
         domains.socketPlugPools,
         domains.socketPlugMembers,
+        {},
+        {},
+        {},
     };
-    return items::catalysts::matches_derived(source, facts, domains.exoticCatalysts);
+    return items::catalysts::matches_cached(source, facts, domains.exoticCatalysts);
 }
 
 } // namespace sunrise::state::build_data::cache::records

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

@@ -205,8 +205,8 @@ bool valid_domains(const BuildIdentity& build, Domains domains) noexcept {
         || domains.constants.weaponPowerStatRow >= constants::kStatRowCount || domains.named.empty()
         || domains.items.empty() || domains.collectibles.empty()
         || domains.materialRequirementSets.empty() || domains.socketPlugRules.empty()
-        || domains.socketPlugPools.empty()
-        || domains.inventoryBuckets.empty() || domains.socketEntryLists.empty()
+        || domains.socketPlugPools.empty() || domains.inventoryBuckets.empty()
+        || domains.socketEntryLists.empty()
         || !std::all_of(domains.named.begin(), domains.named.end(), valid_name)
         || !strictly_ordered(domains.named, named_less) || !items::valid(domains.items)
         || !collectibles::valid(domains.collectibles)

+ 27 - 6
Sunrise/src/state/build_data/cache/records/cache_exotic_catalyst_record_codec.cpp

@@ -10,9 +10,20 @@ bool encode(const items::catalysts::Definition& value, ExoticCatalystRecord& rec
     record.itemDefinitionHash = value.itemDefinitionHash;
     record.itemDefinitionIndex = value.itemDefinitionIndex;
     record.completedPlugDefinitionIndex = value.completedPlugDefinitionIndex;
+    record.progressPlugDefinitionIndex = value.progressPlugDefinitionIndex;
     record.effectDefinitionIndex = value.effectDefinitionIndex;
+    record.acquisitionDefinitionIndex = value.acquisitionDefinitionIndex;
+    record.completionFlagDefinitionIndices = value.completion.flags;
+    for (std::size_t index = 0; index < value.completion.values.size(); ++index) {
+        record.completionValueIndices[index] = value.completion.values[index].index;
+        record.completionValues[index] = value.completion.values[index].minimum;
+    }
+    record.objectiveDefinitionIndex = value.objective.definitionIndex;
     record.socketLane = value.socketLane;
     record.availability = static_cast<std::uint8_t>(value.availability);
+    record.completionFlagCount = value.completion.flagCount;
+    record.completionValueCount = value.completion.valueCount;
+    record.objectiveValue = value.objective.value;
     return true;
 }
 
@@ -22,12 +33,22 @@ bool decode(const ExoticCatalystRecord& record, items::catalysts::Definition& va
     if (!items::catalysts::valid_availability(availability)) {
         return false;
     }
-    value = {record.itemDefinitionHash,
-             record.itemDefinitionIndex,
-             record.completedPlugDefinitionIndex,
-             record.effectDefinitionIndex,
-             record.socketLane,
-             availability};
+    value.itemDefinitionHash = record.itemDefinitionHash;
+    value.itemDefinitionIndex = record.itemDefinitionIndex;
+    value.completedPlugDefinitionIndex = record.completedPlugDefinitionIndex;
+    value.progressPlugDefinitionIndex = record.progressPlugDefinitionIndex;
+    value.effectDefinitionIndex = record.effectDefinitionIndex;
+    value.acquisitionDefinitionIndex = record.acquisitionDefinitionIndex;
+    value.completion.flags = record.completionFlagDefinitionIndices;
+    for (std::size_t index = 0; index < value.completion.values.size(); ++index) {
+        value.completion.values[index] = {record.completionValueIndices[index],
+                                         record.completionValues[index]};
+    }
+    value.completion.flagCount = record.completionFlagCount;
+    value.completion.valueCount = record.completionValueCount;
+    value.objective = {record.objectiveDefinitionIndex, record.objectiveValue};
+    value.socketLane = record.socketLane;
+    value.availability = availability;
     return true;
 }
 

+ 15 - 3
Sunrise/src/state/build_data/cache/records/format.h

@@ -11,6 +11,7 @@
 #include "../../constants/definition.h"
 #include "../../definition.h"
 #include "../../hash_names/definition.h"
+#include "../../items/catalysts/definition.h"
 #include "../../items/details/definition.h"
 #include "../../items/item_catalog.h"
 #include "../../items/socket_plugs/definition.h"
@@ -28,9 +29,8 @@ inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', '
  * Current build-data cache format. An older cache is rebuilt rather than read.
  * 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.
- * Development builds wrote formats 46 through 48, so those numbers cannot be reused.
  */
-inline constexpr std::uint32_t kCacheFormatVersion = 49;
+inline constexpr std::uint32_t kCacheFormatVersion = 47;
 /** 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. */
@@ -210,9 +210,20 @@ struct ExoticCatalystRecord {
     std::uint32_t itemDefinitionHash{};
     std::uint16_t itemDefinitionIndex{};
     std::uint16_t completedPlugDefinitionIndex{};
+    std::uint16_t progressPlugDefinitionIndex{};
     std::uint16_t effectDefinitionIndex{};
+    std::uint16_t acquisitionDefinitionIndex{};
+    std::array<std::uint16_t, items::catalysts::kCompletionFlagCapacity>
+        completionFlagDefinitionIndices{};
+    std::array<std::uint16_t, items::catalysts::kCompletionValueCapacity>
+        completionValueIndices{};
+    std::uint16_t objectiveDefinitionIndex{items::catalysts::kUnavailableObjectiveIndex};
     std::uint8_t socketLane{};
     std::uint8_t availability{};
+    std::uint8_t completionFlagCount{};
+    std::uint8_t completionValueCount{};
+    std::array<std::int32_t, items::catalysts::kCompletionValueCapacity> completionValues{};
+    std::int32_t objectiveValue{};
 };
 
 /** Disk form of one inventory-bucket array-routing descriptor. */
@@ -506,7 +517,8 @@ static_assert(sizeof(SocketPlugRuleRecord)
 static_assert(sizeof(SocketPlugPoolRecord) == 2 * sizeof(std::uint32_t));
 static_assert(sizeof(SocketPlugMemberRecord) == sizeof(std::uint16_t));
 static_assert(sizeof(ExoticCatalystRecord)
-              == sizeof(std::uint32_t) + 3 * sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t));
+              == 6 * sizeof(std::uint32_t) + 14 * sizeof(std::uint16_t)
+                     + 4 * sizeof(std::uint8_t));
 static_assert(sizeof(InventoryBucketRecord)
               == 4 * sizeof(std::uint8_t) + 2 * sizeof(std::uint16_t));
 static_assert(sizeof(SocketEntryListRecord)

+ 91 - 0
Sunrise/src/state/build_data/items/catalysts/definition.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include <array>
 #include <cstddef>
 #include <cstdint>
 #include <string_view>
@@ -8,6 +9,45 @@ namespace sunrise::state::build_data::items::catalysts {
 
 /** The target build contains fewer than 128 exotic weapon catalyst-shaped sockets. */
 inline constexpr std::size_t kDefinitionCapacity = 128;
+/** All bits set mark a catalyst relation with no completion-value requirement. */
+inline constexpr std::uint16_t kUnavailableCompletionValueIndex = 0xFFFFU;
+/** All bits set mark a catalyst relation with no completion-flag requirement. */
+inline constexpr std::uint16_t kUnavailableCompletionFlagIndex = 0xFFFFU;
+/** All bits set mark a catalyst relation with no acquired-state gate. */
+inline constexpr std::uint16_t kUnavailableAcquisitionIndex = 0xFFFFU;
+/** All bits set mark a catalyst relation with no account objective. */
+inline constexpr std::uint16_t kUnavailableObjectiveIndex = 0xFFFFU;
+/** No shipped catalyst effect needs more than four distinct Family-5 flags or values. */
+inline constexpr std::size_t kCompletionFlagCapacity = 4;
+inline constexpr std::size_t kCompletionValueCapacity = 4;
+
+/** One Family-5 signed value that must reach a build-derived minimum. */
+struct CompletionValue {
+    std::uint16_t index{};
+    std::int32_t minimum{};
+
+    [[nodiscard]] constexpr bool operator==(const CompletionValue& other) const noexcept = default;
+};
+
+/** All positive Family-5 terms read from one catalyst effect's completion expressions. */
+struct CompletionRequirements {
+    std::array<std::uint16_t, kCompletionFlagCapacity> flags{};
+    std::array<CompletionValue, kCompletionValueCapacity> values{};
+    std::uint8_t flagCount{};
+    std::uint8_t valueCount{};
+
+    [[nodiscard]] constexpr bool
+    operator==(const CompletionRequirements& other) const noexcept = default;
+};
+
+/** Optional account objective that marks one legacy catalyst's work complete. */
+struct ObjectiveCompletion {
+    std::uint16_t definitionIndex{kUnavailableObjectiveIndex};
+    std::int32_t value{};
+
+    [[nodiscard]] constexpr bool operator==(const ObjectiveCompletion& other) const noexcept =
+        default;
+};
 
 /** Release status from the build-scoped Season 11 availability data. */
 enum class Availability : std::uint8_t {
@@ -34,6 +74,9 @@ enum class Error : std::uint8_t {
     missingReleased,
     ambiguousLifecycle,
     invalidSocket,
+    invalidAcquisition,
+    invalidCompletion,
+    invalidObjective,
     capacityExceeded,
 };
 
@@ -57,6 +100,12 @@ enum class Error : std::uint8_t {
         return "ambiguous_lifecycle";
     case Error::invalidSocket:
         return "invalid_socket";
+    case Error::invalidAcquisition:
+        return "invalid_acquisition";
+    case Error::invalidCompletion:
+        return "invalid_completion";
+    case Error::invalidObjective:
+        return "invalid_objective";
     case Error::capacityExceeded:
         return "capacity_exceeded";
     }
@@ -69,8 +118,16 @@ struct Definition {
     std::uint16_t itemDefinitionIndex{};
     /** Unsupported rows use the unavailable item-index value. */
     std::uint16_t completedPlugDefinitionIndex{};
+    /** Legacy in-progress display plug, or the unavailable item-index value. */
+    std::uint16_t progressPlugDefinitionIndex{};
     /** Item row that supplies the completed catalyst's native perks and stat changes. */
     std::uint16_t effectDefinitionIndex{};
+    /** Family-5 acquired-state slot that makes the catalyst socket visible. */
+    std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
+    /** Family-5 terms required by the catalyst's active effect item. */
+    CompletionRequirements completion{};
+    /** Account objective referenced by a legacy progress plug, when present. */
+    ObjectiveCompletion objective{};
     std::uint8_t socketLane{};
     Availability availability{Availability::unsupported};
 
@@ -95,7 +152,41 @@ struct Definition {
 struct CompletedCatalyst {
     std::uint8_t socketLane{};
     std::uint16_t completedPlugDefinitionIndex{};
+    std::uint16_t progressPlugDefinitionIndex{};
     std::uint16_t effectDefinitionIndex{};
+    std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
+    CompletionRequirements completion{};
+    ObjectiveCompletion objective{};
+};
+
+/** Result of scanning one effect item's postfix completion expressions. */
+enum class CompletionConditionState : std::uint8_t {
+    absent = 0,
+    present = 1,
+    ambiguous = 2,
+};
+
+/** One item definition's build-derived completion flag and value. */
+struct CompletionCondition {
+    std::uint16_t itemDefinitionIndex{};
+    CompletionRequirements completion{};
+    /** First objective reference carried by the item, when it has exactly one. */
+    std::uint16_t objectiveDefinitionIndex{kUnavailableObjectiveIndex};
+    CompletionConditionState state{CompletionConditionState::absent};
+};
+
+/** Result of reading one socket type's acquired-state rule. */
+enum class AcquisitionState : std::uint8_t {
+    absent = 0,
+    present = 1,
+    ambiguous = 2,
+};
+
+/** One socket type's build-derived Family-5 acquisition gate. */
+struct AcquisitionGate {
+    std::uint16_t socketType{};
+    std::uint16_t definitionIndex{kUnavailableAcquisitionIndex};
+    AcquisitionState state{AcquisitionState::absent};
 };
 
 /** One resolver result. Errors never carry a completed state. */

+ 8 - 0
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_build_data_runtime.cpp

@@ -102,6 +102,14 @@ complete_exotic_catalyst(std::uint16_t itemDefinitionIndex,
     return items::catalysts::apply_completed(itemDefinitionIndex, flags, plugs);
 }
 
+bool complete_exotic_catalyst_investment(Family5State& family) noexcept {
+    return items::catalysts::append_investment_overrides(family);
+}
+
+bool complete_exotic_catalyst_objectives(std::span<std::int32_t> values) noexcept {
+    return items::catalysts::append_objective_completions(values);
+}
+
 std::uint16_t resolve_exotic_catalyst_effect(std::uint16_t itemDefinitionIndex,
                                              std::uint8_t socketLane,
                                              std::uint16_t plugDefinitionIndex) noexcept {

+ 252 - 19
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp

@@ -5,6 +5,8 @@
 #include <functional>
 #include <optional>
 
+#include "../../../unlocks/definition.h"
+
 namespace sunrise::state::build_data::items::catalysts {
 namespace {
 
@@ -16,7 +18,11 @@ struct LaneResult {
     bool catalyst{};
     Error error{Error::none};
     std::uint16_t completedPlugDefinitionIndex{details::kUnavailableItemIndex};
+    std::uint16_t progressPlugDefinitionIndex{details::kUnavailableItemIndex};
     std::uint16_t effectDefinitionIndex{details::kUnavailableItemIndex};
+    std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
+    CompletionRequirements completion{};
+    ObjectiveCompletion objective{};
 };
 
 /**
@@ -68,6 +74,16 @@ find_indexed(std::span<const Value> definitions, std::uint16_t index, Index inde
     return value.definitionIndex;
 }
 
+/** @return The completion condition's native item index. */
+[[nodiscard]] constexpr std::uint16_t completion_index(const CompletionCondition& value) noexcept {
+    return value.itemDefinitionIndex;
+}
+
+/** @return The acquisition gate's native socket type. */
+[[nodiscard]] constexpr std::uint16_t acquisition_index(const AcquisitionGate& value) noexcept {
+    return value.socketType;
+}
+
 /**
  * @param definitions Installed item rows in native index order.
  * @param index Native item index to find.
@@ -88,16 +104,33 @@ find_detail(std::span<const details::Definition> definitions, std::uint16_t inde
     return find_indexed(definitions, index, detail_index);
 }
 
+/** @return The matching completion condition, or null. */
+[[nodiscard]] const CompletionCondition*
+find_completion_condition(std::span<const CompletionCondition> definitions,
+                          std::uint16_t index) noexcept {
+    return find_indexed(definitions, index, completion_index);
+}
+
+/** @return The matching acquisition gate, or null. */
+[[nodiscard]] const AcquisitionGate*
+find_acquisition_gate(std::span<const AcquisitionGate> definitions,
+                      std::uint16_t socketType) noexcept {
+    return find_indexed(definitions, socketType, acquisition_index);
+}
+
 /**
  * Resolves the exotic item row that owns one completed plug's native perks and stat changes.
  * Later catalysts use that item as their socket plug. Legacy sockets use a display-only plug in
  * the same category, so the unique exotic stackable item in that category supplies the effect.
  * @param source Parsed target-build tables.
  * @param completedPlugDefinitionIndex Completed display or active plug from the socket pool.
+ * @param socketType Native socket type that owns the acquired-state gate.
  * @return Completed lane relation, or an effect mapping error.
  */
 [[nodiscard]] LaneResult complete_lane(const Source& source,
-                                       std::uint16_t completedPlugDefinitionIndex) noexcept {
+                                       std::uint16_t completedPlugDefinitionIndex,
+                                       std::uint16_t progressPlugDefinitionIndex,
+                                       std::uint16_t socketType) noexcept {
     const items::Definition* completed = find_item(source.items, completedPlugDefinitionIndex);
     if (completed == nullptr || completed->plugCategoryHash == 0) {
         return {};
@@ -124,10 +157,64 @@ find_detail(std::span<const details::Definition> definitions, std::uint16_t inde
         return {};
     }
 
+    const AcquisitionGate* acquisition = find_acquisition_gate(source.acquisitionGates, socketType);
+    if (acquisition == nullptr || acquisition->state != AcquisitionState::present
+        || acquisition->definitionIndex == kUnavailableAcquisitionIndex) {
+        return {.catalyst = true,
+                .error = Error::invalidAcquisition,
+                .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
+                .progressPlugDefinitionIndex = progressPlugDefinitionIndex,
+                .effectDefinitionIndex = *effect};
+    }
+
+    const CompletionCondition* condition =
+        find_completion_condition(source.completionConditions, *effect);
+    if (condition == nullptr || condition->state != CompletionConditionState::present
+        || (condition->completion.flagCount == 0 && condition->completion.valueCount == 0)) {
+        return {.catalyst = true,
+                .error = Error::invalidCompletion,
+                .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
+                .progressPlugDefinitionIndex = progressPlugDefinitionIndex,
+                .effectDefinitionIndex = *effect,
+                .acquisitionDefinitionIndex = acquisition->definitionIndex};
+    }
+
+    ObjectiveCompletion objective{};
+    if (progressPlugDefinitionIndex != details::kUnavailableItemIndex) {
+        const CompletionCondition* progress =
+            find_completion_condition(source.completionConditions, progressPlugDefinitionIndex);
+        if (progress == nullptr || progress->state == CompletionConditionState::ambiguous) {
+            return {.catalyst = true,
+                    .error = Error::invalidObjective,
+                    .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
+                    .progressPlugDefinitionIndex = progressPlugDefinitionIndex,
+                    .effectDefinitionIndex = *effect,
+                    .acquisitionDefinitionIndex = acquisition->definitionIndex};
+        }
+        if (progress->objectiveDefinitionIndex != kUnavailableObjectiveIndex) {
+            if (progress->state != CompletionConditionState::present
+                || progress->objectiveDefinitionIndex >= source.objectiveCompletionValues.size()
+                || source.objectiveCompletionValues[progress->objectiveDefinitionIndex] <= 0) {
+                return {.catalyst = true,
+                        .error = Error::invalidObjective,
+                        .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
+                        .progressPlugDefinitionIndex = progressPlugDefinitionIndex,
+                        .effectDefinitionIndex = *effect,
+                        .acquisitionDefinitionIndex = acquisition->definitionIndex};
+            }
+            objective = {progress->objectiveDefinitionIndex,
+                         source.objectiveCompletionValues[progress->objectiveDefinitionIndex]};
+        }
+    }
+
     return {.catalyst = true,
             .error = Error::none,
             .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
-            .effectDefinitionIndex = *effect};
+            .progressPlugDefinitionIndex = progressPlugDefinitionIndex,
+            .effectDefinitionIndex = *effect,
+            .acquisitionDefinitionIndex = acquisition->definitionIndex,
+            .completion = condition->completion,
+            .objective = objective};
 }
 
 /**
@@ -211,13 +298,15 @@ classify_lane(const Source& source, const details::Definition& detail, std::uint
         if (active == members.end()) {
             return {};
         }
-        const LaneResult completed = complete_lane(source, *active);
+        const LaneResult completed = complete_lane(
+            source, *active, details::kUnavailableItemIndex, detail.socketTypes[lane]);
         return completed.catalyst && completed.effectDefinitionIndex == *active ? completed
                                                                                 : LaneResult{};
     }
 
     std::size_t progressCount = 0;
     std::size_t completedCount = 0;
+    std::uint16_t progressIndex = details::kUnavailableItemIndex;
     std::uint16_t completedIndex = details::kUnavailableItemIndex;
     for (const std::uint16_t member : members) {
         if (member == defaultIndex) {
@@ -233,13 +322,15 @@ classify_lane(const Source& source, const details::Definition& detail, std::uint
         }
         if (plugDetail->maxStackSize == 1) {
             ++progressCount;
+            progressIndex = member;
         } else if (plugDetail->maxStackSize > 1) {
             ++completedCount;
             completedIndex = member;
         }
     }
     if (progressCount == 1 && completedCount == 1) {
-        const LaneResult completed = complete_lane(source, completedIndex);
+        const LaneResult completed =
+            complete_lane(source, completedIndex, progressIndex, detail.socketTypes[lane]);
         return completed.catalyst && completed.effectDefinitionIndex != completedIndex
                    ? completed
                    : LaneResult{};
@@ -362,8 +453,13 @@ bool derive(const Source& source,
                 unclear = Error::ambiguousLifecycle;
                 continue;
             }
-            completed = CompletedCatalyst{
-                lane, result.completedPlugDefinitionIndex, result.effectDefinitionIndex};
+            completed = CompletedCatalyst{lane,
+                                          result.completedPlugDefinitionIndex,
+                                          result.progressPlugDefinitionIndex,
+                                          result.effectDefinitionIndex,
+                                          result.acquisitionDefinitionIndex,
+                                          result.completion,
+                                          result.objective};
         }
         if (!completed.has_value() && !unclear.has_value()) {
             continue;
@@ -372,12 +468,15 @@ bool derive(const Source& source,
             if (release.has_value()) {
                 return fail(output, count, report, *unclear, item->definitionHash, detectedLane);
             }
-            const Definition unsupported{item->definitionHash,
-                                         item->definitionIndex,
-                                         details::kUnavailableItemIndex,
-                                         details::kUnavailableItemIndex,
-                                         detectedLane,
-                                         Availability::unsupported};
+            Definition unsupported{};
+            unsupported.itemDefinitionHash = item->definitionHash;
+            unsupported.itemDefinitionIndex = item->definitionIndex;
+            unsupported.completedPlugDefinitionIndex = details::kUnavailableItemIndex;
+            unsupported.progressPlugDefinitionIndex = details::kUnavailableItemIndex;
+            unsupported.effectDefinitionIndex = details::kUnavailableItemIndex;
+            unsupported.acquisitionDefinitionIndex = kUnavailableAcquisitionIndex;
+            unsupported.socketLane = detectedLane;
+            unsupported.availability = Availability::unsupported;
             if (!append_definition(output, count, report, unsupported)) {
                 return false;
             }
@@ -394,13 +493,18 @@ bool derive(const Source& source,
                             completed->socketLane);
             }
         }
-        const Definition definition{item->definitionHash,
-                                    item->definitionIndex,
-                                    completed->completedPlugDefinitionIndex,
-                                    completed->effectDefinitionIndex,
-                                    completed->socketLane,
-                                    release.has_value() ? Availability::released
-                                                        : Availability::placeholder};
+        Definition definition{};
+        definition.itemDefinitionHash = item->definitionHash;
+        definition.itemDefinitionIndex = item->definitionIndex;
+        definition.completedPlugDefinitionIndex = completed->completedPlugDefinitionIndex;
+        definition.progressPlugDefinitionIndex = completed->progressPlugDefinitionIndex;
+        definition.effectDefinitionIndex = completed->effectDefinitionIndex;
+        definition.acquisitionDefinitionIndex = completed->acquisitionDefinitionIndex;
+        definition.completion = completed->completion;
+        definition.objective = completed->objective;
+        definition.socketLane = completed->socketLane;
+        definition.availability =
+            release.has_value() ? Availability::released : Availability::placeholder;
         if (!append_definition(output, count, report, definition)) {
             return false;
         }
@@ -435,4 +539,133 @@ bool matches_derived(const Source& source,
     return std::equal(expected.begin(), expected.begin() + expectedCount, definitions.begin());
 }
 
+bool matches_cached(const Source& source,
+                    const Facts& facts,
+                    std::span<const Definition> definitions) noexcept {
+    std::array<CompletionCondition, 2 * kDefinitionCapacity> completionConditions{};
+    std::array<AcquisitionGate, kDefinitionCapacity> acquisitionGates{};
+    std::array<std::int32_t, state::unlocks::kObjectiveValueCapacity> objectiveValues{};
+    std::size_t completionCount = 0;
+    std::size_t acquisitionCount = 0;
+    std::size_t objectiveCount = 0;
+
+    const auto appendCondition = [&](const CompletionCondition& condition) {
+        const auto prior = std::find_if(
+            completionConditions.begin(),
+            completionConditions.begin() + completionCount,
+            [&condition](const CompletionCondition& candidate) {
+                return candidate.itemDefinitionIndex == condition.itemDefinitionIndex;
+            });
+        if (prior != completionConditions.begin() + completionCount) {
+            return prior->completion == condition.completion
+                   && prior->objectiveDefinitionIndex == condition.objectiveDefinitionIndex
+                   && prior->state == condition.state;
+        }
+        if (completionCount >= completionConditions.size()) {
+            return false;
+        }
+        completionConditions[completionCount++] = condition;
+        return true;
+    };
+
+    for (const Definition& definition : definitions) {
+        if (definition.availability == Availability::unsupported) {
+            continue;
+        }
+        const details::Definition* detail =
+            find_detail(source.details, definition.itemDefinitionIndex);
+        const bool hasObjective =
+            definition.objective.definitionIndex != kUnavailableObjectiveIndex;
+        if (detail == nullptr || definition.socketLane >= detail->ordinarySocketCount
+            || find_item(source.items, definition.acquisitionDefinitionIndex) == nullptr
+            || definition.completion.flagCount > definition.completion.flags.size()
+            || definition.completion.valueCount > definition.completion.values.size()
+            || hasObjective != (definition.objective.value > 0)
+            || (definition.progressPlugDefinitionIndex != details::kUnavailableItemIndex
+                && find_item(source.items, definition.progressPlugDefinitionIndex) == nullptr)
+            || (hasObjective
+                && (definition.progressPlugDefinitionIndex == details::kUnavailableItemIndex
+                    || definition.objective.definitionIndex >= objectiveValues.size()))) {
+            return false;
+        }
+        for (std::size_t flag = 0; flag < definition.completion.flagCount; ++flag) {
+            if (find_item(source.items, definition.completion.flags[flag]) == nullptr) {
+                return false;
+            }
+        }
+        for (std::size_t value = 0; value < definition.completion.valueCount; ++value) {
+            const CompletionValue& requirement = definition.completion.values[value];
+            if (requirement.index >= kUnavailableCompletionValueIndex
+                || requirement.minimum <= 0) {
+                return false;
+            }
+        }
+
+        const std::uint16_t socketType = detail->socketTypes[definition.socketLane];
+        const auto priorGate = std::find_if(
+            acquisitionGates.begin(),
+            acquisitionGates.begin() + acquisitionCount,
+            [socketType](const AcquisitionGate& gate) { return gate.socketType == socketType; });
+        if (priorGate != acquisitionGates.begin() + acquisitionCount) {
+            if (priorGate->definitionIndex != definition.acquisitionDefinitionIndex) {
+                return false;
+            }
+        } else if (acquisitionCount >= acquisitionGates.size()) {
+            return false;
+        } else {
+            acquisitionGates[acquisitionCount++] = {
+                socketType, definition.acquisitionDefinitionIndex, AcquisitionState::present};
+        }
+
+        CompletionCondition effect{};
+        effect.itemDefinitionIndex = definition.effectDefinitionIndex;
+        effect.completion = definition.completion;
+        effect.objectiveDefinitionIndex = kUnavailableObjectiveIndex;
+        effect.state = CompletionConditionState::present;
+        if (!appendCondition(effect)) {
+            return false;
+        }
+        if (definition.progressPlugDefinitionIndex != details::kUnavailableItemIndex) {
+            CompletionCondition progress{};
+            progress.itemDefinitionIndex = definition.progressPlugDefinitionIndex;
+            if (hasObjective) {
+                progress.objectiveDefinitionIndex = definition.objective.definitionIndex;
+                progress.state = CompletionConditionState::present;
+            } else {
+                progress.objectiveDefinitionIndex = kUnavailableObjectiveIndex;
+                progress.state = CompletionConditionState::absent;
+            }
+            if (!appendCondition(progress)) {
+                return false;
+            }
+        }
+        if (hasObjective) {
+            std::int32_t& objective = objectiveValues[definition.objective.definitionIndex];
+            if (objective != 0 && objective != definition.objective.value) {
+                return false;
+            }
+            objective = definition.objective.value;
+            objectiveCount = (std::max)(objectiveCount,
+                                        static_cast<std::size_t>(
+                                            definition.objective.definitionIndex)
+                                            + 1);
+        }
+    }
+    std::sort(completionConditions.begin(),
+              completionConditions.begin() + completionCount,
+              [](const CompletionCondition& left, const CompletionCondition& right) {
+                  return left.itemDefinitionIndex < right.itemDefinitionIndex;
+              });
+    std::sort(acquisitionGates.begin(),
+              acquisitionGates.begin() + acquisitionCount,
+              [](const AcquisitionGate& left, const AcquisitionGate& right) {
+                  return left.socketType < right.socketType;
+              });
+    Source rebuilt = source;
+    rebuilt.completionConditions = std::span(completionConditions).first(completionCount);
+    rebuilt.acquisitionGates = std::span(acquisitionGates).first(acquisitionCount);
+    rebuilt.objectiveCompletionValues = std::span(objectiveValues).first(objectiveCount);
+    return matches_derived(rebuilt, facts, definitions);
+}
+
 } // namespace sunrise::state::build_data::items::catalysts

+ 19 - 0
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.h

@@ -28,6 +28,12 @@ struct Source {
     std::span<const socket_plugs::Rule> socketPlugRules;
     std::span<const socket_plugs::Pool> socketPlugPools;
     std::span<const socket_plugs::Member> socketPlugMembers;
+    /** Dense item-indexed completion conditions read from item definition expressions. */
+    std::span<const CompletionCondition> completionConditions;
+    /** Dense socket-type-indexed acquired-state gates read from socket type definitions. */
+    std::span<const AcquisitionGate> acquisitionGates;
+    /** Dense objective-indexed completion values read from the installed objective table. */
+    std::span<const std::int32_t> objectiveCompletionValues;
 };
 
 /** @return The generated facts pinned to Destiny 2 build 86657.20.08.23. */
@@ -67,4 +73,17 @@ struct Source {
                                    const Facts& facts,
                                    std::span<const Definition> definitions) noexcept;
 
+/**
+ * Rebuilds package-only transient relations from a stored catalog, then re-derives it.
+ * The stored acquisition and completion flag rows must still name installed items.
+ * Conflicting rows fail closed.
+ * @param source Installed cache domains. Its transient spans are ignored.
+ * @param facts Build-scoped role and release facts.
+ * @param definitions Stored candidate catalog.
+ * @return True when one fresh derivation exactly matches the stored catalog.
+ */
+[[nodiscard]] bool matches_cached(const Source& source,
+                                  const Facts& facts,
+                                  std::span<const Definition> definitions) noexcept;
+
 } // namespace sunrise::state::build_data::items::catalysts

+ 178 - 7
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp

@@ -4,6 +4,7 @@
 #include <atomic>
 
 #include "../../../account/inventory/item_state.h"
+#include "../../../unlocks/definition.h"
 #include "../../table.h"
 #include "../details/definition.h"
 
@@ -32,6 +33,100 @@ std::atomic<bool> g_completionEnabled{true};
                : nullptr;
 }
 
+/** Sets one flag slot and removes duplicate authored rows for that slot. */
+[[nodiscard]] bool upsert_flag(state::Family5State& family, std::uint16_t slot) noexcept {
+    const std::size_t oldCount = family.flagCount;
+    std::size_t write = 0;
+    bool found = false;
+    for (std::size_t read = 0; read < oldCount; ++read) {
+        const state::UnlockFlagOverride row = family.flags[read];
+        if (row.slot == slot) {
+            if (!found) {
+                family.flags[write++] = {slot, state::unlocks::kFlagSet};
+                found = true;
+            }
+            continue;
+        }
+        family.flags[write++] = row;
+    }
+    if (!found) {
+        if (write >= family.flags.size()) {
+            return false;
+        }
+        family.flags[write++] = {slot, state::unlocks::kFlagSet};
+    }
+    for (std::size_t index = write; index < oldCount; ++index) {
+        family.flags[index] = {};
+    }
+    family.flagCount = write;
+    return true;
+}
+
+/** Raises one signed value slot and removes duplicate authored rows for that slot. */
+[[nodiscard]] bool upsert_completion_value(state::Family5State& family,
+                                           std::uint16_t slot,
+                                           std::int32_t minimum) noexcept {
+    const std::size_t oldCount = family.valueCount;
+    std::size_t write = 0;
+    std::optional<std::size_t> mergedIndex;
+    for (std::size_t read = 0; read < oldCount; ++read) {
+        const state::UnlockValueOverride row = family.values[read];
+        if (row.slot == slot) {
+            if (!mergedIndex.has_value()) {
+                mergedIndex = write;
+                family.values[write++] = {slot, (std::max)(row.value, minimum)};
+            } else {
+                state::UnlockValueOverride& merged = family.values[*mergedIndex];
+                merged.value = (std::max)(merged.value, row.value);
+            }
+            continue;
+        }
+        family.values[write++] = row;
+    }
+    if (!mergedIndex.has_value()) {
+        if (write >= family.values.size()) {
+            return false;
+        }
+        family.values[write++] = {slot, minimum};
+    }
+    for (std::size_t index = write; index < oldCount; ++index) {
+        family.values[index] = {};
+    }
+    family.valueCount = write;
+    return true;
+}
+
+/** @return True when one requirement set uses a strict prefix and a zero tail. */
+[[nodiscard]] bool valid_completion(const CompletionRequirements& completion) noexcept {
+    if (completion.flagCount > completion.flags.size()
+        || completion.valueCount > completion.values.size()
+        || (completion.flagCount == 0 && completion.valueCount == 0)) {
+        return false;
+    }
+    for (std::size_t index = 0; index < completion.flags.size(); ++index) {
+        if (index < completion.flagCount) {
+            if (completion.flags[index] >= kUnavailableCompletionFlagIndex
+                || (index != 0 && completion.flags[index - 1] >= completion.flags[index])) {
+                return false;
+            }
+        } else if (completion.flags[index] != 0) {
+            return false;
+        }
+    }
+    for (std::size_t index = 0; index < completion.values.size(); ++index) {
+        const CompletionValue& value = completion.values[index];
+        if (index < completion.valueCount) {
+            if (value.index >= kUnavailableCompletionValueIndex || value.minimum <= 0
+                || (index != 0 && completion.values[index - 1].index >= value.index)) {
+                return false;
+            }
+        } else if (value.index != 0 || value.minimum != 0) {
+            return false;
+        }
+    }
+    return true;
+}
+
 } // namespace
 
 void clear() noexcept {
@@ -56,13 +151,29 @@ bool valid(std::span<const Definition> definitions) noexcept {
         const bool hasCompletedPlug =
             definition.completedPlugDefinitionIndex != details::kUnavailableItemIndex;
         const bool hasEffect = definition.effectDefinitionIndex != details::kUnavailableItemIndex;
+        const bool hasProgress =
+            definition.progressPlugDefinitionIndex != details::kUnavailableItemIndex;
+        const bool hasAcquisition =
+            definition.acquisitionDefinitionIndex != kUnavailableAcquisitionIndex;
+        const bool hasObjective =
+            definition.objective.definitionIndex != kUnavailableObjectiveIndex;
+        const bool directEffect =
+            definition.completedPlugDefinitionIndex == definition.effectDefinitionIndex;
         if (definition.itemDefinitionHash == 0
             || definition.socketLane >= details::kInitialPlugCapacity
             || !valid_availability(definition.availability)
             || (definition.availability == Availability::unsupported
-                && (hasCompletedPlug || hasEffect))
+                && (hasCompletedPlug || hasProgress || hasEffect || hasAcquisition
+                    || definition.completion.flagCount != 0
+                    || definition.completion.valueCount != 0 || hasObjective
+                    || definition.objective.value != 0))
+            || (definition.availability != Availability::unsupported
+                && (!hasCompletedPlug || !hasEffect || !hasAcquisition
+                    || !valid_completion(definition.completion)))
+            || (hasObjective != (definition.objective.value > 0))
             || (definition.availability != Availability::unsupported
-                && (!hasCompletedPlug || !hasEffect))
+                && ((directEffect && (hasProgress || hasObjective))
+                    || (!directEffect && !hasProgress)))
             || (index != 0 && !definition_index_less(definitions[index - 1], definition))) {
             return false;
         }
@@ -94,7 +205,11 @@ Result resolve(std::uint16_t itemDefinitionIndex) noexcept {
             Availability::released,
             {definition->socketLane,
              definition->completedPlugDefinitionIndex,
-             definition->effectDefinitionIndex}};
+             definition->progressPlugDefinitionIndex,
+             definition->effectDefinitionIndex,
+             definition->acquisitionDefinitionIndex,
+             definition->completion,
+             definition->objective}};
 }
 
 std::uint16_t resolve_effect(std::uint16_t itemDefinitionIndex,
@@ -104,7 +219,8 @@ std::uint16_t resolve_effect(std::uint16_t itemDefinitionIndex,
     const Definition* definition = find(g_definitions.rows(), itemDefinitionIndex);
     if (definition == nullptr || definition->availability != Availability::released
         || definition->socketLane != socketLane
-        || definition->completedPlugDefinitionIndex != plugDefinitionIndex
+        || (definition->completedPlugDefinitionIndex != plugDefinitionIndex
+            && definition->effectDefinitionIndex != plugDefinitionIndex)
         || definition->effectDefinitionIndex == details::kUnavailableItemIndex) {
         return plugDefinitionIndex;
     }
@@ -129,18 +245,73 @@ ApplyResult apply_completed(std::uint16_t itemDefinitionIndex,
     }
     if (result.error != Error::none || !account::inventory::valid_item_state(flags)
         || result.completed.socketLane >= plugs.size()
-        || result.completed.completedPlugDefinitionIndex == details::kUnavailableItemIndex) {
+        || result.completed.effectDefinitionIndex == details::kUnavailableItemIndex) {
         return ApplyResult::failed;
     }
 
     const std::uint32_t completedFlags = flags | account::inventory::kMasterworkItemFlag;
-    const std::optional<std::uint16_t> completedPlug =
-        result.completed.completedPlugDefinitionIndex;
+    const std::optional<std::uint16_t> completedPlug = result.completed.effectDefinitionIndex;
     plugs[result.completed.socketLane] = completedPlug;
     flags = completedFlags;
     return ApplyResult::completed;
 }
 
+bool append_investment_overrides(state::Family5State& family) noexcept {
+    if (!completion_enabled()) {
+        return true;
+    }
+    if (family.flagCount > family.flags.size() || family.valueCount > family.values.size()) {
+        return false;
+    }
+
+    state::Family5State candidate = family;
+    const Lock::Shared guard(g_lock);
+    for (const Definition& definition : g_definitions.rows()) {
+        if (definition.availability != Availability::released) {
+            continue;
+        }
+        if (!upsert_flag(candidate, definition.acquisitionDefinitionIndex)) {
+            return false;
+        }
+        for (std::size_t flag = 0; flag < definition.completion.flagCount; ++flag) {
+            if (!upsert_flag(candidate, definition.completion.flags[flag])) {
+                return false;
+            }
+        }
+        for (std::size_t value = 0; value < definition.completion.valueCount; ++value) {
+            const CompletionValue& requirement = definition.completion.values[value];
+            if (!upsert_completion_value(candidate, requirement.index, requirement.minimum)) {
+                return false;
+            }
+        }
+    }
+    family = candidate;
+    return true;
+}
+
+bool append_objective_completions(std::span<std::int32_t> values) noexcept {
+    if (!completion_enabled()) {
+        return true;
+    }
+    const Lock::Shared guard(g_lock);
+    for (const Definition& definition : g_definitions.rows()) {
+        if (definition.availability == Availability::released
+            && definition.objective.definitionIndex != kUnavailableObjectiveIndex
+            && (definition.objective.definitionIndex >= values.size()
+                || definition.objective.value <= 0)) {
+            return false;
+        }
+    }
+    for (const Definition& definition : g_definitions.rows()) {
+        if (definition.availability == Availability::released
+            && definition.objective.definitionIndex != kUnavailableObjectiveIndex) {
+            std::int32_t& value = values[definition.objective.definitionIndex];
+            value = (std::max)(value, definition.objective.value);
+        }
+    }
+    return true;
+}
+
 bool snapshot(std::span<Definition> output, std::size_t& outputCount) noexcept {
     outputCount = 0;
     const Lock::Shared guard(g_lock);

+ 18 - 0
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h

@@ -5,6 +5,7 @@
 #include <optional>
 #include <span>
 
+#include "../../../investment/investment.h"
 #include "definition.h"
 
 namespace sunrise::state::build_data::items::catalysts {
@@ -68,6 +69,23 @@ void set_completion_enabled(bool enabled) noexcept;
                                           std::uint32_t& flags,
                                           std::span<std::optional<std::uint16_t>> plugs) noexcept;
 
+/**
+ * Adds acquired-state gates, completion flags, and completion values for released catalysts.
+ * Existing authored rows with the same slot are raised to the required value. The input stays
+ * unchanged when either fixed override bank cannot hold the complete deduplicated result.
+ * @param family Candidate Family-5 state.
+ * @return True when completion is disabled or every released override fits atomically.
+ */
+[[nodiscard]] bool append_investment_overrides(state::Family5State& family) noexcept;
+
+/**
+ * Raises account objective values required by released legacy catalysts.
+ * The input stays unchanged if any derived objective is outside the supplied bank.
+ * @param values Candidate account objective bank.
+ * @return True when completion is disabled or every objective applies atomically.
+ */
+[[nodiscard]] bool append_objective_completions(std::span<std::int32_t> values) noexcept;
+
 /**
  * Copies the complete catalog under its shared lock.
  * @param output Caller-owned fixed catalog storage.

+ 15 - 0
Sunrise/src/state/build_data/runtime.h

@@ -7,6 +7,7 @@
 #include <span>
 #include <string_view>
 
+#include "../investment/investment.h"
 #include "abilities/definition.h"
 #include "collectibles/collectible_catalog.h"
 #include "constants/definition.h"
@@ -232,6 +233,20 @@ complete_exotic_catalyst(std::uint16_t itemDefinitionIndex,
                          std::uint32_t& flags,
                          std::span<std::optional<std::uint16_t>> plugs) noexcept;
 
+/**
+ * Adds all released catalyst acquisition and completion overrides to one Family-5 snapshot.
+ * @param family Candidate Family-5 state.
+ * @return True when all overrides fit and the complete state commits.
+ */
+[[nodiscard]] bool complete_exotic_catalyst_investment(Family5State& family) noexcept;
+
+/**
+ * Raises every account objective required by a released legacy catalyst.
+ * @param values Candidate account objective bank.
+ * @return True when all derived objectives fit and apply.
+ */
+[[nodiscard]] bool complete_exotic_catalyst_objectives(std::span<std::int32_t> values) noexcept;
+
 /**
  * Resolves the item row that supplies one socketed catalyst's native perks and stat changes.
  * @param itemDefinitionIndex Native weapon definition index.

+ 1 - 1
Sunrise/src/state/investment/investment.h

@@ -7,7 +7,7 @@
 namespace sunrise::state {
 
 /** Fixed runtime capacity for safe account unlock overrides. */
-inline constexpr std::size_t kUnlockOverrideCapacity = 100;
+inline constexpr std::size_t kUnlockOverrideCapacity = 127;
 
 /** One logical unlock-flag value stored by slot. */
 struct UnlockFlagOverride {

+ 6 - 2
Sunrise/src/state/runtime/runtime.h

@@ -492,7 +492,11 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 /** @return A copy of the active account state, read under the lock. */
 [[nodiscard]] AccountState account_snapshot() noexcept;
 
-/** @return A copy of the evaluated content state, read under the lock. */
-[[nodiscard]] InvestmentState investment_snapshot() noexcept;
+/**
+ * Copies the evaluated content state and adds build-derived catalyst completion overrides.
+ * @param output Receives one complete Family-5 snapshot on success.
+ * @return False when the fixed override banks cannot hold the complete state.
+ */
+[[nodiscard]] bool investment_snapshot(InvestmentState& output) noexcept;
 
 } // namespace sunrise::state

+ 8 - 4
Sunrise/src/state/runtime/state_runtime.cpp

@@ -364,12 +364,16 @@ bool new_bap_session(BapState& output) noexcept {
     return true;
 }
 
-/** @return A copy of the evaluated content state, read under the lock. */
-InvestmentState investment_snapshot() noexcept {
+/** Copies one complete evaluated content state with build-derived catalyst overrides. */
+bool investment_snapshot(InvestmentState& output) noexcept {
     AcquireSRWLockShared(&runtime::storage::g_stateLock);
-    const InvestmentState snapshot = runtime::storage::g_state.investment;
+    InvestmentState snapshot = runtime::storage::g_state.investment;
     ReleaseSRWLockShared(&runtime::storage::g_stateLock);
-    return snapshot;
+    if (!build_data::complete_exotic_catalyst_investment(snapshot.family5)) {
+        return false;
+    }
+    output = snapshot;
+    return true;
 }
 
 } // namespace sunrise::state