소스 검색

fix(investment): address catalyst review findings

y9522 2 주 전
부모
커밋
9cafa1d7d2
38개의 변경된 파일1266개의 추가작업 그리고 1181개의 파일을 삭제
  1. 0 1
      Sunrise/Sunrise.vcxproj
  2. 3 9
      Sunrise/src/client/content/items/packages/internal.h
  3. 3 4
      Sunrise/src/client/content/items/packages/package_build_report.cpp
  4. 0 108
      Sunrise/src/client/content/items/packages/package_catalyst_condition_reader.cpp
  5. 2 15
      Sunrise/src/client/content/items/packages/package_item_build.cpp
  6. 15 33
      Sunrise/src/client/content/items/packages/package_item_rows.cpp
  7. 11 87
      Sunrise/src/client/content/items/packages/package_socket_plug_build.cpp
  8. 7 34
      Sunrise/src/client/content/items/packages/package_socket_plug_build.h
  9. 0 4
      Sunrise/src/core/settings/parser.h
  10. 0 9
      Sunrise/src/middleware/content/packages/tables/definition_index_table.h
  11. 1 2
      Sunrise/src/middleware/datagen/character_record/appearance/character_appearance_stats.cpp
  12. 6 5
      Sunrise/src/middleware/datagen/family4/loadout/loadout_item_resolver.cpp
  13. 1 1
      Sunrise/src/middleware/web_service/messages/opcode406_codec.cpp
  14. 5 6
      Sunrise/src/server/web_service/web_service_runtime.cpp
  15. 2 4
      Sunrise/src/state/build_data/build_data_runtime.cpp
  16. 4 4
      Sunrise/src/state/build_data/cache/read/cache_payload_reader.cpp
  17. 11 16
      Sunrise/src/state/build_data/cache/records/cache_detail_links.cpp
  18. 8 9
      Sunrise/src/state/build_data/cache/records/cache_domain_validation.cpp
  19. 5 19
      Sunrise/src/state/build_data/cache/records/cache_exotic_catalyst_record_codec.cpp
  20. 4 2
      Sunrise/src/state/build_data/cache/records/codec.h
  21. 2 7
      Sunrise/src/state/build_data/cache/records/format.h
  22. 8 15
      Sunrise/src/state/build_data/cache/records/validation.h
  23. 9 63
      Sunrise/src/state/build_data/items/catalysts/definition.h
  24. 52 114
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_build_data_runtime.cpp
  25. 137 305
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp
  26. 1 24
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.h
  27. 15 165
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp
  28. 4 20
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h
  29. 15 34
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_generated.cpp
  30. 22 35
      Sunrise/src/state/build_data/runtime.h
  31. 8 12
      Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp
  32. 2 6
      Sunrise/src/state/runtime/runtime.h
  33. 4 9
      Sunrise/src/state/runtime/state_runtime.cpp
  34. 118 0
      tests/Sunrise.Tests.vcxproj
  35. 348 0
      tests/catalyst_regression_tests.cpp
  36. 226 0
      tests/loadout_catalyst_regression_tests.cpp
  37. 67 0
      tests/opcode406_item_state_regression_tests.cpp
  38. 140 0
      tests/settings_regression_tests.cpp

+ 0 - 1
Sunrise/Sunrise.vcxproj

@@ -1020,7 +1020,6 @@
     <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" />

+ 3 - 9
Sunrise/src/client/content/items/packages/internal.h

@@ -62,12 +62,6 @@ 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 legacy 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{};
     /** 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. */
@@ -311,9 +305,9 @@ void report_row_count(std::size_t walked,
 
 /** Reports the exact socket-rule, deduplicated-pool, member, and skipped-lane counts. */
 void report_socket_plug_count(std::size_t rules,
-                               std::size_t pools,
-                               std::size_t members,
-                               std::size_t skipped) noexcept;
+                              std::size_t pools,
+                              std::size_t members,
+                              std::size_t skipped) noexcept;
 
 /**
  * Reports released, placeholder, and unsupported catalyst catalog counts.

+ 3 - 4
Sunrise/src/client/content/items/packages/package_build_report.cpp

@@ -145,12 +145,11 @@ void report_catalyst_catalog(const state::build_data::items::catalysts::Report&
         report.itemDefinitionHash,
         static_cast<unsigned>(report.socketLane));
     if (written > 0) {
-        const core::log::Level level = !built                  ? core::log::Level::error
+        const core::log::Level level = !built                    ? core::log::Level::error
                                        : report.unsupported != 0 ? core::log::Level::warn
                                                                  : core::log::Level::info;
-        core::log::write(core::log::Channel::client,
-                         level,
-                         {line.data(), static_cast<std::size_t>(written)});
+        core::log::write(
+            core::log::Channel::client, level, {line.data(), static_cast<std::size_t>(written)});
     }
 }
 

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

@@ -1,108 +0,0 @@
-#include "package_socket_plug_build.h"
-
-#include <array>
-#include <cstring>
-#include <limits>
-
-#include "../../../../middleware/content/packages/tables/definition_index_table.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 kFlagExpressionTokenCount = 1;
-constexpr std::size_t kValueExpressionTokenCount = 3;
-
-/** Marks a conflicting native completion rule and clears its partial operands. */
-void mark_ambiguous(catalysts::CompletionCondition& output) noexcept {
-    output.flagDefinitionIndex = catalysts::kUnavailableCompletionFlagIndex;
-    output.valueIndex = catalysts::kUnavailableCompletionValueIndex;
-    output.value = 0;
-    output.state = catalysts::CompletionConditionState::ambiguous;
-}
-
-} // namespace
-
-/**
- * Finds the unique flag and `value >= literal` completion rules in one effect item.
- * @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 {
-    output = {};
-    output.itemDefinitionIndex = itemDefinitionIndex;
-    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::kInvestmentExpressionRowClass
-            || expression.dataOffset > definition.size()) {
-            continue;
-        }
-
-        if (expression.count == kFlagExpressionTokenCount
-            && definition.size() - expression.dataOffset
-                   >= kFlagExpressionTokenCount * kExpressionTokenSize) {
-            std::array<std::uint32_t, kFlagExpressionTokenCount * 2> tokens{};
-            std::memcpy(tokens.data(),
-                        definition.data() + expression.dataOffset,
-                        tokens.size() * sizeof(tokens.front()));
-            if (tokens[0] != kFlagOpcode
-                || tokens[1] >= state::build_data::items::kDefinitionCapacity) {
-                continue;
-            }
-            const auto flagDefinitionIndex = static_cast<std::uint16_t>(tokens[1]);
-            if (output.flagDefinitionIndex == catalysts::kUnavailableCompletionFlagIndex) {
-                output.flagDefinitionIndex = flagDefinitionIndex;
-                output.state = catalysts::CompletionConditionState::present;
-            } else if (output.flagDefinitionIndex != flagDefinitionIndex) {
-                mark_ambiguous(output);
-                return;
-            }
-            continue;
-        }
-
-        if (expression.count != kValueExpressionTokenCount
-            || definition.size() - expression.dataOffset
-                   < kValueExpressionTokenCount * kExpressionTokenSize) {
-            continue;
-        }
-        std::array<std::uint32_t, kValueExpressionTokenCount * 2> tokens{};
-        std::memcpy(tokens.data(),
-                    definition.data() + expression.dataOffset,
-                    tokens.size() * sizeof(tokens.front()));
-        if (tokens[0] != kValueOpcode || 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;
-        }
-        const auto valueIndex = static_cast<std::uint16_t>(tokens[1]);
-        const auto value = static_cast<std::int32_t>(tokens[3]);
-        if (output.valueIndex == catalysts::kUnavailableCompletionValueIndex) {
-            output.valueIndex = valueIndex;
-            output.value = value;
-            output.state = catalysts::CompletionConditionState::present;
-        } else if (output.valueIndex != valueIndex || output.value != value) {
-            mark_ambiguous(output);
-            return;
-        }
-    }
-}
-
-} // namespace sunrise::client::content::items::packages

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

@@ -42,8 +42,7 @@ 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::exotic_catalysts_ready();
+           && state::build_data::investment_constants_ready();
 }
 
 /** @return True when every item and investment-root domain is published. */
@@ -58,8 +57,7 @@ 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::exotic_catalysts_ready();
+           && state::build_data::investment_constants_ready();
 }
 
 } // namespace
@@ -140,17 +138,6 @@ 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)) {
-                    continue;
-                }
-            }
             reason = "buckets";
             if (!build_buckets(source, storage, std::span<const std::byte>{storage.root})) {
                 continue;

+ 15 - 33
Sunrise/src/client/content/items/packages/package_item_rows.cpp

@@ -1,7 +1,7 @@
 #include <array>
 #include <span>
-#include <vector>
 
+#include "../../../../state/build_data/items/catalysts/exotic_catalyst_builder.h"
 #include "../../../../state/build_data/items/details/item_detail_catalog.h"
 #include "../../../../state/build_data/runtime.h"
 #include "internal.h"
@@ -55,15 +55,6 @@ 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";
@@ -156,13 +147,6 @@ 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},
@@ -180,21 +164,20 @@ bool build_item_rows(const reader::Source& source,
             catalystRows{};
         std::size_t catalystCount = 0;
         state::build_data::items::catalysts::Report catalystReport{};
+        const state::build_data::items::catalysts::Source catalystSource{
+            {},
+            std::span(storage.rows).first(rowCount),
+            std::span(storage.details).first(builtDetailCount),
+            socketPlugBuild.rules(),
+            socketPlugBuild.pools(),
+            socketPlugBuild.members(),
+        };
+        bool catalystBuilt = false;
         if (published && needCatalysts) {
             reason = "exotic_catalysts";
-            const bool built = state::build_data::derive_exotic_catalysts(
-                std::span(storage.rows).first(rowCount),
-                std::span(storage.details).first(builtDetailCount),
-                socketPlugBuild.rules(),
-                socketPlugBuild.pools(),
-                socketPlugBuild.members(),
-                storage.catalystCompletionConditions,
-                storage.catalystAcquisitionGates,
-                catalystRows,
-                catalystCount,
-                catalystReport);
-            report_catalyst_catalog(catalystReport, built);
-            published = built;
+            catalystBuilt = state::build_data::derive_exotic_catalysts(
+                catalystSource, catalystRows, catalystCount, catalystReport);
+            report_catalyst_catalog(catalystReport, catalystBuilt);
         }
         if (published && needSocketPlugs) {
             const std::size_t rules = socketPlugBuild.rule_count();
@@ -207,10 +190,10 @@ bool build_item_rows(const reader::Source& source,
                 report_socket_plug_count(rules, pools, members, skipped);
             }
         }
-        if (published && needCatalysts) {
+        if (published && needCatalysts && catalystBuilt) {
             reason = "exotic_catalysts";
             published = state::build_data::publish_exotic_catalysts(
-                std::span(catalystRows).first(catalystCount));
+                catalystSource, std::span(catalystRows).first(catalystCount));
         }
     }
     // Ability buckets read the socket entry list table again and depend on the detail domain, so
@@ -244,7 +227,6 @@ 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();
 }
 

+ 11 - 87
Sunrise/src/client/content/items/packages/package_socket_plug_build.cpp

@@ -1,10 +1,8 @@
 #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 {
@@ -27,27 +25,17 @@ 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 {
     SocketPlugBuild* build{};
     std::size_t itemDefinitionCount{};
 };
 
-/** @return Whether the package-provided member was accepted into bounded scratch. */
+/**
+ * @param opaque Visitor context supplied by the socket build.
+ * @param itemDefinitionIndex Package-provided plug index.
+ * @return True when the plug was accepted into bounded scratch.
+ */
 [[nodiscard]] bool visit_member(void* opaque, std::uint32_t itemDefinitionIndex) noexcept {
     auto& context = *static_cast<VisitorContext*>(opaque);
     return context.build != nullptr
@@ -56,68 +44,6 @@ struct VisitorContext {
 
 } // namespace
 
-/**
- * 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.
- */
-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;
-}
-
 /** 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) {
@@ -295,15 +221,13 @@ bool SocketPlugBuild::append(const tables::items::Row& item,
     return complete;
 }
 
-/** Publishes the bounded relation and releases all transient interning memory. */
+/** Publishes the bounded relation and retains its rows for dependent package builders. */
 bool SocketPlugBuild::publish() noexcept {
-    const bool published =
-        !rules_.empty() && !pools_.empty() && !members_.empty()
-        && state::build_data::publish_socket_plug_rules(std::span(rules_.data(), ruleCount_),
-                                                        std::span(pools_.data(), poolCount_),
-                                                        std::span(members_.data(), memberCount_));
-    release();
-    return published;
+    return !rules_.empty() && !pools_.empty() && !members_.empty()
+           && state::build_data::publish_socket_plug_rules(
+               std::span(rules_.data(), ruleCount_),
+               std::span(pools_.data(), poolCount_),
+               std::span(members_.data(), memberCount_));
 }
 
 /** Reports how many lanes failed closed during extraction. */

+ 7 - 34
Sunrise/src/client/content/items/packages/package_socket_plug_build.h

@@ -6,47 +6,14 @@
 #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;
-
-/**
- * Finds the unique three-token `value >= literal` completion rule in one effect item.
- * Duplicate expressions with the same values remain one condition. Distinct matches are marked
- * ambiguous so a released catalyst fails before publication.
- * @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 {
@@ -72,16 +39,22 @@ public:
                               std::span<const std::byte> plugSetTable,
                               std::size_t itemDefinitionCount) noexcept;
 
-    /** Publishes the completed exact relation, then releases its transient scratch. */
+    /** Publishes the completed exact relation. The object retains source rows until destruction. */
     [[nodiscard]] bool publish() noexcept;
 
     /** @return Socket lanes skipped because their package lists were malformed or over capacity. */
     [[nodiscard]] std::size_t skipped() const noexcept;
+    /** @return Number of extracted socket rules. */
     [[nodiscard]] std::size_t rule_count() const noexcept;
+    /** @return Number of interned socket pools. */
     [[nodiscard]] std::size_t pool_count() const noexcept;
+    /** @return Number of members in all interned socket pools. */
     [[nodiscard]] std::size_t member_count() const noexcept;
+    /** @return Extracted socket rules, valid until this object changes or is destroyed. */
     [[nodiscard]] std::span<const socket_plugs::Rule> rules() const noexcept;
+    /** @return Interned socket pools, valid until this object changes or is destroyed. */
     [[nodiscard]] std::span<const socket_plugs::Pool> pools() const noexcept;
+    /** @return Interned socket members, valid until this object changes or is destroyed. */
     [[nodiscard]] std::span<const socket_plugs::Member> members() const noexcept;
 
     /** Package-list visitor entry point; accepts only an in-range bounded native index. */

+ 0 - 4
Sunrise/src/core/settings/parser.h

@@ -11,8 +11,6 @@
 
 namespace sunrise::core::settings::parser {
 
-struct ParserTestAccess;
-
 /**
  * Fixed-storage JSON reader for the supported Core settings.
  * Every parse below writes `output` only once its whole object is valid, and skips unknown keys.
@@ -27,8 +25,6 @@ public:
     [[nodiscard]] bool parse_root(Settings& output) noexcept;
 
 private:
-    friend struct ParserTestAccess;
-
     /** Parses the Core settings object. */
     [[nodiscard]] bool core(Settings& output) noexcept;
     /** Parses the activity SDK generation boot gate. `enabled` must be unique and boolean. */

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

@@ -183,15 +183,6 @@ 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;
 /** 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. */

+ 1 - 2
Sunrise/src/middleware/datagen/character_record/appearance/character_appearance_stats.cpp

@@ -63,8 +63,7 @@ collect_rows(const Equipped& equipped, std::span<std::uint8_t> rows, std::size_t
     const std::size_t lanes = equipped.laneCount + 1;
     for (std::size_t source = 0; source < lanes; ++source) {
         const std::uint16_t definitionIndex =
-            source == 0 ? equipped.definitionIndex
-                        : resolve_effective_plug(equipped, source - 1);
+            source == 0 ? equipped.definitionIndex : resolve_effective_plug(equipped, source - 1);
         details::Definition detail{};
         if (definitionIndex == details::kUnavailableItemIndex
             || !state::build_data::find_configured_item_detail(definitionIndex, detail)) {

+ 6 - 5
Sunrise/src/middleware/datagen/family4/loadout/loadout_item_resolver.cpp

@@ -162,12 +162,13 @@ bool resolve_item(const authored_inventory::Item& authored,
                                      candidate.item.instance.ordinarySockets)) {
         return false;
     }
+    std::uint32_t completedFlags = candidate.item.flags;
+    auto completedPlugs = candidate.item.instance.ordinarySockets.plugs;
     if (state::build_data::complete_exotic_catalyst(
-            itemDefinition.definitionIndex,
-            candidate.item.flags,
-            candidate.item.instance.ordinarySockets.plugs)
-        == state::build_data::items::catalysts::ApplyResult::failed) {
-        return false;
+            itemDefinition.definitionIndex, completedFlags, completedPlugs)
+        == state::build_data::items::catalysts::ApplyResult::completed) {
+        candidate.item.flags = completedFlags;
+        candidate.item.instance.ordinarySockets.plugs = completedPlugs;
     }
 
     candidate.item.instance.instanceSoid = authored.instanceSoid;

+ 1 - 1
Sunrise/src/middleware/web_service/messages/opcode406_codec.cpp

@@ -1,7 +1,7 @@
 #include <cstddef>
 
-#include "../../encoding/bit_reader.h"
 #include "../../../state/account/inventory/item_state.h"
+#include "../../encoding/bit_reader.h"
 #include "opcode406.h"
 
 namespace sunrise::middleware::web_service::messages::opcode406 {

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

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

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

@@ -12,7 +12,6 @@
 #include "constants/investment_constant_catalog.h"
 #include "hash_names/hash_name_catalog.h"
 #include "inventory/buckets/inventory_bucket_catalog.h"
-#include "items/catalysts/exotic_catalyst_builder.h"
 #include "items/catalysts/exotic_catalyst_catalog.h"
 #include "items/details/item_detail_catalog.h"
 #include "items/socket_plugs/socket_plug_catalog.h"
@@ -58,8 +57,7 @@ bool initialize(void* module, std::uint64_t configuredEquipmentHash) noexcept {
     if (!core::path::append(persistenceState.cacheDirectory, kCacheDirectorySuffix)
         || !core::path::append(persistenceState.cachePath, kCacheFileSuffix)
         || !cache::current_build_identity(configuredEquipmentHash,
-                                           persistenceState.buildIdentity)
-        || !items::catalysts::matches_target_build(persistenceState.buildIdentity)) {
+                                          persistenceState.buildIdentity)) {
         runtime::persistence::clear_locked(persistenceState);
         ReleaseSRWLockExclusive(&persistenceState.lock);
         return false;
@@ -109,7 +107,7 @@ bool initialize(void* module, std::uint64_t configuredEquipmentHash) noexcept {
         // makes the lists ready, the package build skips itself, and no ability is picked.
         || !socket_entry_lists::replace_entry_tables(domains.socketEntryTables) || !detailsReplaced
         || !items::socket_plugs::replace(
-             domains.socketPlugRules, domains.socketPlugPools, domains.socketPlugMembers)
+            domains.socketPlugRules, domains.socketPlugPools, domains.socketPlugMembers)
         || !items::catalysts::replace(domains.exoticCatalysts)
         || !abilities::replace(domains.abilityBuckets)
         || !progressions::replace(domains.progressions)

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

@@ -72,8 +72,8 @@ void clear(records::MutableDomains output) noexcept {
     std::fill(
         output.socketPlugPools.begin(), output.socketPlugPools.end(), items::socket_plugs::Pool{});
     std::fill(output.socketPlugMembers.begin(),
-               output.socketPlugMembers.end(),
-               items::socket_plugs::Member{});
+              output.socketPlugMembers.end(),
+              items::socket_plugs::Member{});
     std::fill(output.exoticCatalysts.begin(),
               output.exoticCatalysts.end(),
               items::catalysts::Definition{});
@@ -162,8 +162,8 @@ bool read_payload(HANDLE file,
             && read_domain<records::SocketPlugPoolRecord>(
                 file, output.socketPlugPools.first(counts.socketPlugPools), checksum);
     valid = valid
-             && read_domain<records::SocketPlugMemberRecord>(
-                 file, output.socketPlugMembers.first(counts.socketPlugMembers), checksum);
+            && read_domain<records::SocketPlugMemberRecord>(
+                file, output.socketPlugMembers.first(counts.socketPlugMembers), checksum);
     valid = valid
             && read_domain<records::ExoticCatalystRecord>(
                 file, output.exoticCatalysts.first(counts.exoticCatalysts), checksum);

+ 11 - 16
Sunrise/src/state/build_data/cache/records/cache_detail_links.cpp

@@ -1,7 +1,7 @@
 #include <algorithm>
 
-#include "../../items/details/item_detail_catalog.h"
 #include "../../items/catalysts/exotic_catalyst_builder.h"
+#include "../../items/details/item_detail_catalog.h"
 #include "../../items/socket_plugs/socket_plug_catalog.h"
 #include "validation.h"
 
@@ -111,27 +111,22 @@ bool valid_socket_plug_links(std::span<const items::socket_plugs::Rule> rules,
 }
 
 /** Checks each catalyst against its item, detail, pool, plug, and pinned release state. */
-bool valid_exotic_catalyst_links(
-    std::span<const items::catalysts::Definition> catalysts,
-    std::span<const items::Definition> itemDefinitions,
-    std::span<const items::details::Definition> details,
-    std::span<const items::socket_plugs::Rule> rules,
-    std::span<const items::socket_plugs::Pool> pools,
-    std::span<const items::socket_plugs::Member> members) noexcept {
-    if (itemDefinitions.empty() || details.empty()
-        || !items::socket_plugs::valid(rules, pools, members)) {
+bool valid_exotic_catalyst_links(Domains domains) noexcept {
+    if (domains.items.empty() || domains.itemDetails.empty()
+        || !items::socket_plugs::valid(
+            domains.socketPlugRules, domains.socketPlugPools, domains.socketPlugMembers)) {
         return false;
     }
     const items::catalysts::Facts facts = items::catalysts::generated_facts();
     const items::catalysts::Source source{
         {facts.imageTimestamp, facts.imageSize, 0},
-        itemDefinitions,
-        details,
-        rules,
-        pools,
-        members,
+        domains.items,
+        domains.itemDetails,
+        domains.socketPlugRules,
+        domains.socketPlugPools,
+        domains.socketPlugMembers,
     };
-    return items::catalysts::matches_cached(source, facts, catalysts);
+    return items::catalysts::matches_derived(source, facts, domains.exoticCatalysts);
 }
 
 } // namespace sunrise::state::build_data::cache::records

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

@@ -75,7 +75,11 @@ namespace {
            || (left.itemDefinitionIndex == right.itemDefinitionIndex && left.lane < right.lane);
 }
 
-/** @return Native item-index order for catalyst rows. */
+/**
+ * @param left First catalyst row.
+ * @param right Second catalyst row.
+ * @return True when the first native item index is less than the second.
+ */
 [[nodiscard]] bool catalyst_less(const items::catalysts::Definition& left,
                                  const items::catalysts::Definition& right) noexcept {
     return left.itemDefinitionIndex < right.itemDefinitionIndex;
@@ -276,14 +280,9 @@ bool valid_domains(Domains domains) noexcept {
            && valid_socket_plug_links(domains.socketPlugRules,
                                       domains.socketPlugPools,
                                       domains.socketPlugMembers,
-                                       domains.items,
-                                       domains.itemDetails)
-           && valid_exotic_catalyst_links(domains.exoticCatalysts,
-                                           domains.items,
-                                           domains.itemDetails,
-                                           domains.socketPlugRules,
-                                           domains.socketPlugPools,
-                                           domains.socketPlugMembers);
+                                      domains.items,
+                                      domains.itemDetails)
+           && valid_exotic_catalyst_links(domains);
 }
 
 } // namespace sunrise::state::build_data::cache::records

+ 5 - 19
Sunrise/src/state/build_data/cache/records/cache_exotic_catalyst_record_codec.cpp

@@ -2,46 +2,32 @@
 
 namespace sunrise::state::build_data::cache::records {
 
-bool encode(const items::catalysts::Definition& value,
-            ExoticCatalystRecord& record) noexcept {
+bool encode(const items::catalysts::Definition& value, ExoticCatalystRecord& record) noexcept {
     record = {};
-    if (value.availability != items::catalysts::Availability::released
-            && value.availability != items::catalysts::Availability::placeholder
-            && value.availability != items::catalysts::Availability::unsupported) {
+    if (!items::catalysts::valid_availability(value.availability)) {
         return false;
     }
     record.itemDefinitionHash = value.itemDefinitionHash;
     record.itemDefinitionIndex = value.itemDefinitionIndex;
     record.completedPlugDefinitionIndex = value.completedPlugDefinitionIndex;
     record.effectDefinitionIndex = value.effectDefinitionIndex;
-    record.acquisitionDefinitionIndex = value.acquisitionDefinitionIndex;
-    record.completionFlagDefinitionIndex = value.completionFlagDefinitionIndex;
-    record.completionValueIndex = value.completionValueIndex;
     record.socketLane = value.socketLane;
     record.availability = static_cast<std::uint8_t>(value.availability);
-    record.completionValue = value.completionValue;
     return true;
 }
 
-bool decode(const ExoticCatalystRecord& record,
-            items::catalysts::Definition& value) noexcept {
+bool decode(const ExoticCatalystRecord& record, items::catalysts::Definition& value) noexcept {
     value = {};
     const auto availability = static_cast<items::catalysts::Availability>(record.availability);
-    if (availability != items::catalysts::Availability::released
-            && availability != items::catalysts::Availability::placeholder
-            && availability != items::catalysts::Availability::unsupported) {
+    if (!items::catalysts::valid_availability(availability)) {
         return false;
     }
     value = {record.itemDefinitionHash,
              record.itemDefinitionIndex,
              record.completedPlugDefinitionIndex,
              record.effectDefinitionIndex,
-             record.acquisitionDefinitionIndex,
-             record.completionFlagDefinitionIndex,
-             record.completionValueIndex,
              record.socketLane,
-             availability,
-             record.completionValue};
+             availability};
     return true;
 }
 

+ 4 - 2
Sunrise/src/state/build_data/cache/records/codec.h

@@ -139,7 +139,8 @@ namespace sunrise::state::build_data::cache::records {
 
 /**
  * @param value Runtime catalyst relation to pack.
- * @param record Receives the canonical disk form.
+ * @param record Receives the canonical disk
+ * form.
  * @return True when the runtime relation has a supported availability value.
  */
 [[nodiscard]] bool encode(const items::catalysts::Definition& value,
@@ -147,7 +148,8 @@ namespace sunrise::state::build_data::cache::records {
 
 /**
  * @param record Canonical disk form to unpack.
- * @param value Receives the runtime catalyst relation.
+ * @param value Receives the runtime catalyst
+ * relation.
  * @return True when the disk form has a supported availability value.
  */
 [[nodiscard]] bool decode(const ExoticCatalystRecord& record,

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

@@ -30,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 = 48;
+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. */
@@ -211,12 +211,8 @@ struct ExoticCatalystRecord {
     std::uint16_t itemDefinitionIndex{};
     std::uint16_t completedPlugDefinitionIndex{};
     std::uint16_t effectDefinitionIndex{};
-    std::uint16_t acquisitionDefinitionIndex{};
-    std::uint16_t completionFlagDefinitionIndex{};
-    std::uint16_t completionValueIndex{};
     std::uint8_t socketLane{};
     std::uint8_t availability{};
-    std::int32_t completionValue{};
 };
 
 /** Disk form of one inventory-bucket array-routing descriptor. */
@@ -510,8 +506,7 @@ 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)
-              == 2 * sizeof(std::uint32_t) + 6 * sizeof(std::uint16_t)
-                     + 2 * sizeof(std::uint8_t));
+              == sizeof(std::uint32_t) + 3 * sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t));
 static_assert(sizeof(InventoryBucketRecord)
               == 4 * sizeof(std::uint8_t) + 2 * sizeof(std::uint16_t));
 static_assert(sizeof(SocketEntryListRecord)

+ 8 - 15
Sunrise/src/state/build_data/cache/records/validation.h

@@ -28,7 +28,11 @@ valid_item_detail_links(std::span<const items::details::Definition> details,
 
 /**
  * Checks exact socket rules and members against the complete item and detail domains.
- *
+ * @param rules Exact item and lane rules.
+ * @param pools Deduplicated plug pools.
+ * @param members Flat plug indices owned by the pools.
+ * @param itemDefinitions Complete dense item rows.
+ * @param details Configured item details.
  * @return True when every target lane exists and every plug index names an installed item row.
  */
 [[nodiscard]] bool
@@ -36,25 +40,14 @@ valid_socket_plug_links(std::span<const items::socket_plugs::Rule> rules,
                         std::span<const items::socket_plugs::Pool> pools,
                         std::span<const items::socket_plugs::Member> members,
                         std::span<const items::Definition> itemDefinitions,
-                         std::span<const items::details::Definition> details) noexcept;
+                        std::span<const items::details::Definition> details) noexcept;
 
 /**
  * Checks catalyst item, socket, plug, lifecycle, and pinned release links.
- * @param catalysts Complete candidate catalyst catalog.
- * @param itemDefinitions Complete installed item table.
- * @param details Complete configured item-detail table.
- * @param rules Complete item-and-lane socket rule table.
- * @param pools Complete interned socket plug pools.
- * @param members Complete socket plug pool members.
+ * @param domains Complete cache domains.
  * @return True when a fresh derivation exactly matches the candidate catalog.
  */
-[[nodiscard]] bool valid_exotic_catalyst_links(
-    std::span<const items::catalysts::Definition> catalysts,
-    std::span<const items::Definition> itemDefinitions,
-    std::span<const items::details::Definition> details,
-    std::span<const items::socket_plugs::Rule> rules,
-    std::span<const items::socket_plugs::Pool> pools,
-    std::span<const items::socket_plugs::Member> members) noexcept;
+[[nodiscard]] bool valid_exotic_catalyst_links(Domains domains) noexcept;
 
 /**
  * Checks every collectible item index against the complete dense item table.

+ 9 - 63
Sunrise/src/state/build_data/items/catalysts/definition.h

@@ -8,12 +8,6 @@ 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;
 
 /** Release status from the build-scoped Season 11 availability data. */
 enum class Availability : std::uint8_t {
@@ -22,6 +16,15 @@ enum class Availability : std::uint8_t {
     unsupported = 2,
 };
 
+/**
+ * @param availability Stored release state to check.
+ * @return True when the value is one of the declared states.
+ */
+[[nodiscard]] constexpr bool valid_availability(Availability availability) noexcept {
+    return availability == Availability::released || availability == Availability::placeholder
+           || availability == Availability::unsupported;
+}
+
 /** Safe catalog and resolver outcomes. */
 enum class Error : std::uint8_t {
     none = 0,
@@ -31,11 +34,6 @@ enum class Error : std::uint8_t {
     missingReleased,
     ambiguousLifecycle,
     invalidSocket,
-    invalidPlug,
-    invalidEffect,
-    invalidAcquisition,
-    invalidCompletion,
-    invalidItemState,
 };
 
 /**
@@ -58,16 +56,6 @@ enum class Error : std::uint8_t {
         return "ambiguous_lifecycle";
     case Error::invalidSocket:
         return "invalid_socket";
-    case Error::invalidPlug:
-        return "invalid_plug";
-    case Error::invalidEffect:
-        return "invalid_effect";
-    case Error::invalidAcquisition:
-        return "invalid_acquisition";
-    case Error::invalidCompletion:
-        return "invalid_completion";
-    case Error::invalidItemState:
-        return "invalid_item_state";
     }
     return "unknown";
 }
@@ -80,16 +68,8 @@ struct Definition {
     std::uint16_t completedPlugDefinitionIndex{};
     /** 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 flag slot used by later catalyst completion expressions. */
-    std::uint16_t completionFlagDefinitionIndex{kUnavailableCompletionFlagIndex};
-    /** Family-5 value slot used by legacy catalyst completion expressions. */
-    std::uint16_t completionValueIndex{kUnavailableCompletionValueIndex};
     std::uint8_t socketLane{};
     Availability availability{Availability::unsupported};
-    /** Required value for a legacy completion expression; zero when no value is needed. */
-    std::int32_t completionValue{};
 };
 
 /** The only state a released catalyst exposes to callers. */
@@ -97,40 +77,6 @@ struct CompletedCatalyst {
     std::uint8_t socketLane{};
     std::uint16_t completedPlugDefinitionIndex{};
     std::uint16_t effectDefinitionIndex{};
-    std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
-    std::uint16_t completionFlagDefinitionIndex{kUnavailableCompletionFlagIndex};
-    std::uint16_t completionValueIndex{kUnavailableCompletionValueIndex};
-    std::int32_t completionValue{};
-};
-
-/** 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{};
-    std::uint16_t flagDefinitionIndex{kUnavailableCompletionFlagIndex};
-    std::uint16_t valueIndex{kUnavailableCompletionValueIndex};
-    std::int32_t value{};
-    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. */

+ 52 - 114
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_build_data_runtime.cpp

@@ -1,84 +1,48 @@
-#include "../../runtime.h"
-
 #include <Windows.h>
 
-#include <algorithm>
-#include <array>
-
+#include "../../runtime.h"
 #include "../../runtime/persistence/build_data_persistence.h"
 #include "../../runtime/persistence/publication_transaction.h"
-#include "../details/item_detail_catalog.h"
-#include "../item_catalog.h"
-#include "../socket_plugs/socket_plug_catalog.h"
 #include "exotic_catalyst_builder.h"
 #include "exotic_catalyst_catalog.h"
 
 namespace sunrise::state::build_data {
 namespace {
 
-/** Checks one derived record against all published source domains. */
-[[nodiscard]] bool valid_publication(
-    std::span<const items::catalysts::Definition> definitions) noexcept {
-    if (!items::catalysts::valid(definitions) || !items::catalysts::matches_target_build(
-                                                   runtime::persistence::context().buildIdentity)) {
+/**
+ * Binds caller-owned package domains to the active executable identity.
+ * @param source Caller-owned installed package domains.
+ * @param bound Receives the same domains with the active build identity.
+ * @return True when the build-data runtime has an active identity.
+ */
+[[nodiscard]] bool bind_active_build(const items::catalysts::Source& source,
+                                     items::catalysts::Source& bound) noexcept {
+    runtime::persistence::Context& state = runtime::persistence::context();
+    AcquireSRWLockShared(&state.lock);
+    const BuildIdentity build = state.buildIdentity;
+    const bool enabled = state.enabled;
+    ReleaseSRWLockShared(&state.lock);
+    if (!enabled) {
         return false;
     }
-    const items::catalysts::Facts facts = items::catalysts::generated_facts();
-    std::array<bool, items::catalysts::kDefinitionCapacity> releasedFound{};
-    for (const items::catalysts::Definition& catalyst : definitions) {
-        items::Definition item{};
-        items::Definition completedPlug{};
-        items::Definition effect{};
-        items::Definition acquisition{};
-        items::details::Definition detail{};
-        items::details::Definition effectDetail{};
-        const auto released = std::lower_bound(facts.releasedWeaponHashes.begin(),
-                                               facts.releasedWeaponHashes.end(),
-                                               catalyst.itemDefinitionHash);
-        const bool isReleased = released != facts.releasedWeaponHashes.end()
-                                && *released == catalyst.itemDefinitionHash;
-        if (isReleased) {
-            const std::size_t index = static_cast<std::size_t>(
-                released - facts.releasedWeaponHashes.begin());
-            if (releasedFound[index]
-                || catalyst.availability != items::catalysts::Availability::released) {
-                return false;
-            }
-            releasedFound[index] = true;
-        } else if (catalyst.availability == items::catalysts::Availability::released) {
-            return false;
-        }
-        if (!items::find_index(catalyst.itemDefinitionIndex, item)
-            || item.definitionHash != catalyst.itemDefinitionHash
-            || !items::details::find(catalyst.itemDefinitionIndex, detail)
-            || detail.definitionHash != catalyst.itemDefinitionHash
-            || catalyst.socketLane >= detail.ordinarySocketCount) {
-            return false;
-        }
-        if (catalyst.availability != items::catalysts::Availability::unsupported
-            && !items::socket_plugs::allowed(catalyst.itemDefinitionIndex,
-                                              catalyst.socketLane,
-                                              catalyst.completedPlugDefinitionIndex)) {
-            return false;
-        }
-        if (catalyst.availability != items::catalysts::Availability::unsupported
-            && (!items::find_index(catalyst.completedPlugDefinitionIndex, completedPlug)
-                || !items::find_index(catalyst.effectDefinitionIndex, effect)
-                || !items::find_index(catalyst.acquisitionDefinitionIndex, acquisition)
-                || !items::details::find(catalyst.effectDefinitionIndex, effectDetail)
-                || effectDetail.definitionHash != effect.definitionHash
-                || completedPlug.plugCategoryHash == 0
-                || completedPlug.plugCategoryHash != effect.plugCategoryHash
-                || effect.tier != static_cast<std::uint8_t>(items::Tier::exotic)
-                || effectDetail.instancedDefinitionState
-                       != items::details::InstancedDefinitionState::stackable
-                || (effectDetail.sandboxPerkCount == 0 && effectDetail.statCount == 0))) {
-            return false;
-        }
-    }
-    return std::all_of(releasedFound.begin(),
-                       releasedFound.begin() + facts.releasedWeaponHashes.size(),
-                       [](bool found) { return found; });
+    bound = source;
+    bound.build = build;
+    return true;
+}
+
+/**
+ * Re-derives one candidate from the same package domains before publication.
+ * @param source Caller-owned installed package domains.
+ * @param definitions Candidate catalyst rows.
+ * @return True when a fresh derivation exactly matches the candidate.
+ */
+[[nodiscard]] bool
+valid_publication(const items::catalysts::Source& source,
+                  std::span<const items::catalysts::Definition> definitions) noexcept {
+    items::catalysts::Source bound{};
+    return bind_active_build(source, bound) && items::catalysts::valid(definitions)
+           && items::catalysts::matches_derived(
+               bound, items::catalysts::generated_facts(), definitions);
 }
 
 } // namespace
@@ -87,77 +51,51 @@ bool exotic_catalysts_ready() noexcept {
     return items::catalysts::count() != 0;
 }
 
-bool derive_exotic_catalysts(
-    std::span<const items::Definition> itemDefinitions,
-    std::span<const items::details::Definition> itemDetails,
-    std::span<const items::socket_plugs::Rule> socketPlugRules,
-    std::span<const items::socket_plugs::Pool> socketPlugPools,
-    std::span<const items::socket_plugs::Member> socketPlugMembers,
-    std::span<const items::catalysts::CompletionCondition> completionConditions,
-    std::span<const items::catalysts::AcquisitionGate> acquisitionGates,
-    std::span<items::catalysts::Definition> output,
-    std::size_t& count,
-    items::catalysts::Report& report) noexcept {
-    runtime::persistence::Context& state = runtime::persistence::context();
-    AcquireSRWLockShared(&state.lock);
-    const BuildIdentity build = state.buildIdentity;
-    const bool enabled = state.enabled;
-    ReleaseSRWLockShared(&state.lock);
-    if (!enabled) {
+bool derive_exotic_catalysts(const items::catalysts::Source& source,
+                             std::span<items::catalysts::Definition> output,
+                             std::size_t& count,
+                             items::catalysts::Report& report) noexcept {
+    items::catalysts::Source bound{};
+    if (!bind_active_build(source, bound)) {
         count = 0;
         report = {};
         report.error = items::catalysts::Error::unsupportedBuild;
         report.unsupported = 1;
         return false;
     }
-    const items::catalysts::Source source{build,
-                                          itemDefinitions,
-                                          itemDetails,
-                                          socketPlugRules,
-                                          socketPlugPools,
-                                          socketPlugMembers,
-                                          completionConditions,
-                                          acquisitionGates};
     return items::catalysts::derive(
-        source, items::catalysts::generated_facts(), output, count, report);
+        bound, items::catalysts::generated_facts(), output, count, report);
 }
 
-bool publish_exotic_catalysts(
-    std::span<const items::catalysts::Definition> definitions) noexcept {
+bool publish_exotic_catalysts(const items::catalysts::Source& source,
+                              std::span<const items::catalysts::Definition> definitions) noexcept {
+    if (!valid_publication(source, definitions)) {
+        return false;
+    }
     runtime::persistence::Transaction transaction;
-    if (!transaction.active() || !valid_publication(definitions)) {
+    if (!transaction.active()) {
         return false;
     }
     return transaction.finish(items::catalysts::replace(definitions), items::catalysts::clear);
 }
 
-items::catalysts::ApplyResult complete_exotic_catalyst(
-    std::uint16_t itemDefinitionIndex,
-    std::uint32_t& flags,
-    std::span<std::optional<std::uint16_t>> plugs) noexcept {
+items::catalysts::ApplyResult
+complete_exotic_catalyst(std::uint16_t itemDefinitionIndex,
+                         std::uint32_t& flags,
+                         std::span<std::optional<std::uint16_t>> plugs) noexcept {
     return items::catalysts::apply_completed(itemDefinitionIndex, flags, plugs);
 }
 
-bool complete_exotic_catalyst_investment(Family5State& family) noexcept {
-    return items::catalysts::append_investment_overrides(family);
-}
-
 std::uint16_t resolve_exotic_catalyst_effect(std::uint16_t itemDefinitionIndex,
                                              std::uint8_t socketLane,
                                              std::uint16_t plugDefinitionIndex) noexcept {
-    return items::catalysts::resolve_effect(
-        itemDefinitionIndex, socketLane, plugDefinitionIndex);
+    return items::catalysts::resolve_effect(itemDefinitionIndex, socketLane, plugDefinitionIndex);
 }
 
-bool is_exotic_catalyst_lane(std::uint16_t itemDefinitionIndex,
-                              std::uint8_t socketLane) noexcept {
+bool is_exotic_catalyst_lane(std::uint16_t itemDefinitionIndex, std::uint8_t socketLane) noexcept {
     return items::catalysts::owns_lane(itemDefinitionIndex, socketLane);
 }
 
-items::catalysts::Report exotic_catalyst_report() noexcept {
-    return items::catalysts::report();
-}
-
 void set_exotic_catalyst_completion_enabled(bool enabled) noexcept {
     items::catalysts::set_completion_enabled(enabled);
 }

+ 137 - 305
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp

@@ -17,10 +17,6 @@ struct LaneResult {
     Error error{Error::none};
     std::uint16_t completedPlugDefinitionIndex{details::kUnavailableItemIndex};
     std::uint16_t effectDefinitionIndex{details::kUnavailableItemIndex};
-    std::uint16_t acquisitionDefinitionIndex{kUnavailableAcquisitionIndex};
-    std::uint16_t completionFlagDefinitionIndex{kUnavailableCompletionFlagIndex};
-    std::uint16_t completionValueIndex{kUnavailableCompletionValueIndex};
-    std::int32_t completionValue{};
 };
 
 /**
@@ -28,84 +24,68 @@ struct LaneResult {
  * @return True when the table is strict and contains no zero hash.
  */
 [[nodiscard]] bool valid_hashes(std::span<const std::uint32_t> hashes) noexcept {
-    return std::none_of(
-               hashes.begin(), hashes.end(), [](std::uint32_t hash) { return hash == 0; })
+    return std::none_of(hashes.begin(), hashes.end(), [](std::uint32_t hash) { return hash == 0; })
            && std::adjacent_find(hashes.begin(), hashes.end(), std::greater_equal{})
                   == hashes.end();
 }
 
 /**
- * Finds a source item without requiring a dense test fixture.
- * @param definitions Source item definitions in native index order.
- * @param index Native item index to find.
- * @return The matching item, or null when no item has the index.
+ * Finds one row in a dense or sorted native-index table.
+ * @tparam Value Row type.
+ * @tparam Index Returns the row's native index.
+ * @param definitions Source rows in native index order.
+ * @param index Native index to find.
+ * @param indexOf Row-index reader.
+ * @return The matching row, or null when no row has the index.
  */
-[[nodiscard]] const items::Definition*
-find_item(std::span<const items::Definition> definitions, std::uint16_t index) noexcept {
+template <typename Value, typename Index>
+[[nodiscard]] const Value*
+find_indexed(std::span<const Value> definitions, std::uint16_t index, Index indexOf) noexcept {
     if (static_cast<std::size_t>(index) < definitions.size()
-        && definitions[index].definitionIndex == index) {
+        && indexOf(definitions[index]) == index) {
         return &definitions[index];
     }
     const auto found = std::lower_bound(
-        definitions.begin(),
-        definitions.end(),
-        index,
-        [](const items::Definition& value, auto key) { return value.definitionIndex < key; });
-    return found != definitions.end() && found->definitionIndex == index ? &*found : nullptr;
+        definitions.begin(), definitions.end(), index, [&indexOf](const Value& value, auto key) {
+            return indexOf(value) < key;
+        });
+    return found != definitions.end() && indexOf(*found) == index ? &*found : nullptr;
 }
 
 /**
- * Finds a source detail without requiring a dense test fixture.
- * @param definitions Source item details in native index order.
- * @param index Native item index to find.
- * @return The matching detail, or null when no detail has the index.
+ * @param value Installed item row.
+ * @return The row's native item index.
  */
-[[nodiscard]] const details::Definition*
-find_detail(std::span<const details::Definition> definitions, std::uint16_t index) noexcept {
-    if (static_cast<std::size_t>(index) < definitions.size()
-        && definitions[index].definitionIndex == index) {
-        return &definitions[index];
-    }
-    const auto found = std::lower_bound(
-        definitions.begin(),
-        definitions.end(),
-        index,
-        [](const details::Definition& value, auto key) { return value.definitionIndex < key; });
-    return found != definitions.end() && found->definitionIndex == index ? &*found : nullptr;
+[[nodiscard]] constexpr std::uint16_t item_index(const items::Definition& value) noexcept {
+    return value.definitionIndex;
 }
 
-/** Finds one dense or sorted completion-condition row. */
-[[nodiscard]] const CompletionCondition*
-find_completion_condition(std::span<const CompletionCondition> definitions,
-                          std::uint16_t index) noexcept {
-    if (static_cast<std::size_t>(index) < definitions.size()
-        && definitions[index].itemDefinitionIndex == index) {
-        return &definitions[index];
-    }
-    const auto found = std::lower_bound(
-        definitions.begin(),
-        definitions.end(),
-        index,
-        [](const CompletionCondition& value, auto key) {
-            return value.itemDefinitionIndex < key;
-        });
-    return found != definitions.end() && found->itemDefinitionIndex == index ? &*found : nullptr;
+/**
+ * @param value Installed item detail row.
+ * @return The detail row's native item index.
+ */
+[[nodiscard]] constexpr std::uint16_t detail_index(const details::Definition& value) noexcept {
+    return value.definitionIndex;
 }
 
-/** Finds one dense or sorted socket-type acquisition row. */
-[[nodiscard]] const AcquisitionGate*
-find_acquisition_gate(std::span<const AcquisitionGate> definitions,
-                      std::uint16_t socketType) noexcept {
-    if (static_cast<std::size_t>(socketType) < definitions.size()
-        && definitions[socketType].socketType == socketType) {
-        return &definitions[socketType];
-    }
-    const auto found = std::lower_bound(
-        definitions.begin(),
-        definitions.end(),
-        socketType,
-        [](const AcquisitionGate& value, auto key) { return value.socketType < key; });
-    return found != definitions.end() && found->socketType == socketType ? &*found : nullptr;
+/**
+ * @param definitions Installed item rows in native index order.
+ * @param index Native item index to find.
+ * @return The matching source item, or null.
+ */
+[[nodiscard]] const items::Definition* find_item(std::span<const items::Definition> definitions,
+                                                 std::uint16_t index) noexcept {
+    return find_indexed(definitions, index, item_index);
+}
+
+/**
+ * @param definitions Installed item details in native index order.
+ * @param index Native item index to find.
+ * @return The matching source detail, or null.
+ */
+[[nodiscard]] const details::Definition*
+find_detail(std::span<const details::Definition> definitions, std::uint16_t index) noexcept {
+    return find_indexed(definitions, index, detail_index);
 }
 
 /**
@@ -114,26 +94,13 @@ find_acquisition_gate(std::span<const AcquisitionGate> definitions,
  * 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,
-                                       std::uint16_t socketType) noexcept {
+                                       std::uint16_t completedPlugDefinitionIndex) noexcept {
     const items::Definition* completed = find_item(source.items, completedPlugDefinitionIndex);
     if (completed == nullptr || completed->plugCategoryHash == 0) {
-        return {.catalyst = true,
-                .error = Error::invalidEffect,
-                .completedPlugDefinitionIndex = completedPlugDefinitionIndex};
-    }
-
-    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};
+        return {};
     }
 
     std::optional<std::uint16_t> effect;
@@ -144,49 +111,23 @@ find_acquisition_gate(std::span<const AcquisitionGate> definitions,
         }
         const details::Definition* detail = find_detail(source.details, candidate.definitionIndex);
         if (detail == nullptr || detail->definitionHash != candidate.definitionHash
-            || detail->instancedDefinitionState
-                   != details::InstancedDefinitionState::stackable
+            || detail->instancedDefinitionState != details::InstancedDefinitionState::stackable
             || (detail->sandboxPerkCount == 0 && detail->statCount == 0)) {
             continue;
         }
         if (effect.has_value()) {
-            return {.catalyst = true,
-                    .error = Error::invalidEffect,
-                    .completedPlugDefinitionIndex = completedPlugDefinitionIndex};
+            return {};
         }
         effect = candidate.definitionIndex;
     }
     if (!effect.has_value()) {
-        return {.catalyst = true,
-                .error = Error::invalidEffect,
-                .completedPlugDefinitionIndex = completedPlugDefinitionIndex};
+        return {};
     }
 
-    const CompletionCondition* condition =
-        find_completion_condition(source.completionConditions, *effect);
-    const bool hasFlag = condition != nullptr
-                         && condition->flagDefinitionIndex
-                                != kUnavailableCompletionFlagIndex;
-    const bool hasValue = condition != nullptr
-                          && condition->valueIndex != kUnavailableCompletionValueIndex
-                          && condition->value > 0;
-    const bool directEffect = *effect == completedPlugDefinitionIndex;
-    if (condition == nullptr || condition->state != CompletionConditionState::present
-        || (directEffect && !hasFlag) || (!directEffect && !hasValue)) {
-        return {.catalyst = true,
-                .error = Error::invalidCompletion,
-                .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
-                .effectDefinitionIndex = *effect,
-                .acquisitionDefinitionIndex = acquisition->definitionIndex};
-    }
     return {.catalyst = true,
             .error = Error::none,
             .completedPlugDefinitionIndex = completedPlugDefinitionIndex,
-            .effectDefinitionIndex = *effect,
-            .acquisitionDefinitionIndex = acquisition->definitionIndex,
-            .completionFlagDefinitionIndex = condition->flagDefinitionIndex,
-            .completionValueIndex = condition->valueIndex,
-            .completionValue = condition->value};
+            .effectDefinitionIndex = *effect};
 }
 
 /**
@@ -196,18 +137,17 @@ find_acquisition_gate(std::span<const AcquisitionGate> definitions,
  * @param lane Native socket lane to find.
  * @return The matching rule, or null when no rule matches.
  */
-[[nodiscard]] const socket_plugs::Rule*
-find_rule(std::span<const socket_plugs::Rule> rules,
-          std::uint16_t itemDefinitionIndex,
-          std::uint8_t lane) noexcept {
-    const auto found = std::lower_bound(
-        rules.begin(),
-        rules.end(),
-        std::pair{itemDefinitionIndex, lane},
-        [](const socket_plugs::Rule& value, const auto& key) {
-            return value.itemDefinitionIndex < key.first
-                   || (value.itemDefinitionIndex == key.first && value.lane < key.second);
-        });
+[[nodiscard]] const socket_plugs::Rule* find_rule(std::span<const socket_plugs::Rule> rules,
+                                                  std::uint16_t itemDefinitionIndex,
+                                                  std::uint8_t lane) noexcept {
+    const auto found = std::lower_bound(rules.begin(),
+                                        rules.end(),
+                                        std::pair{itemDefinitionIndex, lane},
+                                        [](const socket_plugs::Rule& value, const auto& key) {
+                                            return value.itemDefinitionIndex < key.first
+                                                   || (value.itemDefinitionIndex == key.first
+                                                       && value.lane < key.second);
+                                        });
     return found != rules.end() && found->itemDefinitionIndex == itemDefinitionIndex
                    && found->lane == lane
                ? &*found
@@ -219,8 +159,8 @@ find_rule(std::span<const socket_plugs::Rule> rules,
  * @param hash Weapon hash to find.
  * @return Index of a released hash, or no value for a placeholder.
  */
-[[nodiscard]] std::optional<std::size_t>
-released_index(std::span<const std::uint32_t> released, std::uint32_t hash) noexcept {
+[[nodiscard]] std::optional<std::size_t> released_index(std::span<const std::uint32_t> released,
+                                                        std::uint32_t hash) noexcept {
     const auto found = std::lower_bound(released.begin(), released.end(), hash);
     if (found == released.end() || *found != hash) {
         return std::nullopt;
@@ -229,17 +169,14 @@ released_index(std::span<const std::uint32_t> released, std::uint32_t hash) noex
 }
 
 /**
- * Classifies one lane when its pool has a target-build catalyst marker.
+ * Classifies one lane from its installed two-state or legacy three-state lifecycle.
  * @param source Parsed target-build tables.
- * @param facts Pinned facts for the target build.
  * @param detail Item detail row that owns the lane.
  * @param lane Native socket lane to classify.
  * @return The catalyst state and any safe-failure reason.
  */
-[[nodiscard]] LaneResult classify_lane(const Source& source,
-                                       const Facts& facts,
-                                       const details::Definition& detail,
-                                       std::uint8_t lane) noexcept {
+[[nodiscard]] LaneResult
+classify_lane(const Source& source, const details::Definition& detail, std::uint8_t lane) noexcept {
     const socket_plugs::Rule* rule =
         find_rule(source.socketPlugRules, detail.definitionIndex, lane);
     if (rule == nullptr) {
@@ -255,55 +192,59 @@ released_index(std::span<const std::uint32_t> released, std::uint32_t hash) noex
     }
 
     const auto members = source.socketPlugMembers.subspan(pool.memberOffset, pool.memberCount);
-    bool hasEmpty = false;
-    std::size_t legacyCount = 0;
-    std::uint16_t legacyCompletionIndex = 0;
-    bool invalidMember = false;
-    for (const std::uint16_t member : members) {
-        const items::Definition* plug = find_item(source.items, member);
-        if (plug == nullptr) {
-            invalidMember = true;
-            continue;
-        }
-        if (plug->definitionHash == facts.emptyCatalystPlugHash) {
-            hasEmpty = true;
-        }
-        if (std::binary_search(facts.legacyCompletionPlugHashes.begin(),
-                               facts.legacyCompletionPlugHashes.end(),
-                               plug->definitionHash)) {
-            ++legacyCount;
-            legacyCompletionIndex = member;
-        }
-    }
-    if (!hasEmpty && legacyCount == 0) {
+    if (members.size() != 2 && members.size() != 3) {
         return {};
     }
-    if (invalidMember) {
-        return {.catalyst = true, .error = Error::invalidPlug};
-    }
 
     const std::uint16_t defaultIndex = detail.initialPlugIndices[lane];
     const items::Definition* defaultPlug = find_item(source.items, defaultIndex);
     if (defaultIndex == details::kUnavailableItemIndex || defaultPlug == nullptr
         || std::find(members.begin(), members.end(), defaultIndex) == members.end()) {
-        return {.catalyst = true, .error = Error::invalidPlug};
+        return {};
     }
 
-    if (members.size() == 2 && hasEmpty && legacyCount == 0
-        && defaultPlug->definitionHash == facts.emptyCatalystPlugHash) {
+    if (members.size() == 2) {
         const auto active =
             std::find_if(members.begin(), members.end(), [defaultIndex](auto member) {
                 return member != defaultIndex;
             });
-        return active != members.end()
-                   ? complete_lane(source, *active, detail.socketTypes[lane])
-                   : LaneResult{.catalyst = true, .error = Error::ambiguousLifecycle};
+        if (active == members.end()) {
+            return {};
+        }
+        const LaneResult completed = complete_lane(source, *active);
+        return completed.catalyst && completed.effectDefinitionIndex == *active ? completed
+                                                                                : LaneResult{};
+    }
+
+    std::size_t progressCount = 0;
+    std::size_t completedCount = 0;
+    std::uint16_t completedIndex = details::kUnavailableItemIndex;
+    for (const std::uint16_t member : members) {
+        if (member == defaultIndex) {
+            continue;
+        }
+        const items::Definition* plug = find_item(source.items, member);
+        const details::Definition* plugDetail = find_detail(source.details, member);
+        if (plug == nullptr || plugDetail == nullptr
+            || plugDetail->definitionHash != plug->definitionHash
+            || plugDetail->instancedDefinitionState
+                   != details::InstancedDefinitionState::stackable) {
+            return {};
+        }
+        if (plugDetail->maxStackSize == 1) {
+            ++progressCount;
+        } else if (plugDetail->maxStackSize > 1) {
+            ++completedCount;
+            completedIndex = member;
+        }
     }
-    if (members.size() == 3 && !hasEmpty && legacyCount == 1
-        && legacyCompletionIndex != defaultIndex) {
-        return complete_lane(source, legacyCompletionIndex, detail.socketTypes[lane]);
+    if (progressCount == 1 && completedCount == 1) {
+        const LaneResult completed = complete_lane(source, completedIndex);
+        return completed.catalyst && completed.effectDefinitionIndex != completedIndex
+                   ? completed
+                   : LaneResult{};
     }
-    return {.catalyst = true, .error = Error::ambiguousLifecycle};
+    return {};
 }
 
 /**
@@ -337,7 +278,7 @@ released_index(std::span<const std::uint32_t> released, std::uint32_t hash) noex
  * @return True for an exotic in one of the three weapon equipment slots.
  */
 [[nodiscard]] bool exotic_weapon(const items::Definition& item,
-                                  const details::Definition& detail) noexcept {
+                                 const details::Definition& detail) noexcept {
     return item.tier == static_cast<std::uint8_t>(items::Tier::exotic)
            && detail.instancedDefinitionState == details::InstancedDefinitionState::instanced
            && detail.equipmentSlot.has_value() && *detail.equipmentSlot >= kFirstWeaponSlot
@@ -356,10 +297,8 @@ bool derive(const Source& source,
     count = 0;
     report = {};
     std::fill(output.begin(), output.end(), Definition{});
-    if (facts.imageTimestamp == 0 || facts.imageSize == 0 || facts.emptyCatalystPlugHash == 0
-        || facts.legacyCompletionPlugHashes.size() > kDefinitionCapacity
+    if (facts.imageTimestamp == 0 || facts.imageSize == 0
         || facts.releasedWeaponHashes.size() > kDefinitionCapacity
-        || !valid_hashes(facts.legacyCompletionPlugHashes)
         || !valid_hashes(facts.releasedWeaponHashes)) {
         return fail(output, count, report, Error::unsupportedBuild);
     }
@@ -383,7 +322,7 @@ bool derive(const Source& source,
         std::uint8_t detectedLane = 0;
         for (std::size_t laneIndex = 0; laneIndex < detail.ordinarySocketCount; ++laneIndex) {
             const auto lane = static_cast<std::uint8_t>(laneIndex);
-            const LaneResult result = classify_lane(source, facts, detail, lane);
+            const LaneResult result = classify_lane(source, detail, lane);
             if (!result.catalyst) {
                 continue;
             }
@@ -396,25 +335,15 @@ bool derive(const Source& source,
                 unclear = Error::ambiguousLifecycle;
                 continue;
             }
-            completed = CompletedCatalyst{lane,
-                                          result.completedPlugDefinitionIndex,
-                                          result.effectDefinitionIndex,
-                                          result.acquisitionDefinitionIndex,
-                                          result.completionFlagDefinitionIndex,
-                                          result.completionValueIndex,
-                                          result.completionValue};
+            completed = CompletedCatalyst{
+                lane, result.completedPlugDefinitionIndex, result.effectDefinitionIndex};
         }
         if (!completed.has_value() && !unclear.has_value()) {
             continue;
         }
         if (unclear.has_value()) {
             if (release.has_value()) {
-                return fail(output,
-                            count,
-                            report,
-                            *unclear,
-                            item->definitionHash,
-                            detectedLane);
+                return fail(output, count, report, *unclear, item->definitionHash, detectedLane);
             }
             if (count >= output.size() || count >= kDefinitionCapacity) {
                 return fail(output,
@@ -428,12 +357,8 @@ bool derive(const Source& source,
                                          item->definitionIndex,
                                          details::kUnavailableItemIndex,
                                          details::kUnavailableItemIndex,
-                                         kUnavailableAcquisitionIndex,
-                                         kUnavailableCompletionFlagIndex,
-                                         kUnavailableCompletionValueIndex,
                                          detectedLane,
-                                         Availability::unsupported,
-                                         0};
+                                         Availability::unsupported};
             ++report.unsupported;
             continue;
         }
@@ -459,32 +384,26 @@ bool derive(const Source& source,
         } else {
             ++report.placeholder;
         }
-        output[count++] = Definition{item->definitionHash,
-                                     item->definitionIndex,
-                                     completed->completedPlugDefinitionIndex,
-                                     completed->effectDefinitionIndex,
-                                     completed->acquisitionDefinitionIndex,
-                                     completed->completionFlagDefinitionIndex,
-                                     completed->completionValueIndex,
-                                     completed->socketLane,
-                                     release.has_value() ? Availability::released
-                                                         : Availability::placeholder,
-                                     completed->completionValue};
+        output[count++] =
+            Definition{item->definitionHash,
+                       item->definitionIndex,
+                       completed->completedPlugDefinitionIndex,
+                       completed->effectDefinitionIndex,
+                       completed->socketLane,
+                       release.has_value() ? Availability::released : Availability::placeholder};
     }
 
     for (std::size_t index = 0; index < facts.releasedWeaponHashes.size(); ++index) {
         if (!releasedFound[index]) {
-            return fail(output,
-                        count,
-                        report,
-                        Error::missingReleased,
-                        facts.releasedWeaponHashes[index]);
+            return fail(
+                output, count, report, Error::missingReleased, facts.releasedWeaponHashes[index]);
         }
     }
-    std::sort(output.begin(), output.begin() + count, [](const Definition& left,
-                                                         const Definition& right) {
-        return left.itemDefinitionIndex < right.itemDefinitionIndex;
-    });
+    std::sort(output.begin(),
+              output.begin() + count,
+              [](const Definition& left, const Definition& right) {
+                  return left.itemDefinitionIndex < right.itemDefinitionIndex;
+              });
     return true;
 }
 
@@ -498,105 +417,18 @@ bool matches_derived(const Source& source,
         || expectedCount != definitions.size()) {
         return false;
     }
-    return std::equal(
-        expected.begin(),
-        expected.begin() + expectedCount,
-        definitions.begin(),
-        [](const Definition& left, const Definition& right) {
-            return left.itemDefinitionHash == right.itemDefinitionHash
-                   && left.itemDefinitionIndex == right.itemDefinitionIndex
-                   && left.completedPlugDefinitionIndex == right.completedPlugDefinitionIndex
-                   && left.effectDefinitionIndex == right.effectDefinitionIndex
-                   && left.acquisitionDefinitionIndex == right.acquisitionDefinitionIndex
-                   && left.completionFlagDefinitionIndex
-                          == right.completionFlagDefinitionIndex
-                   && left.completionValueIndex == right.completionValueIndex
-                   && left.socketLane == right.socketLane
-                   && left.availability == right.availability
-                   && left.completionValue == right.completionValue;
-        });
-}
-
-bool matches_cached(const Source& source,
-                    const Facts& facts,
-                    std::span<const Definition> definitions) noexcept {
-    std::array<CompletionCondition, kDefinitionCapacity> completionConditions{};
-    std::array<AcquisitionGate, kDefinitionCapacity> acquisitionGates{};
-    std::size_t completionCount = 0;
-    std::size_t acquisitionCount = 0;
-    for (const Definition& definition : definitions) {
-        if (definition.availability == Availability::unsupported) {
-            continue;
-        }
-        const details::Definition* detail = find_detail(source.details,
-                                                        definition.itemDefinitionIndex);
-        if (detail == nullptr || definition.socketLane >= detail->ordinarySocketCount
-            || find_item(source.items, definition.acquisitionDefinitionIndex) == nullptr
-            || (definition.completionFlagDefinitionIndex
-                        != kUnavailableCompletionFlagIndex
-                && find_item(source.items, definition.completionFlagDefinitionIndex)
-                       == nullptr)) {
-            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};
-        }
-
-        if (definition.completionFlagDefinitionIndex == kUnavailableCompletionFlagIndex
-            && definition.completionValueIndex == kUnavailableCompletionValueIndex) {
-            continue;
-        }
-        const auto priorCondition = std::find_if(
-            completionConditions.begin(),
-            completionConditions.begin() + completionCount,
-            [&definition](const CompletionCondition& condition) {
-                return condition.itemDefinitionIndex == definition.effectDefinitionIndex;
-            });
-        if (priorCondition != completionConditions.begin() + completionCount) {
-            if (priorCondition->flagDefinitionIndex
-                    != definition.completionFlagDefinitionIndex
-                || priorCondition->valueIndex != definition.completionValueIndex
-                || priorCondition->value != definition.completionValue) {
-                return false;
-            }
-        } else if (completionCount >= completionConditions.size()) {
-            return false;
-        } else {
-            completionConditions[completionCount++] = {
-                definition.effectDefinitionIndex,
-                definition.completionFlagDefinitionIndex,
-                definition.completionValueIndex,
-                definition.completionValue,
-                CompletionConditionState::present};
-        }
-    }
-    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);
-    return matches_derived(rebuilt, facts, definitions);
+    return std::equal(expected.begin(),
+                      expected.begin() + expectedCount,
+                      definitions.begin(),
+                      [](const Definition& left, const Definition& right) {
+                          return left.itemDefinitionHash == right.itemDefinitionHash
+                                 && left.itemDefinitionIndex == right.itemDefinitionIndex
+                                 && left.completedPlugDefinitionIndex
+                                        == right.completedPlugDefinitionIndex
+                                 && left.effectDefinitionIndex == right.effectDefinitionIndex
+                                 && left.socketLane == right.socketLane
+                                 && left.availability == right.availability;
+                      });
 }
 
 } // namespace sunrise::state::build_data::items::catalysts

+ 1 - 24
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.h

@@ -16,8 +16,7 @@ namespace sunrise::state::build_data::items::catalysts {
 struct Facts {
     std::uint32_t imageTimestamp{};
     std::uint32_t imageSize{};
-    std::uint32_t emptyCatalystPlugHash{};
-    std::span<const std::uint32_t> legacyCompletionPlugHashes;
+    /** Historical release state is not present in the installed Season 11 item relations. */
     std::span<const std::uint32_t> releasedWeaponHashes;
 };
 
@@ -29,21 +28,11 @@ 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;
 };
 
 /** @return The generated facts pinned to Destiny 2 build 86657.20.08.23. */
 [[nodiscard]] Facts generated_facts() noexcept;
 
-/**
- * @param build Executable identity to compare with the generated content build.
- * @return True when the executable identity matches the generated content build.
- */
-[[nodiscard]] bool matches_target_build(const BuildIdentity& build) noexcept;
-
 /**
  * Derives all released and placeholder catalyst records without display text.
  * On failure, count is zero and no partial record is visible.
@@ -71,16 +60,4 @@ 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 row must still name an installed item. 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

+ 15 - 165
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp

@@ -4,7 +4,6 @@
 #include <atomic>
 
 #include "../../../account/inventory/item_state.h"
-#include "../../../unlocks/definition.h"
 #include "../../table.h"
 #include "../details/definition.h"
 
@@ -13,7 +12,6 @@ namespace {
 
 Lock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
-Report g_report;
 std::atomic<bool> g_completionEnabled{true};
 
 /**
@@ -31,8 +29,8 @@ std::atomic<bool> g_completionEnabled{true};
  * @param itemDefinitionIndex Native item index to find.
  * @return The matching definition, or null when no definition matches.
  */
-[[nodiscard]] const Definition*
-find(std::span<const Definition> definitions, std::uint16_t itemDefinitionIndex) noexcept {
+[[nodiscard]] const Definition* find(std::span<const Definition> definitions,
+                                     std::uint16_t itemDefinitionIndex) noexcept {
     const auto found = std::lower_bound(
         definitions.begin(),
         definitions.end(),
@@ -43,75 +41,11 @@ find(std::span<const Definition> definitions, std::uint16_t itemDefinitionIndex)
                : 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;
-}
-
 } // namespace
 
 void clear() noexcept {
     const Lock::Exclusive guard(g_lock);
     g_definitions.clear();
-    g_report = {};
     g_completionEnabled.store(true, std::memory_order_release);
 }
 
@@ -131,30 +65,14 @@ bool valid(std::span<const Definition> definitions) noexcept {
         const Definition& definition = definitions[index];
         const bool hasCompletedPlug =
             definition.completedPlugDefinitionIndex != details::kUnavailableItemIndex;
-        const bool hasEffect =
-            definition.effectDefinitionIndex != details::kUnavailableItemIndex;
-        const bool hasAcquisition =
-            definition.acquisitionDefinitionIndex != kUnavailableAcquisitionIndex;
-        const bool hasCompletionFlag = definition.completionFlagDefinitionIndex
-                                       != kUnavailableCompletionFlagIndex;
-        const bool hasCompletionValue =
-            definition.completionValueIndex != kUnavailableCompletionValueIndex;
-        const bool directEffect = definition.completedPlugDefinitionIndex
-                                  == definition.effectDefinitionIndex;
+        const bool hasEffect = definition.effectDefinitionIndex != details::kUnavailableItemIndex;
         if (definition.itemDefinitionHash == 0
             || definition.socketLane >= details::kInitialPlugCapacity
-            || (definition.availability != Availability::released
-                && definition.availability != Availability::placeholder
-                && definition.availability != Availability::unsupported)
+            || !valid_availability(definition.availability)
             || (definition.availability == Availability::unsupported
-                && (hasCompletedPlug || hasEffect || hasAcquisition || hasCompletionFlag
-                    || hasCompletionValue || definition.completionValue != 0))
-            || (definition.availability != Availability::unsupported
-                && (!hasCompletedPlug || !hasEffect || !hasAcquisition))
-            || (hasCompletionValue != (definition.completionValue > 0))
+                && (hasCompletedPlug || hasEffect))
             || (definition.availability != Availability::unsupported
-                && ((directEffect && !hasCompletionFlag)
-                    || (!directEffect && !hasCompletionValue)))
+                && (!hasCompletedPlug || !hasEffect))
             || (index != 0 && !less(definitions[index - 1], definition))) {
             return false;
         }
@@ -166,22 +84,8 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    Report nextReport{};
-    for (const Definition& definition : definitions) {
-        if (definition.availability == Availability::released) {
-            ++nextReport.released;
-        } else if (definition.availability == Availability::placeholder) {
-            ++nextReport.placeholder;
-        } else {
-            ++nextReport.unsupported;
-        }
-    }
     const Lock::Exclusive guard(g_lock);
-    if (!g_definitions.replace(definitions)) {
-        return false;
-    }
-    g_report = nextReport;
-    return true;
+    return g_definitions.replace(definitions);
 }
 
 Result resolve(std::uint16_t itemDefinitionIndex) noexcept {
@@ -200,11 +104,7 @@ Result resolve(std::uint16_t itemDefinitionIndex) noexcept {
             Availability::released,
             {definition->socketLane,
              definition->completedPlugDefinitionIndex,
-             definition->effectDefinitionIndex,
-             definition->acquisitionDefinitionIndex,
-             definition->completionFlagDefinitionIndex,
-             definition->completionValueIndex,
-             definition->completionValue}};
+             definition->effectDefinitionIndex}};
 }
 
 std::uint16_t resolve_effect(std::uint16_t itemDefinitionIndex,
@@ -214,8 +114,7 @@ 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->effectDefinitionIndex != plugDefinitionIndex)
+        || definition->completedPlugDefinitionIndex != plugDefinitionIndex
         || definition->effectDefinitionIndex == details::kUnavailableItemIndex) {
         return plugDefinitionIndex;
     }
@@ -238,68 +137,24 @@ ApplyResult apply_completed(std::uint16_t itemDefinitionIndex,
     if (result.error == Error::noCatalyst || result.availability != Availability::released) {
         return ApplyResult::unchanged;
     }
-    if (result.error != Error::none
-        || !account::inventory::valid_item_state(flags)
+    if (result.error != Error::none || !account::inventory::valid_item_state(flags)
         || result.completed.socketLane >= plugs.size()
-        || result.completed.effectDefinitionIndex == details::kUnavailableItemIndex) {
+        || result.completed.completedPlugDefinitionIndex == details::kUnavailableItemIndex) {
         return ApplyResult::failed;
     }
 
     const std::uint32_t completedFlags = flags | account::inventory::kMasterworkItemFlag;
-    const std::optional<std::uint16_t> completedPlug = result.completed.effectDefinitionIndex;
+    const std::optional<std::uint16_t> completedPlug =
+        result.completed.completedPlugDefinitionIndex;
     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;
-        }
-
-        if (definition.completionFlagDefinitionIndex != kUnavailableCompletionFlagIndex
-            && !upsert_flag(candidate, definition.completionFlagDefinitionIndex)) {
-            return false;
-        }
-
-        if (definition.completionValueIndex == kUnavailableCompletionValueIndex) {
-            continue;
-        }
-        if (!upsert_completion_value(candidate,
-                                     definition.completionValueIndex,
-                                     definition.completionValue)) {
-            return false;
-        }
-    }
-    family = candidate;
-    return true;
-}
-
-bool snapshot(std::span<Definition> output,
-              std::size_t& outputCount,
-              Report& outputReport) noexcept {
+bool snapshot(std::span<Definition> output, std::size_t& outputCount) noexcept {
     outputCount = 0;
-    outputReport = {};
     const Lock::Shared guard(g_lock);
-    if (!g_definitions.snapshot(output, outputCount)) {
-        return false;
-    }
-    outputReport = g_report;
-    return true;
+    return g_definitions.snapshot(output, outputCount);
 }
 
 std::size_t count() noexcept {
@@ -307,9 +162,4 @@ std::size_t count() noexcept {
     return g_definitions.count();
 }
 
-Report report() noexcept {
-    const Lock::Shared guard(g_lock);
-    return g_report;
-}
-
 } // namespace sunrise::state::build_data::items::catalysts

+ 4 - 20
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h

@@ -5,7 +5,6 @@
 #include <optional>
 #include <span>
 
-#include "../../../investment/investment.h"
 #include "definition.h"
 
 namespace sunrise::state::build_data::items::catalysts {
@@ -65,34 +64,19 @@ void set_completion_enabled(bool enabled) noexcept;
  * @param plugs Candidate ordinary socket plugs.
  * @return Completed, unchanged, or failed without a partial change.
  */
-[[nodiscard]] ApplyResult
-apply_completed(std::uint16_t itemDefinitionIndex,
-                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;
+[[nodiscard]] ApplyResult apply_completed(std::uint16_t itemDefinitionIndex,
+                                          std::uint32_t& flags,
+                                          std::span<std::optional<std::uint16_t>> plugs) noexcept;
 
 /**
  * Copies the complete catalog under its shared lock.
  * @param output Caller-owned fixed catalog storage.
  * @param count Receives the number of copied rows.
- * @param report Receives the published catalog counts.
  * @return False only when output cannot hold the complete catalog.
  */
-[[nodiscard]] bool
-snapshot(std::span<Definition> output, std::size_t& count, Report& report) noexcept;
+[[nodiscard]] bool snapshot(std::span<Definition> output, std::size_t& count) noexcept;
 
 /** @return Published catalyst record count. */
 [[nodiscard]] std::size_t count() noexcept;
 
-/** @return Counts for the current complete catalog. */
-[[nodiscard]] Report report() noexcept;
-
 } // namespace sunrise::state::build_data::items::catalysts

+ 15 - 34
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_generated.cpp

@@ -1,51 +1,32 @@
-#include "exotic_catalyst_builder.h"
-
 #include <array>
 
+#include "exotic_catalyst_builder.h"
+
 namespace sunrise::state::build_data::items::catalysts {
 namespace {
 
 /** PE identity of the supported 86657.20.08.23 client. */
 constexpr std::uint32_t kImageTimestamp = 0x5F43138BU;
 constexpr std::uint32_t kImageSize = 0x08A5EA00U;
-/** Later catalysts use this build's shared empty socket plug. */
-constexpr std::uint32_t kEmptyCatalystPlugHash = 0x5957A904U;
-
-/** Legacy completed-plug facts generated from the pinned target manifest. */
-constexpr std::array<std::uint32_t, 30> kLegacyCompletionPlugHashes{
-    0x151E1554U, 0x151E1555U, 0x174B3FEBU, 0x206EDFE0U, 0x206EDFE1U,
-    0x288A777DU, 0x2FB02CF0U, 0x32F442E6U, 0x4FE33F81U, 0x6096F61AU,
-    0x6096F61BU, 0x61935831U, 0x641204BFU, 0x69A468F9U, 0x70B8A117U,
-    0x7D463B2FU, 0x7FB372AAU, 0x8808888CU, 0x8808888DU, 0x8F90F557U,
-    0x9C8C0A61U, 0xA651C900U, 0xAA5EEFD0U, 0xAA5EEFD1U, 0xC9C0E4C0U,
-    0xE2CB93CBU, 0xE3700214U, 0xE681F877U, 0xFC5C45A8U, 0xFC5C45A9U,
-};
-
-/** Released Season 11 catalyst facts, in definition-hash order. */
+/**
+ * Released Season 11 weapon hashes, in definition-hash order.
+ * The installed build stages later catalyst sockets with the same item, socket, and visibility
+ * fields as released catalysts. Runtime extraction can recover every relation except release date.
+ */
 constexpr std::array<std::uint32_t, 45> kReleasedWeaponHashes{
-    0x012248BAU, 0x14B465B2U, 0x17D8FEABU, 0x2E43BDEEU, 0x3092080DU,
-    0x4F5CCF1DU, 0x50384F32U, 0x50384F33U, 0x5141601FU, 0x59EFED62U,
-    0x5BDBCC56U, 0x634C6957U, 0x6F22FCECU, 0x70BEF156U, 0x83A19696U,
-    0x8843C72AU, 0x8C8180D6U, 0x8CD074B1U, 0x93E680C3U, 0xA7DBFF3AU,
-    0xAA45882AU, 0xAD4746D4U, 0xAD4746D5U, 0xB824C63DU, 0xBB46CCD2U,
-    0xBB46CCD3U, 0xBF704917U, 0xCB6F6266U, 0xCB7B5EDFU, 0xCCE7D927U,
-    0xD15517D4U, 0xD38BCABAU, 0xD38BCABBU, 0xD5704484U, 0xD5704485U,
-    0xD84E04AAU, 0xD84E04ABU, 0xE0794C51U, 0xE5296126U, 0xE86A25CFU,
-    0xEF7D3366U, 0xF0923C79U, 0xF5DE4480U, 0xF9C0B6B0U, 0xFDA23E68U,
+    0x012248BAU, 0x14B465B2U, 0x17D8FEABU, 0x2E43BDEEU, 0x3092080DU, 0x4F5CCF1DU, 0x50384F32U,
+    0x50384F33U, 0x5141601FU, 0x59EFED62U, 0x5BDBCC56U, 0x634C6957U, 0x6F22FCECU, 0x70BEF156U,
+    0x83A19696U, 0x8843C72AU, 0x8C8180D6U, 0x8CD074B1U, 0x93E680C3U, 0xA7DBFF3AU, 0xAA45882AU,
+    0xAD4746D4U, 0xAD4746D5U, 0xB824C63DU, 0xBB46CCD2U, 0xBB46CCD3U, 0xBF704917U, 0xCB6F6266U,
+    0xCB7B5EDFU, 0xCCE7D927U, 0xD15517D4U, 0xD38BCABAU, 0xD38BCABBU, 0xD5704484U, 0xD5704485U,
+    0xD84E04AAU, 0xD84E04ABU, 0xE0794C51U, 0xE5296126U, 0xE86A25CFU, 0xEF7D3366U, 0xF0923C79U,
+    0xF5DE4480U, 0xF9C0B6B0U, 0xFDA23E68U,
 };
 
 } // namespace
 
 Facts generated_facts() noexcept {
-    return {kImageTimestamp,
-            kImageSize,
-            kEmptyCatalystPlugHash,
-            kLegacyCompletionPlugHashes,
-            kReleasedWeaponHashes};
-}
-
-bool matches_target_build(const BuildIdentity& build) noexcept {
-    return build.imageTimestamp == kImageTimestamp && build.imageSize == kImageSize;
+    return {kImageTimestamp, kImageSize, kReleasedWeaponHashes};
 }
 
 } // namespace sunrise::state::build_data::items::catalysts

+ 22 - 35
Sunrise/src/state/build_data/runtime.h

@@ -26,6 +26,10 @@
 #include "spawn_sets/definition.h"
 #include "vendors/definition.h"
 
+namespace sunrise::state::build_data::items::catalysts {
+struct Source;
+}
+
 namespace sunrise::state::build_data {
 
 /**
@@ -198,36 +202,25 @@ publish_socket_plug_rules(std::span<const items::socket_plugs::Rule> rules,
 /**
  * Derives released and placeholder catalysts from one complete package pass.
  * The active PE identity supplies the build fingerprint.
- * @param itemDefinitions Complete installed item table.
- * @param itemDetails Complete configured item-detail table.
- * @param socketPlugRules Complete item-and-lane rule table.
- * @param socketPlugPools Complete interned plug-pool table.
- * @param socketPlugMembers Complete plug-pool member table.
- * @param completionConditions Dense item-indexed catalyst completion expressions.
- * @param acquisitionGates Dense socket-type-indexed acquired-state gates.
+ * @param source Complete installed item, detail, and socket-pool domains.
  * @param output Fixed storage that receives the catalyst catalog.
  * @param count Receives the used output row count.
  * @param report Receives catalog counts and the first unsafe released relation.
  * @return True when the complete catalog is safe for publication.
  */
-[[nodiscard]] bool derive_exotic_catalysts(
-    std::span<const items::Definition> itemDefinitions,
-    std::span<const items::details::Definition> itemDetails,
-    std::span<const items::socket_plugs::Rule> socketPlugRules,
-    std::span<const items::socket_plugs::Pool> socketPlugPools,
-    std::span<const items::socket_plugs::Member> socketPlugMembers,
-    std::span<const items::catalysts::CompletionCondition> completionConditions,
-    std::span<const items::catalysts::AcquisitionGate> acquisitionGates,
-    std::span<items::catalysts::Definition> output,
-    std::size_t& count,
-    items::catalysts::Report& report) noexcept;
+[[nodiscard]] bool derive_exotic_catalysts(const items::catalysts::Source& source,
+                                           std::span<items::catalysts::Definition> output,
+                                           std::size_t& count,
+                                           items::catalysts::Report& report) noexcept;
 
 /**
+ * @param source Complete installed item, detail, and socket-pool domains.
  * @param definitions Complete build-derived catalyst catalog.
  * @return True when validation, publication, and any due cache write succeed.
  */
-[[nodiscard]] bool publish_exotic_catalysts(
-    std::span<const items::catalysts::Definition> definitions) noexcept;
+[[nodiscard]] bool
+publish_exotic_catalysts(const items::catalysts::Source& source,
+                         std::span<const items::catalysts::Definition> definitions) noexcept;
 
 /**
  * @param itemDefinitionIndex Native item definition index.
@@ -235,13 +228,10 @@ publish_socket_plug_rules(std::span<const items::socket_plugs::Rule> rules,
  * @param plugs Candidate ordinary socket plugs.
  * @return Completed, unchanged, or failed without a partial change.
  */
-[[nodiscard]] items::catalysts::ApplyResult 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. */
-[[nodiscard]] bool complete_exotic_catalyst_investment(Family5State& family) noexcept;
+[[nodiscard]] items::catalysts::ApplyResult
+complete_exotic_catalyst(std::uint16_t itemDefinitionIndex,
+                         std::uint32_t& flags,
+                         std::span<std::optional<std::uint16_t>> plugs) noexcept;
 
 /**
  * Resolves the item row that supplies one socketed catalyst's native perks and stat changes.
@@ -250,10 +240,10 @@ publish_socket_plug_rules(std::span<const items::socket_plugs::Rule> rules,
  * @param plugDefinitionIndex Socketed plug definition index.
  * @return Effective definition index, or the input plug when no released completion matches.
  */
-[[nodiscard]] std::uint16_t resolve_exotic_catalyst_effect(
-    std::uint16_t itemDefinitionIndex,
-    std::uint8_t socketLane,
-    std::uint16_t plugDefinitionIndex) noexcept;
+[[nodiscard]] std::uint16_t
+resolve_exotic_catalyst_effect(std::uint16_t itemDefinitionIndex,
+                               std::uint8_t socketLane,
+                               std::uint16_t plugDefinitionIndex) noexcept;
 
 /**
  * @param itemDefinitionIndex Native item definition index.
@@ -261,10 +251,7 @@ publish_socket_plug_rules(std::span<const items::socket_plugs::Rule> rules,
  * @return True when the catalog owns this catalyst socket lane.
  */
 [[nodiscard]] bool is_exotic_catalyst_lane(std::uint16_t itemDefinitionIndex,
-                                            std::uint8_t socketLane) noexcept;
-
-/** @return Counts and status for the published catalyst catalog. */
-[[nodiscard]] items::catalysts::Report exotic_catalyst_report() noexcept;
+                                           std::uint8_t socketLane) noexcept;
 
 /** @param enabled True to complete released catalysts during item resolution. */
 void set_exotic_catalyst_completion_enabled(bool enabled) noexcept;

+ 8 - 12
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp

@@ -65,7 +65,6 @@ to_record(const constants::InvestmentConstants& value) noexcept {
     counts = {};
     const cache::records::MutableDomains scratch = scratch_domains(state);
     *scratch.constants = to_record(constants::snapshot());
-    items::catalysts::Report catalystReport{};
     return content::snapshot(scratch.named, counts.named)
            && items::snapshot(scratch.items, counts.items)
            && collectibles::snapshot(scratch.collectibles, counts.collectibles)
@@ -78,8 +77,7 @@ to_record(const constants::InvestmentConstants& value) noexcept {
                                             counts.socketPlugPools,
                                             scratch.socketPlugMembers,
                                             counts.socketPlugMembers)
-           && items::catalysts::snapshot(
-               scratch.exoticCatalysts, counts.exoticCatalysts, catalystReport)
+           && items::catalysts::snapshot(scratch.exoticCatalysts, counts.exoticCatalysts)
            && inventory::buckets::snapshot(scratch.inventoryBuckets, counts.inventoryBuckets)
            && socket_entry_lists::snapshot(scratch.socketEntryLists, counts.socketEntryLists)
            && socket_entry_lists::snapshot_entry_tables(scratch.socketEntryTables,
@@ -112,11 +110,10 @@ bool all_domains_ready() noexcept {
     constants::InvestmentConstants published{};
     return runtime::named::ready() && item_definitions_ready() && configured_item_details_ready()
            && collectible_definitions_ready() && socket_plug_rules_ready()
-           && exotic_catalysts_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()
-           && hash_names_ready() && constants::find(published);
+           && exotic_catalysts_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() && hash_names_ready() && constants::find(published);
 }
 
 /** Gives mutable views over every fixed snapshot buffer. */
@@ -144,10 +141,9 @@ cache::records::MutableDomains scratch_domains(Context& state) noexcept {
     const auto socketPlugMembers = ensure_scratch<build_data::items::socket_plugs::Member,
                                                   build_data::items::socket_plugs::kMemberCapacity>(
         state.socketPlugMemberScratch);
-    const auto exoticCatalysts =
-        ensure_scratch<build_data::items::catalysts::Definition,
-                       build_data::items::catalysts::kDefinitionCapacity>(
-            state.exoticCatalystScratch);
+    const auto exoticCatalysts = ensure_scratch<build_data::items::catalysts::Definition,
+                                                build_data::items::catalysts::kDefinitionCapacity>(
+        state.exoticCatalystScratch);
     const auto inventoryBuckets =
         ensure_scratch<inventory::buckets::Descriptor, inventory::buckets::kDescriptorCapacity>(
             state.inventoryBucketScratch);

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

@@ -492,11 +492,7 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 /** @return A copy of the active account state, read under the lock. */
 [[nodiscard]] AccountState account_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;
+/** @return A copy of the evaluated content state, read under the lock. */
+[[nodiscard]] InvestmentState investment_snapshot() noexcept;
 
 } // namespace sunrise::state

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

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

+ 118 - 0
tests/Sunrise.Tests.vcxproj

@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>18.0</VCProjectVersion>
+    <ProjectGuid>{9D2A5866-9D2F-4A07-A88C-6E713A355C94}</ProjectGuid>
+    <RootNamespace>sunrise_tests</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
+    <ProjectName>Sunrise.Tests</ProjectName>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <PlatformToolset>v145</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
+    <UseDebugLibraries>true</UseDebugLibraries>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
+    <UseDebugLibraries>false</UseDebugLibraries>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <PropertyGroup>
+    <OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>$(ProjectDir)..\build\obj\$(Platform)\$(Configuration)\tests\</IntDir>
+  </PropertyGroup>
+  <ItemDefinitionGroup>
+    <ClCompile>
+      <LanguageStandard>stdcpp20</LanguageStandard>
+      <WarningLevel>Level4</WarningLevel>
+      <TreatWarningAsError>true</TreatWarningAsError>
+      <ConformanceMode>true</ConformanceMode>
+      <MultiProcessorCompilation>false</MultiProcessorCompilation>
+      <AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
+      <AdditionalIncludeDirectories>$(ProjectDir)..\Sunrise\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions>WIN32;_WINDOWS;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
+    <ClCompile>
+      <Optimization>Disabled</Optimization>
+      <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
+    </ClCompile>
+    <Link>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
+    <ClCompile>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
+    </ClCompile>
+    <Link>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <OptimizeReferences>true</OptimizeReferences>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClCompile Include="settings_regression_tests.cpp" />
+    <ClCompile Include="catalyst_regression_tests.cpp" />
+    <ClCompile Include="loadout_catalyst_regression_tests.cpp" />
+    <ClCompile Include="opcode406_item_state_regression_tests.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\json_number.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\json_scalar.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\json_structure.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state_settings.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\address_text.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\client\client_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\client\client_ui_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\client\external\client_external_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\server\activation\activation_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\server\entitlement_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\server\gameplay\gameplay_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\server\gameplay\gameplay_settings_validation.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\server\server_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\account_rows_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\account_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\activity_arrival_override_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\activity_default_destination_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\audio_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\controls_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\display_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\family5_override_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\interface_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\inventory_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\key_bindings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\state\social_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\core\settings\steam\steam_settings_parser.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\account\inventory\inventory_state.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\items\catalysts\exotic_catalyst_builder.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\items\catalysts\exotic_catalyst_catalog.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\cache\records\cache_exotic_catalyst_record_codec.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\datagen\family4\loadout\loadout_item_resolver.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\datagen\family4\loadout\subclass_socket_selection.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\datagen\character_record\appearance\character_appearance_banks.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\datagen\character_record\appearance\character_appearance_render.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\datagen\character_record\appearance\character_appearance_stats.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\items\item_catalog.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\items\details\item_detail_catalog.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\inventory\buckets\inventory_bucket_catalog.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\socket_entry_lists\socket_entry_list_catalog.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\encoding\bit_reader.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\web_service\messages\opcode406_codec.cpp" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+</Project>

+ 348 - 0
tests/catalyst_regression_tests.cpp

@@ -0,0 +1,348 @@
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <optional>
+#include <span>
+
+#include "state/account/inventory/item_state.h"
+#include "state/build_data/cache/records/codec.h"
+#include "state/build_data/items/catalysts/exotic_catalyst_builder.h"
+#include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
+
+extern int failures;
+
+namespace {
+
+namespace cache_records = sunrise::state::build_data::cache::records;
+namespace catalysts = sunrise::state::build_data::items::catalysts;
+namespace details = sunrise::state::build_data::items::details;
+namespace items = sunrise::state::build_data::items;
+namespace socket_plugs = sunrise::state::build_data::items::socket_plugs;
+
+constexpr std::uint32_t kTimestamp = 0x12345678U;
+constexpr std::uint32_t kImageSize = 0x01000000U;
+constexpr std::uint32_t kLegacyWeaponHash = 0x10000010U;
+constexpr std::uint32_t kLaterWeaponHash = 0x10000020U;
+constexpr std::uint32_t kPlaceholderWeaponHash = 0x10000030U;
+constexpr std::uint32_t kNonCatalystWeaponHash = 0x10000040U;
+
+void expect(bool value, const char* label) noexcept {
+    if (!value) {
+        std::fprintf(stderr, "FAIL %s\n", label);
+        ++failures;
+    }
+}
+
+items::Definition item(std::uint16_t index,
+                       std::uint32_t hash,
+                       items::Tier tier = items::Tier::none,
+                       std::uint32_t category = 0) noexcept {
+    items::Definition value{};
+    value.definitionIndex = index;
+    value.definitionHash = hash;
+    value.tier = static_cast<std::uint8_t>(tier);
+    value.plugCategoryHash = category;
+    return value;
+}
+
+details::Definition weapon_detail(std::uint16_t index,
+                                  std::uint32_t hash,
+                                  std::uint8_t lane,
+                                  std::uint16_t defaultPlug) noexcept {
+    details::Definition value{};
+    value.definitionIndex = index;
+    value.definitionHash = hash;
+    value.instancedDefinitionState = details::InstancedDefinitionState::instanced;
+    value.equipmentSlot = std::int8_t{7};
+    value.ordinarySocketState = details::OrdinarySocketState::present;
+    value.ordinarySocketCount = static_cast<std::uint8_t>(lane + 1);
+    value.initialPlugIndices[lane] = defaultPlug;
+    return value;
+}
+
+details::Definition plug_detail(std::uint16_t index,
+                                std::uint32_t hash,
+                                std::int32_t maxStackSize,
+                                bool hasEffect = false) noexcept {
+    details::Definition value{};
+    value.definitionIndex = index;
+    value.definitionHash = hash;
+    value.maxStackSize = maxStackSize;
+    value.instancedDefinitionState = details::InstancedDefinitionState::stackable;
+    if (hasEffect) {
+        value.sandboxPerkCount = 1;
+        value.sandboxPerks[0] = 7;
+    }
+    return value;
+}
+
+struct Fixture {
+    static constexpr std::uint16_t kLegacyWeapon = 10;
+    static constexpr std::uint16_t kLaterWeapon = 11;
+    static constexpr std::uint16_t kPlaceholderWeapon = 12;
+    static constexpr std::uint16_t kNonCatalystWeapon = 13;
+    static constexpr std::uint16_t kLegacyDefault = 20;
+    static constexpr std::uint16_t kLegacyProgress = 21;
+    static constexpr std::uint16_t kLegacyComplete = 22;
+    static constexpr std::uint16_t kLegacyEffect = 23;
+    static constexpr std::uint16_t kLaterDefault = 30;
+    static constexpr std::uint16_t kLaterActive = 31;
+    static constexpr std::uint16_t kPlaceholderDefault = 40;
+    static constexpr std::uint16_t kPlaceholderActive = 41;
+    static constexpr std::uint16_t kOtherDefault = 50;
+    static constexpr std::uint16_t kOtherActive = 51;
+
+    std::array<items::Definition, 13> itemRows{};
+    std::array<details::Definition, 11> detailRows{};
+    std::array<socket_plugs::Rule, 4> rules{{
+        {kLegacyWeapon, 7, 0, 1},
+        {kLaterWeapon, 6, 0, 2},
+        {kPlaceholderWeapon, 5, 0, 3},
+        {kNonCatalystWeapon, 4, 0, 4},
+    }};
+    std::array<socket_plugs::Pool, 5> pools{{
+        {0, 0},
+        {0, 3},
+        {3, 2},
+        {5, 2},
+        {7, 2},
+    }};
+    std::array<socket_plugs::Member, 9> members{{
+        kLegacyDefault,
+        kLegacyProgress,
+        kLegacyComplete,
+        kLaterDefault,
+        kLaterActive,
+        kPlaceholderDefault,
+        kPlaceholderActive,
+        kOtherDefault,
+        kOtherActive,
+    }};
+    std::array<std::uint32_t, 2> released{{kLegacyWeaponHash, kLaterWeaponHash}};
+    std::array<catalysts::Definition, catalysts::kDefinitionCapacity> output{};
+
+    Fixture() noexcept {
+        constexpr std::uint32_t kLegacyCategory = 0x20000010U;
+        constexpr std::uint32_t kLaterCategory = 0x20000020U;
+        constexpr std::uint32_t kPlaceholderCategory = 0x20000030U;
+        constexpr std::uint32_t kOtherCategory = 0x20000040U;
+        itemRows = {
+            item(kLegacyWeapon, kLegacyWeaponHash, items::Tier::exotic),
+            item(kLaterWeapon, kLaterWeaponHash, items::Tier::exotic),
+            item(kPlaceholderWeapon, kPlaceholderWeaponHash, items::Tier::exotic),
+            item(kNonCatalystWeapon, kNonCatalystWeaponHash, items::Tier::exotic),
+            item(kLegacyDefault, 0x30000010U, items::Tier::none, kLegacyCategory),
+            item(kLegacyProgress, 0x30000011U, items::Tier::none, kLegacyCategory),
+            item(kLegacyComplete, 0x30000012U, items::Tier::none, kLegacyCategory),
+            item(kLegacyEffect, 0x30000013U, items::Tier::exotic, kLegacyCategory),
+            item(kLaterDefault, 0x30000020U, items::Tier::none, kLaterCategory),
+            item(kLaterActive, 0x30000021U, items::Tier::exotic, kLaterCategory),
+            item(kPlaceholderDefault, 0x30000030U, items::Tier::none, kPlaceholderCategory),
+            item(kPlaceholderActive, 0x30000031U, items::Tier::exotic, kPlaceholderCategory),
+            item(kOtherActive, 0x30000041U, items::Tier::legendary, kOtherCategory),
+        };
+        detailRows = {
+            weapon_detail(kLegacyWeapon, kLegacyWeaponHash, 7, kLegacyDefault),
+            weapon_detail(kLaterWeapon, kLaterWeaponHash, 6, kLaterDefault),
+            weapon_detail(kPlaceholderWeapon, kPlaceholderWeaponHash, 5, kPlaceholderDefault),
+            weapon_detail(kNonCatalystWeapon, kNonCatalystWeaponHash, 4, kOtherDefault),
+            plug_detail(kLegacyDefault, 0x30000010U, 100),
+            plug_detail(kLegacyProgress, 0x30000011U, 1),
+            plug_detail(kLegacyComplete, 0x30000012U, 100),
+            plug_detail(kLegacyEffect, 0x30000013U, 100, true),
+            plug_detail(kLaterActive, 0x30000021U, 100, true),
+            plug_detail(kPlaceholderActive, 0x30000031U, 100, true),
+            plug_detail(kOtherActive, 0x30000041U, 100, true),
+        };
+    }
+
+    [[nodiscard]] catalysts::Source source() const noexcept {
+        return {{kTimestamp, kImageSize, 0}, itemRows, detailRows, rules, pools, members};
+    }
+
+    [[nodiscard]] catalysts::Facts facts() const noexcept {
+        return {kTimestamp, kImageSize, released};
+    }
+};
+
+bool derive(Fixture& fixture, std::size_t& count, catalysts::Report& report) noexcept {
+    return catalysts::derive(fixture.source(), fixture.facts(), fixture.output, count, report);
+}
+
+void test_structural_lifecycles() noexcept {
+    Fixture fixture;
+    std::size_t count = 0;
+    catalysts::Report report{};
+    expect(derive(fixture, count, report), "synthetic catalyst catalog derives");
+    expect(count == 3, "only two released and one placeholder catalyst are found");
+    expect(report.released == 2 && report.placeholder == 1 && report.unsupported == 0,
+           "catalog report separates released and placeholder rows");
+    if (count != 3) {
+        return;
+    }
+    expect(fixture.output[0].itemDefinitionIndex == Fixture::kLegacyWeapon
+               && fixture.output[0].completedPlugDefinitionIndex == Fixture::kLegacyComplete
+               && fixture.output[0].effectDefinitionIndex == Fixture::kLegacyEffect,
+           "legacy three-state lifecycle resolves display and effect rows");
+    expect(fixture.output[1].itemDefinitionIndex == Fixture::kLaterWeapon
+               && fixture.output[1].completedPlugDefinitionIndex == Fixture::kLaterActive
+               && fixture.output[1].effectDefinitionIndex == Fixture::kLaterActive,
+           "later two-state lifecycle resolves its direct active plug");
+    expect(fixture.output[2].availability == catalysts::Availability::placeholder,
+           "unreleased structural catalyst remains a placeholder");
+    expect(std::none_of(std::span(fixture.output).first(count).begin(),
+                        std::span(fixture.output).first(count).end(),
+                        [](const catalysts::Definition& value) {
+                            return value.itemDefinitionIndex == Fixture::kNonCatalystWeapon;
+                        }),
+           "a two-state lane without an exotic effect item is not a catalyst");
+    expect(catalysts::matches_derived(
+               fixture.source(), fixture.facts(), std::span(fixture.output).first(count)),
+           "stored catalog matches a fresh structural derivation");
+
+    auto altered = fixture.output;
+    altered[0].completedPlugDefinitionIndex = Fixture::kLegacyProgress;
+    expect(!catalysts::matches_derived(
+               fixture.source(), fixture.facts(), std::span(altered).first(count)),
+           "catalog rejects a completed plug outside the derived role");
+
+    Fixture legendaryWeapon;
+    legendaryWeapon.itemRows[2].tier = static_cast<std::uint8_t>(items::Tier::legendary);
+    expect(derive(legendaryWeapon, count, report) && count == 2 && report.placeholder == 0,
+           "non-exotic weapons are outside catalyst scope");
+
+    Fixture exoticArmor;
+    exoticArmor.detailRows[2].equipmentSlot = std::int8_t{3};
+    expect(derive(exoticArmor, count, report) && count == 2 && report.placeholder == 0,
+           "exotic armor is outside catalyst scope");
+}
+
+void test_safe_derivation_failures() noexcept {
+    Fixture fixture;
+    std::size_t count = 0;
+    catalysts::Report report{};
+    catalysts::Source mismatched = fixture.source();
+    ++mismatched.build.imageSize;
+    expect(!catalysts::derive(mismatched, fixture.facts(), fixture.output, count, report)
+               && count == 0 && report.error == catalysts::Error::unsupportedBuild,
+           "build fingerprint mismatch fails only the catalog derivation");
+
+    std::array<std::uint32_t, 3> missingRelease{kLegacyWeaponHash, kLaterWeaponHash, 0x1FFFFFFFU};
+    catalysts::Facts missingFacts{kTimestamp, kImageSize, missingRelease};
+    expect(!catalysts::derive(fixture.source(), missingFacts, fixture.output, count, report)
+               && count == 0 && report.error == catalysts::Error::missingReleased,
+           "missing released weapon clears all staged catalog rows");
+
+    fixture.detailRows[5].maxStackSize = 100;
+    expect(!derive(fixture, count, report) && count == 0
+               && report.error == catalysts::Error::missingReleased,
+           "unclear released legacy lifecycle fails closed");
+}
+
+void test_catalog_application() noexcept {
+    Fixture fixture;
+    std::size_t count = 0;
+    catalysts::Report report{};
+    if (!derive(fixture, count, report)
+        || !catalysts::replace(std::span(fixture.output).first(count))) {
+        expect(false, "application fixture publishes");
+        return;
+    }
+
+    for (std::uint32_t flags = 0; flags <= 3; ++flags) {
+        std::array<std::optional<std::uint16_t>, 12> plugs{};
+        plugs[7] = Fixture::kLegacyDefault;
+        std::uint32_t changedFlags = flags;
+        expect(catalysts::apply_completed(Fixture::kLegacyWeapon, changedFlags, plugs)
+                   == catalysts::ApplyResult::completed,
+               "released legacy catalyst completes");
+        expect(changedFlags == (flags | sunrise::state::account::inventory::kMasterworkItemFlag),
+               "completion preserves all prior item-state bits");
+        expect(plugs[7] == Fixture::kLegacyComplete,
+               "completion sockets the display plug from the allowed pool");
+    }
+    expect(catalysts::resolve_effect(Fixture::kLegacyWeapon, 7, Fixture::kLegacyComplete)
+               == Fixture::kLegacyEffect,
+           "legacy display plug resolves its effect item");
+    expect(catalysts::resolve_effect(Fixture::kLegacyWeapon, 7, Fixture::kLegacyDefault)
+               == Fixture::kLegacyDefault,
+           "uncompleted display plug remains unchanged");
+
+    std::array<std::optional<std::uint16_t>, 12> laterPlugs{};
+    laterPlugs[6] = Fixture::kLaterDefault;
+    std::uint32_t laterFlags = 0;
+    expect(catalysts::apply_completed(Fixture::kLaterWeapon, laterFlags, laterPlugs)
+                   == catalysts::ApplyResult::completed
+               && laterPlugs[6] == Fixture::kLaterActive,
+           "later catalyst sockets its direct active plug");
+
+    std::array<std::optional<std::uint16_t>, 12> placeholderPlugs{};
+    placeholderPlugs[5] = Fixture::kPlaceholderDefault;
+    const auto placeholderBefore = placeholderPlugs;
+    std::uint32_t placeholderFlags = 3;
+    expect(
+        catalysts::apply_completed(Fixture::kPlaceholderWeapon, placeholderFlags, placeholderPlugs)
+                == catalysts::ApplyResult::unchanged
+            && placeholderFlags == 3 && placeholderPlugs == placeholderBefore,
+        "placeholder catalyst remains unchanged");
+
+    std::array<std::optional<std::uint16_t>, 12> invalidPlugs{};
+    invalidPlugs[7] = Fixture::kLegacyDefault;
+    const auto invalidBefore = invalidPlugs;
+    std::uint32_t invalidFlags = 0x8U;
+    expect(catalysts::apply_completed(Fixture::kLegacyWeapon, invalidFlags, invalidPlugs)
+                   == catalysts::ApplyResult::failed
+               && invalidFlags == 0x8U && invalidPlugs == invalidBefore,
+           "invalid item state fails without a partial change");
+
+    std::array<std::optional<std::uint16_t>, 7> shortPlugs{};
+    const auto shortBefore = shortPlugs;
+    std::uint32_t shortFlags = 1;
+    expect(catalysts::apply_completed(Fixture::kLegacyWeapon, shortFlags, shortPlugs)
+                   == catalysts::ApplyResult::failed
+               && shortFlags == 1 && shortPlugs == shortBefore,
+           "missing catalyst lane fails without a partial change");
+
+    catalysts::set_completion_enabled(false);
+    std::array<std::optional<std::uint16_t>, 12> disabledPlugs{};
+    disabledPlugs[7] = Fixture::kLegacyDefault;
+    const auto disabledBefore = disabledPlugs;
+    std::uint32_t disabledFlags = 2;
+    expect(catalysts::apply_completed(Fixture::kLegacyWeapon, disabledFlags, disabledPlugs)
+                   == catalysts::ApplyResult::unchanged
+               && disabledFlags == 2 && disabledPlugs == disabledBefore,
+           "global policy disables completion without changing the item");
+    catalysts::clear();
+}
+
+void test_cache_record() noexcept {
+    const catalysts::Definition definition{
+        0x11111111U, 3, 7, 9, 5, catalysts::Availability::released};
+    cache_records::ExoticCatalystRecord record{};
+    catalysts::Definition decoded{};
+    expect(cache_records::encode(definition, record), "catalyst cache record encodes");
+    expect(cache_records::decode(record, decoded)
+               && decoded.itemDefinitionHash == definition.itemDefinitionHash
+               && decoded.itemDefinitionIndex == definition.itemDefinitionIndex
+               && decoded.completedPlugDefinitionIndex == definition.completedPlugDefinitionIndex
+               && decoded.effectDefinitionIndex == definition.effectDefinitionIndex
+               && decoded.socketLane == definition.socketLane
+               && decoded.availability == definition.availability,
+           "catalyst cache record round-trips");
+    record.availability = 0xFFU;
+    expect(!cache_records::decode(record, decoded), "invalid catalyst availability is rejected");
+    expect(cache_records::kCacheFormatVersion == 45,
+           "catalyst records use one cache bump over upstream version 44");
+}
+
+} // namespace
+
+void test_exotic_catalysts() noexcept {
+    test_structural_lifecycles();
+    test_safe_derivation_failures();
+    test_catalog_application();
+    test_cache_record();
+    catalysts::clear();
+}

+ 226 - 0
tests/loadout_catalyst_regression_tests.cpp

@@ -0,0 +1,226 @@
+#include <array>
+#include <cstdint>
+#include <cstdio>
+#include <optional>
+#include <span>
+
+#include "middleware/datagen/character_record/appearance/internal.h"
+#include "middleware/datagen/family4/loadout/loadout_item_resolver.h"
+#include "state/build_data/inventory/buckets/inventory_bucket_catalog.h"
+#include "state/build_data/items/catalysts/exotic_catalyst_catalog.h"
+#include "state/build_data/items/details/item_detail_catalog.h"
+#include "state/build_data/items/item_catalog.h"
+#include "state/build_data/runtime.h"
+#include "state/build_data/socket_entry_lists/socket_entry_list_catalog.h"
+
+extern int failures;
+
+namespace {
+
+bool forceCompletionFailure{};
+
+} // namespace
+
+namespace sunrise::state::build_data {
+
+bool find_item_definition_hash(std::uint32_t definitionHash,
+                               items::Definition& definition) noexcept {
+    return items::find_hash(definitionHash, definition);
+}
+
+bool find_configured_item_detail(std::uint16_t definitionIndex,
+                                 items::details::Definition& definition) noexcept {
+    return items::details::find(definitionIndex, definition);
+}
+
+bool find_inventory_bucket_descriptor(std::uint8_t bucketId,
+                                      inventory::buckets::Descriptor& descriptor) noexcept {
+    return inventory::buckets::find(bucketId, descriptor);
+}
+
+bool find_socket_entry_list(std::uint16_t definitionIndex,
+                            socket_entry_lists::Definition& definition) noexcept {
+    return socket_entry_lists::find(definitionIndex, definition);
+}
+
+bool find_socket_entry_table(std::uint16_t definitionIndex,
+                             socket_entry_lists::EntryTable& table) noexcept {
+    return socket_entry_lists::find_entry_table(definitionIndex, table);
+}
+
+items::catalysts::ApplyResult
+complete_exotic_catalyst(std::uint16_t itemDefinitionIndex,
+                         std::uint32_t& flags,
+                         std::span<std::optional<std::uint16_t>> plugs) noexcept {
+    if (forceCompletionFailure) {
+        flags = 7;
+        if (!plugs.empty()) {
+            plugs.front() = std::uint16_t{4};
+            plugs.back() = std::uint16_t{4};
+        }
+        return items::catalysts::ApplyResult::failed;
+    }
+    return items::catalysts::apply_completed(itemDefinitionIndex, flags, plugs);
+}
+
+bool find_investment_constants(constants::InvestmentConstants& value) noexcept {
+    value = {.extracted = true, .lightStatRow = 1, .characterStatRows = {2, 3, 4, 5, 6, 7}};
+    return true;
+}
+
+std::uint16_t resolve_exotic_catalyst_effect(std::uint16_t itemDefinitionIndex,
+                                             std::uint8_t socketLane,
+                                             std::uint16_t plugDefinitionIndex) noexcept {
+    return items::catalysts::resolve_effect(itemDefinitionIndex, socketLane, plugDefinitionIndex);
+}
+
+} // namespace sunrise::state::build_data
+
+namespace {
+
+namespace appearance = sunrise::middleware::datagen::character_record::appearance;
+namespace buckets = sunrise::state::build_data::inventory::buckets;
+namespace catalysts = sunrise::state::build_data::items::catalysts;
+namespace character_layout = sunrise::middleware::datagen::character_record::layout;
+namespace details = sunrise::state::build_data::items::details;
+namespace items = sunrise::state::build_data::items;
+namespace loadout = sunrise::middleware::datagen::family4::loadout;
+namespace socket_lists = sunrise::state::build_data::socket_entry_lists;
+
+constexpr std::uint32_t kWeaponHash = 0x12345678U;
+constexpr std::uint32_t kDefaultPlugHash = 0x23456789U;
+constexpr std::uint32_t kProgressPlugHash = 0x3456789AU;
+constexpr std::uint32_t kCompletedPlugHash = 0x456789ABU;
+constexpr std::uint32_t kEffectHash = 0x56789ABCU;
+constexpr std::uint16_t kCatalystPerk = 77;
+constexpr std::uint8_t kCatalystStatRow = 24;
+constexpr std::int32_t kCatalystStatValue = 7;
+
+void expect(bool value, const char* label) noexcept {
+    if (!value) {
+        std::fprintf(stderr, "FAIL %s\n", label);
+        ++failures;
+    }
+}
+
+items::Definition
+item(std::uint16_t index, std::uint32_t hash, items::Tier tier = items::Tier::none) noexcept {
+    items::Definition value{};
+    value.definitionHash = hash;
+    value.definitionIndex = index;
+    value.tier = static_cast<std::uint8_t>(tier);
+    return value;
+}
+
+void clear_catalogs() noexcept {
+    forceCompletionFailure = false;
+    catalysts::clear();
+    socket_lists::clear();
+    buckets::clear();
+    details::clear();
+    items::clear();
+}
+
+} // namespace
+
+void test_resolved_catalyst_output() noexcept {
+    clear_catalogs();
+    std::array<items::Definition, 5> itemRows{
+        item(0, kWeaponHash, items::Tier::exotic),
+        item(1, kDefaultPlugHash),
+        item(2, kProgressPlugHash),
+        item(3, kCompletedPlugHash),
+        item(4, kEffectHash, items::Tier::exotic),
+    };
+    itemRows[0].bucketId = 0;
+
+    details::Definition detail{};
+    detail.definitionIndex = 0;
+    detail.definitionHash = kWeaponHash;
+    detail.bucketId = 0;
+    detail.maxStackSize = 1;
+    detail.instancedDefinitionState = details::InstancedDefinitionState::instanced;
+    detail.equipmentSlot = std::int8_t{7};
+    detail.ordinarySocketState = details::OrdinarySocketState::present;
+    detail.ordinarySocketCount = 8;
+    detail.initialPlugIndices[7] = 1;
+    detail.socketEntryListIndex = 0;
+    details::Definition effectDetail{};
+    effectDetail.definitionIndex = 4;
+    effectDetail.definitionHash = kEffectHash;
+    effectDetail.bucketId = 0;
+    effectDetail.maxStackSize = 1;
+    effectDetail.instancedDefinitionState = details::InstancedDefinitionState::stackable;
+    effectDetail.sandboxPerkCount = 1;
+    effectDetail.sandboxPerks[0] = kCatalystPerk;
+    effectDetail.statCount = 1;
+    effectDetail.stats[0] = {kCatalystStatRow, kCatalystStatValue};
+    const std::array detailRows{detail, effectDetail};
+
+    const std::array bucketRows{
+        buckets::Descriptor{0, buckets::ArraySelector::character, 0, 10, 7, 0},
+    };
+    const std::array socketListRows{
+        socket_lists::Definition{0x12345678U, 0, 0, 0},
+    };
+    const std::array catalystRows{
+        catalysts::Definition{kWeaponHash, 0, 3, 4, 7, catalysts::Availability::released},
+    };
+
+    const bool ready = items::replace(itemRows) && details::replace(detailRows)
+                       && buckets::replace(bucketRows) && socket_lists::replace(socketListRows)
+                       && catalysts::replace(catalystRows);
+    expect(ready, "resolved-output fixture publishes");
+    if (!ready) {
+        clear_catalogs();
+        return;
+    }
+
+    sunrise::state::account::inventory::Item authored{};
+    authored.instanceSoid = 1;
+    authored.definitionHash = kWeaponHash;
+    authored.level = 50;
+    authored.quantity = 1;
+    authored.flags = 3;
+    sunrise::state::CharacterState character{};
+    loadout::Candidate output{};
+    expect(
+        loadout::resolve_item(authored, character, itemRows.size(), socketListRows.size(), output),
+        "resolved client item accepts catalyst completion");
+    expect(output.item.flags == 7, "resolved client item carries all item-state bits");
+    expect(output.item.instance.ordinarySockets.plugs[7] == 3,
+           "resolved client item carries the completed display plug");
+
+    loadout::ResolvedInstances instances{};
+    instances.itemCount = 1;
+    instances.items[0].equipmentSlot = 7;
+    instances.items[0].instance = output.item.instance;
+    character_layout::Appearance appearanceRecord{};
+    appearanceRecord.smallBankA.fill(character_layout::kEmptyDefinitionIndex);
+    appearanceRecord.overflowHashes.fill(character_layout::kNoHash);
+    appearance::apply_perk_banks(instances, appearanceRecord);
+    expect(appearanceRecord.smallBankA[0] == kCatalystPerk,
+           "display plug resolves the catalyst perk from its effect row");
+    appearance::apply_overflow_hashes(instances, appearanceRecord);
+    expect(appearanceRecord.overflowHashes[0] == kEffectHash,
+           "display plug resolves the catalyst hash from its effect row");
+    expect(appearance::apply_stats(instances, 50, appearanceRecord),
+           "display plug builds catalyst stats");
+    expect(appearanceRecord.weaponStats[0][0].key == static_cast<std::int8_t>(kCatalystStatRow)
+               && appearanceRecord.weaponStats[0][0].value == kCatalystStatValue,
+           "display plug resolves the catalyst stat from its effect row");
+
+    forceCompletionFailure = true;
+    loadout::Candidate failedOutput{};
+    failedOutput.item.flags = 0xA5A5U;
+    expect(loadout::resolve_item(
+               authored, character, itemRows.size(), socketListRows.size(), failedOutput),
+           "catalyst apply failure preserves the resolved base item");
+    expect(failedOutput.item.flags == authored.flags,
+           "catalyst apply failure does not leak changed flags");
+    expect(failedOutput.item.instance.ordinarySockets.plugs[7] == 1
+               && !failedOutput.item.instance.ordinarySockets.plugs[0].has_value(),
+           "catalyst apply failure does not leak changed plugs");
+
+    clear_catalogs();
+}

+ 67 - 0
tests/opcode406_item_state_regression_tests.cpp

@@ -0,0 +1,67 @@
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <span>
+
+#include "middleware/web_service/messages/opcode406.h"
+
+extern int failures;
+
+namespace {
+
+namespace opcode406 = sunrise::middleware::web_service::messages::opcode406;
+
+void expect(bool value, const char* label) noexcept {
+    if (!value) {
+        std::fprintf(stderr, "FAIL %s\n", label);
+        ++failures;
+    }
+}
+
+void write_bits(std::span<std::byte> output,
+                std::size_t& position,
+                std::uint64_t value,
+                std::uint8_t width) noexcept {
+    for (std::uint8_t index = 0; index < width; ++index) {
+        const unsigned shift = static_cast<unsigned>(width - index - 1);
+        const auto bit = static_cast<unsigned>((value >> shift) & 1U);
+        const std::size_t byteIndex = position / 8;
+        const unsigned byteShift = static_cast<unsigned>(7 - (position % 8));
+        output[byteIndex] |= std::byte{static_cast<unsigned char>(bit << byteShift)};
+        ++position;
+    }
+}
+
+std::array<std::byte, 15> payload(std::uint32_t flags) noexcept {
+    std::array<std::byte, 15> output{};
+    std::size_t position = 0;
+    write_bits(output, position, 1, 1);
+    write_bits(output, position, 0x1234U, 64);
+    write_bits(output, position, 1, 1);
+    write_bits(output, position, 42, 15);
+    write_bits(output, position, 0x80000000ULL + flags, 32);
+    write_bits(output, position, 0, 7);
+    return output;
+}
+
+} // namespace
+
+void test_opcode406_item_state() noexcept {
+    for (std::uint32_t flags = 0; flags <= 7; ++flags) {
+        const auto bytes = payload(flags);
+        const sunrise::middleware::web_service::Message message{opcode406::kOpcode, 1, bytes};
+        opcode406::Request request{};
+        expect(opcode406::parse_request(message, request),
+               "opcode 406 accepts known item-state bits");
+        expect(request.instanceSoid == 0x1234U && request.definitionIndex == 42
+                   && request.flags == flags,
+               "opcode 406 preserves known item-state bits");
+    }
+
+    const auto bytes = payload(8);
+    const sunrise::middleware::web_service::Message message{opcode406::kOpcode, 1, bytes};
+    opcode406::Request request{};
+    expect(!opcode406::parse_request(message, request),
+           "opcode 406 rejects unknown item-state bits");
+}

+ 140 - 0
tests/settings_regression_tests.cpp

@@ -0,0 +1,140 @@
+#include <Windows.h>
+
+#include <crtdbg.h>
+#include <cstdint>
+#include <cstdio>
+#include <string_view>
+
+#include "core/settings/settings.h"
+#include "state/account/inventory/item_state.h"
+
+namespace sunrise::core::log {
+
+Settings defaults() noexcept {
+    return {};
+}
+
+void early(std::string_view) noexcept {}
+
+void write(Channel, Level, std::string_view) noexcept {}
+
+} // namespace sunrise::core::log
+
+namespace sunrise::state::activity::defaults {
+
+ActivityDefaults authored() noexcept {
+    return {};
+}
+
+bool valid(const DefaultDestination&) noexcept {
+    return true;
+}
+
+bool valid(const ActivityDefaults&) noexcept {
+    return true;
+}
+
+} // namespace sunrise::state::activity::defaults
+
+namespace sunrise::state::entitlements {
+
+Table authored() noexcept {
+    return {};
+}
+
+bool valid(const Entitlement&) noexcept {
+    return true;
+}
+
+bool valid(const Table&) noexcept {
+    return true;
+}
+
+} // namespace sunrise::state::entitlements
+
+namespace sunrise::state::account {
+
+bool valid_authored(const AccountState&) noexcept {
+    return true;
+}
+
+} // namespace sunrise::state::account
+
+namespace sunrise::state::account::settings {
+
+bool valid(const AccountSettings&) noexcept {
+    return true;
+}
+
+} // namespace sunrise::state::account::settings
+
+int failures{};
+
+namespace {
+
+void expect(bool value, const char* label) noexcept {
+    if (!value) {
+        std::fprintf(stderr, "FAIL %s\n", label);
+        ++failures;
+    }
+}
+
+bool parse_catalyst_policy(std::string_view json, bool& output) noexcept {
+    sunrise::core::settings::Settings settings{};
+    if (!sunrise::core::settings::parse(json, settings)) {
+        return false;
+    }
+    output = settings.completeExoticCatalysts;
+    return true;
+}
+
+void test_item_state_contract() noexcept {
+    for (std::uint32_t flags = 0; flags <= 0x7U; ++flags) {
+        expect(sunrise::state::account::inventory::valid_item_state(flags),
+               "known item-state bits are valid");
+    }
+    expect(!sunrise::state::account::inventory::valid_item_state(0x8U),
+           "unknown item-state bits are invalid");
+}
+
+void test_optional_catalyst_policy() noexcept {
+    bool enabled = false;
+    expect(sunrise::core::settings::kSettingsVersion == 8,
+           "optional catalyst key keeps settings version 8");
+    expect(parse_catalyst_policy(R"({"version":8})", enabled) && enabled,
+           "version 8 without catalyst key keeps the true default");
+    expect(parse_catalyst_policy(R"({"version":8,"complete_exotic_catalysts":false})", enabled)
+               && !enabled,
+           "version 8 accepts an explicit false catalyst policy");
+    expect(
+        !parse_catalyst_policy(
+            R"({"version":8,"complete_exotic_catalysts":true,"complete_exotic_catalysts":false})",
+            enabled),
+        "duplicate catalyst policy is rejected");
+    expect(!parse_catalyst_policy(R"({"version":8,"complete_exotic_catalysts":1})", enabled),
+           "non-boolean catalyst policy is rejected");
+}
+
+} // namespace
+
+void test_exotic_catalysts() noexcept;
+void test_resolved_catalyst_output() noexcept;
+void test_opcode406_item_state() noexcept;
+
+int main() {
+#if defined(_DEBUG)
+    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
+    _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
+#endif
+    test_item_state_contract();
+    test_optional_catalyst_policy();
+    test_opcode406_item_state();
+    test_exotic_catalysts();
+    test_resolved_catalyst_output();
+    if (failures != 0) {
+        std::fprintf(stderr, "%d regression test(s) failed\n", failures);
+        return 1;
+    }
+    std::puts("All regression tests passed");
+    return 0;
+}