فهرست منبع

feat(content): extract inventory action metadata

Read collectible links, socket plug pools, item socket layouts, and material requirement sets from the installed investment packages instead of embedding build-specific gameplay data.

Persist the new domains in cache format v27, validate their item cross-links and canonical ordering, and make readiness/persistence fail closed so incomplete extraction cannot publish a usable catalog.

This provides one runtime-derived source of truth for Collections, cosmetic/mod compatibility, and material charging while preserving the public repository's no-copyrighted-data design.
Thomas Shields 3 هفته پیش
والد
کامیت
7cc5fb10ff
46فایلهای تغییر یافته به همراه2820 افزوده شده و 159 حذف شده
  1. 8 2
      Sunrise/src/client/content/investment/investment_refresh.cpp
  2. 8 3
      Sunrise/src/client/content/items/details/configured_item_detail_extractor.cpp
  3. 86 22
      Sunrise/src/client/content/items/packages/internal.h
  4. 31 5
      Sunrise/src/client/content/items/packages/package_build_report.cpp
  5. 176 0
      Sunrise/src/client/content/items/packages/package_collectible_build.cpp
  6. 68 31
      Sunrise/src/client/content/items/packages/package_detail_build.cpp
  7. 35 4
      Sunrise/src/client/content/items/packages/package_item_build.cpp
  8. 109 23
      Sunrise/src/client/content/items/packages/package_item_rows.cpp
  9. 183 0
      Sunrise/src/client/content/items/packages/package_material_requirement_build.cpp
  10. 275 0
      Sunrise/src/client/content/items/packages/package_socket_plug_build.cpp
  11. 86 0
      Sunrise/src/client/content/items/packages/package_socket_plug_build.h
  12. 39 0
      Sunrise/src/middleware/content/packages/tables/definition_index_table.h
  13. 135 0
      Sunrise/src/middleware/content/packages/tables/item_definition_reader.cpp
  14. 31 0
      Sunrise/src/middleware/content/packages/tables/items.h
  15. 7 0
      Sunrise/src/state/build_data/build_data_runtime.cpp
  16. 13 1
      Sunrise/src/state/build_data/cache/read/cache_file_reader.cpp
  17. 38 0
      Sunrise/src/state/build_data/cache/read/cache_payload_reader.cpp
  18. 69 3
      Sunrise/src/state/build_data/cache/records/cache_detail_links.cpp
  19. 65 2
      Sunrise/src/state/build_data/cache/records/cache_domain_validation.cpp
  20. 111 1
      Sunrise/src/state/build_data/cache/records/cache_record_codec.cpp
  21. 53 0
      Sunrise/src/state/build_data/cache/records/cache_socket_plug_record_codec.cpp
  22. 32 0
      Sunrise/src/state/build_data/cache/records/codec.h
  23. 18 0
      Sunrise/src/state/build_data/cache/records/domains.h
  24. 85 4
      Sunrise/src/state/build_data/cache/records/format.h
  25. 24 0
      Sunrise/src/state/build_data/cache/records/validation.h
  26. 5 0
      Sunrise/src/state/build_data/cache/write/cache_file_writer.cpp
  27. 12 0
      Sunrise/src/state/build_data/cache/write/cache_payload_writer.cpp
  28. 5 0
      Sunrise/src/state/build_data/cache/write/temporary/temporary_cache_file.cpp
  29. 73 0
      Sunrise/src/state/build_data/collectibles/collectible_build_data_runtime.cpp
  30. 105 0
      Sunrise/src/state/build_data/collectibles/collectible_catalog.cpp
  31. 56 0
      Sunrise/src/state/build_data/collectibles/collectible_catalog.h
  32. 8 5
      Sunrise/src/state/build_data/items/details/definition.h
  33. 25 8
      Sunrise/src/state/build_data/items/details/item_detail_catalog.cpp
  34. 8 0
      Sunrise/src/state/build_data/items/item_build_data_runtime.cpp
  35. 4 0
      Sunrise/src/state/build_data/items/item_catalog.h
  36. 39 0
      Sunrise/src/state/build_data/items/socket_plugs/definition.h
  37. 79 0
      Sunrise/src/state/build_data/items/socket_plugs/socket_plug_build_data_runtime.cpp
  38. 129 0
      Sunrise/src/state/build_data/items/socket_plugs/socket_plug_catalog.cpp
  39. 55 0
      Sunrise/src/state/build_data/items/socket_plugs/socket_plug_catalog.h
  40. 40 0
      Sunrise/src/state/build_data/material_requirements/material_requirement_build_data_runtime.cpp
  41. 92 0
      Sunrise/src/state/build_data/material_requirements/material_requirement_catalog.cpp
  42. 45 0
      Sunrise/src/state/build_data/material_requirements/material_requirement_catalog.h
  43. 84 0
      Sunrise/src/state/build_data/runtime.h
  44. 6 0
      Sunrise/src/state/build_data/runtime/build_data_catalog_runtime.cpp
  45. 143 29
      Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp
  46. 22 16
      Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.h

+ 8 - 2
Sunrise/src/client/content/investment/investment_refresh.cpp

@@ -30,7 +30,10 @@ constexpr std::size_t kLineLimit = 96;
  */
 [[nodiscard]] bool ready() noexcept {
     return state::build_data::named_catalog_ready() && state::build_data::item_definitions_ready()
+           && state::build_data::collectible_definitions_ready()
+           && state::build_data::material_requirement_sets_ready()
            && state::build_data::configured_item_details_ready()
+           && state::build_data::socket_plug_rules_ready()
            && state::build_data::inventory_bucket_descriptors_ready()
            && state::build_data::socket_entry_lists_ready()
            && state::build_data::ability_buckets_ready()
@@ -77,7 +80,8 @@ bool refresh() noexcept {
         // The same lock as the extraction path. A cache write holds its own lock across file
         // calls, so a held thread stopped inside one would deadlock the freeze below.
         AcquireSRWLockExclusive(&g_refreshLock);
-        const bool persisted = state::build_data::persist();
+        const bool persisted =
+            state::ensure_profile_item_identities() && state::build_data::persist();
         // Nothing reads a package again until the next boot, so the open files and the held
         // tables go back now rather than at process exit.
         middleware::content::packages::reader::release_caches();
@@ -106,7 +110,9 @@ bool refresh() noexcept {
     }
     // The package pass owns the item table and must not wait on runtime content lookups.
     (void)items::packages::build();
-    const bool complete = ready() && state::build_data::persist();
+    const bool domainsReady = ready();
+    const bool complete =
+        domainsReady && state::ensure_profile_item_identities() && state::build_data::persist();
     process::freeze::release(held);
     // The overlay ends with the work, not with the slice, so it spans every retry the pass needs.
     if (complete) {

+ 8 - 3
Sunrise/src/client/content/items/details/configured_item_detail_extractor.cpp

@@ -1,10 +1,11 @@
 #include "configured_item_detail_extractor.h"
 
 #include <algorithm>
-#include <array>
 #include <bitset>
 #include <cstddef>
 #include <cstdint>
+#include <memory>
+#include <new>
 
 #include "../../../../state/build_data/items/item_catalog.h"
 #include "../../../../state/build_data/socket_entry_lists/definition.h"
@@ -94,7 +95,11 @@ bool extract(const investment::Source& source,
     }
 
     std::bitset<build_items::kDefinitionCapacity> seen{};
-    std::array<build_details::Definition, build_details::kDefinitionCapacity> staged{};
+    std::unique_ptr<build_details::Definition[]> staged{
+        new (std::nothrow) build_details::Definition[requestedDefinitionIndices.size()]};
+    if (!staged) {
+        return false;
+    }
     for (std::size_t position = 0; position < requestedDefinitionIndices.size(); ++position) {
         const std::uint16_t definitionIndex = requestedDefinitionIndices[position];
         if (static_cast<std::size_t>(definitionIndex) >= table.rowCount
@@ -112,7 +117,7 @@ bool extract(const investment::Source& source,
     if (!stable_count(source, table)) {
         return false;
     }
-    std::copy_n(staged.begin(), requestedDefinitionIndices.size(), output.begin());
+    std::copy_n(staged.get(), requestedDefinitionIndices.size(), output.begin());
     count = requestedDefinitionIndices.size();
     return true;
 }

+ 86 - 22
Sunrise/src/client/content/items/packages/internal.h

@@ -1,8 +1,10 @@
 #pragma once
 
 #include <array>
+#include <bitset>
 #include <cstddef>
 #include <cstdint>
+#include <memory>
 #include <span>
 #include <vector>
 
@@ -11,9 +13,11 @@
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
 #include "../../../../middleware/content/packages/tables/items.h"
 #include "../../../../state/build_data/abilities/definition.h"
+#include "../../../../state/build_data/collectibles/collectible_catalog.h"
 #include "../../../../state/build_data/constants/definition.h"
 #include "../../../../state/build_data/items/details/definition.h"
 #include "../../../../state/build_data/items/item_catalog.h"
+#include "../../../../state/build_data/material_requirements/material_requirement_catalog.h"
 #include "../../../../state/build_data/progressions/definition.h"
 
 namespace sunrise::client::content::items::packages {
@@ -21,10 +25,13 @@ namespace sunrise::client::content::items::packages {
 namespace reader = middleware::content::packages::reader;
 namespace tables = middleware::content::packages::tables;
 
-/** Configured equipment rows plus every plug they socket. */
+/** Equippable rows, their initial plugs, and authored override plugs. */
 inline constexpr std::size_t kDetailCapacity =
     state::build_data::items::details::kDefinitionCapacity;
 
+/** Native item indices selected for the deduplicated detail closure. */
+using DetailRequests = std::bitset<state::build_data::items::kDefinitionCapacity>;
+
 /** Authored definition hashes one pass looks for while walking the item index table. */
 struct AuthoredHashes {
     std::array<std::uint32_t, kDetailCapacity> values{};
@@ -51,8 +58,13 @@ struct Storage {
     std::vector<std::byte> child{};
     std::vector<std::byte> root{};
     std::vector<std::byte> definition{};
-    std::array<std::uint16_t, kDetailCapacity> requested{};
-    std::array<state::build_data::items::details::Definition, kDetailCapacity> details{};
+    /** Shared reusable/randomized plug-set table read from investment-root slot 51. */
+    std::vector<std::byte> plugSetTable{};
+    /** Compact 0..3 special plug-category code of every dense installed item row. */
+    std::array<std::uint8_t, state::build_data::items::kDefinitionCapacity> specialPlugCategories{};
+    DetailRequests detailRequests{};
+    std::array<std::uint16_t, kDetailCapacity> requestedDetailIndices{};
+    std::unique_ptr<state::build_data::items::details::Definition[]> details{};
     AuthoredHashes authoredHashes{};
     std::vector<std::byte> abilityTable{};
     std::vector<std::byte> abilityPool{};
@@ -62,6 +74,12 @@ struct Storage {
     std::array<state::build_data::progressions::Definition,
                state::build_data::progressions::kDefinitionCapacity>
         progressionRows{};
+    std::array<state::build_data::collectibles::Definition,
+               state::build_data::collectibles::kDefinitionCapacity>
+        collectibleRows{};
+    std::array<state::build_data::material_requirements::Definition,
+               state::build_data::material_requirements::kDefinitionCapacity>
+        materialRequirementRows{};
     std::array<state::build_data::items::Definition, state::build_data::items::kDefinitionCapacity>
         rows{};
 };
@@ -79,30 +97,46 @@ struct Storage {
 [[nodiscard]] bool authored(const AuthoredHashes& hashes, std::uint32_t hash) noexcept;
 
 /**
- * Adds one definition index to the requested set.
- * @param definitionIndex Native item index.
+ * @param row Installed item row.
+ * @return True when its bucket maps to an equipment slot
+ * supported by the generated loadout.
+ */
+[[nodiscard]] bool equippable(const tables::items::Row& row) noexcept;
+
+/**
+ * Adds one definition index to the deduplicated requested set.
+ * @param definitionIndex Native
+ * item index.
  * @param requested Requested-set storage.
- * @param count Used entries, advanced on success.
- * @return True when the index fits.
  */
-[[nodiscard]] bool request(std::uint16_t definitionIndex,
-                           std::span<std::uint16_t> requested,
-                           std::size_t& count) noexcept;
+void request(std::uint16_t definitionIndex, DetailRequests& requested) noexcept;
 
 /**
  * Adds every socket lane's initial plug to the requested set.
- * A lane the authored loadout leaves unset falls back to this plug, so its detail must exist.
- * @param row Item row already read from its definition blob.
+ * A lane using native defaults falls back to this plug, so its detail must exist.
+ * @param row
+ * Item row already read from its definition blob.
+ * @param itemDefinitionCount Installed
+ * item-table bound.
  * @param requested Requested-set storage.
- * @param count Used entries, advanced per added lane.
- * @return True when every lane fits.
  */
-[[nodiscard]] bool append_initial_plugs(const tables::items::Row& row,
-                                        std::span<std::uint16_t> requested,
-                                        std::size_t& count) noexcept;
+void append_initial_plugs(const tables::items::Row& row,
+                          std::uint64_t itemDefinitionCount,
+                          DetailRequests& requested) noexcept;
 
-/** @param requested Requested-set storage. @param count Sorted and deduplicated in place. */
-void compact_requested(std::span<std::uint16_t> requested, std::size_t& count) noexcept;
+/**
+ * Materializes requested native indices in ascending order.
+ * @param requested Deduplicated
+ * native-index set.
+ * @param output Fixed detail-index storage.
+ * @param count Receives the
+ * number of selected rows, or zero when output is too small.
+ * @return True when every selected
+ * row fits.
+ */
+[[nodiscard]] bool materialize_requests(const DetailRequests& requested,
+                                        std::span<std::uint16_t> output,
+                                        std::size_t& count) noexcept;
 
 /**
  * Reads one requested definition and turns it into its cached detail form.
@@ -113,7 +147,8 @@ void compact_requested(std::span<std::uint16_t> requested, std::size_t& count) n
  */
 [[nodiscard]] bool build_detail(const DetailSource& source,
                                 std::uint16_t definitionIndex,
-                                state::build_data::items::details::Definition& detail) noexcept;
+                                state::build_data::items::details::Definition& detail,
+                                tables::items::Row& item) noexcept;
 
 /**
  * Reads the stat rows the installed investment constants blob names.
@@ -217,8 +252,14 @@ void report_detail_failure(std::size_t slot, std::uint16_t definitionIndex) noex
 /** @param count Ability bucket rows the pass built, one per subclass and ability selection. */
 void report_ability_count(std::size_t count) noexcept;
 
-/** @param count Detail rows the pass built, covering equipped items and every plug they socket. */
-void report_detail_count(std::size_t count) noexcept;
+/** Reports requested, retained, and skipped detail closure rows. */
+void report_detail_count(std::size_t requested, std::size_t built) noexcept;
+
+/** 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;
 
 /** Reports the pass outcome once. @param published Rows published, or zero on failure. */
 void report(std::size_t published, const char* reason) noexcept;
@@ -245,6 +286,23 @@ void report(std::size_t published, const char* reason) noexcept;
                                             Storage& storage,
                                             std::span<const std::byte> root) noexcept;
 
+/**
+ * Reads and publishes the root's dense collectible-to-item mapping table.
+ * @param source
+ * Package source.
+ * @param storage Pass storage, including scratch bytes and bounded row storage.
+
+ * * @param root Investment root bytes.
+ * @param itemDefinitionCount Number of rows in the
+ * installed item index table.
+ * @return True when every tag, class, bound, and item link validates
+ * and publishes.
+ */
+[[nodiscard]] bool build_collectibles(const reader::Source& source,
+                                      Storage& storage,
+                                      std::span<const std::byte> root,
+                                      std::uint64_t itemDefinitionCount) noexcept;
+
 /**
  * Walks the located item index table, then publishes every domain that depends on it.
  * @param source Package source.
@@ -260,4 +318,10 @@ void report(std::size_t published, const char* reason) noexcept;
                                    std::size_t& rowCount,
                                    const char*& reason) noexcept;
 
+/** Reads and publishes every native material-requirement set from investment-root slot 96. */
+[[nodiscard]] bool build_material_requirements(const reader::Source& source,
+                                               Storage& storage,
+                                               std::span<const std::byte> root,
+                                               std::uint64_t itemDefinitionCount) noexcept;
+
 } // namespace sunrise::client::content::items::packages

+ 31 - 5
Sunrise/src/client/content/items/packages/package_build_report.cpp

@@ -39,11 +39,16 @@ void report_ability_count(std::size_t count) noexcept {
     }
 }
 
-/** @param count Detail rows the pass built, covering equipped items and every plug they socket. */
-void report_detail_count(std::size_t count) noexcept {
-    std::array<char, 96> line{};
-    const int written =
-        std::snprintf(line.data(), line.size(), "ev=pkg stage=details result=ok rows=%zu", count);
+/** Reports requested, retained, and skipped rows for the equippable-item detail closure. */
+void report_detail_count(std::size_t requested, std::size_t built) noexcept {
+    std::array<char, 128> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=pkg stage=details result=ok requested=%zu rows=%zu "
+                                      "skipped=%zu",
+                                      requested,
+                                      built,
+                                      requested - built);
     if (written > 0) {
         core::log::write(core::log::Channel::client,
                          core::log::Level::info,
@@ -51,6 +56,27 @@ void report_detail_count(std::size_t count) noexcept {
     }
 }
 
+/** Reports the bounded exact ordinary-socket relation extracted from the installed packages. */
+void report_socket_plug_count(std::size_t rules,
+                              std::size_t pools,
+                              std::size_t members,
+                              std::size_t skipped) noexcept {
+    std::array<char, 160> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=pkg stage=socket_plugs result=ok rules=%zu pools=%zu "
+                                      "members=%zu skipped=%zu",
+                                      rules,
+                                      pools,
+                                      members,
+                                      skipped);
+    if (written > 0) {
+        core::log::write(core::log::Channel::client,
+                         skipped == 0 ? core::log::Level::info : core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(written)});
+    }
+}
+
 /** Reports the pass outcome once. @param published Rows published, or zero on failure. */
 void report(std::size_t published, const char* reason) noexcept {
     if (g_reported.exchange(true, std::memory_order_relaxed)) {

+ 176 - 0
Sunrise/src/client/content/items/packages/package_collectible_build.cpp

@@ -0,0 +1,176 @@
+#include <cstring>
+#include <limits>
+
+#include "../../../../state/build_data/runtime.h"
+#include "internal.h"
+
+namespace sunrise::client::content::items::packages {
+
+/** Reads the fixed collectible table and publishes its native-index item links. */
+bool build_collectibles(const reader::Source& source,
+                        Storage& storage,
+                        std::span<const std::byte> root,
+                        std::uint64_t itemDefinitionCount) noexcept {
+    namespace domain = state::build_data::collectibles;
+    if (state::build_data::collectible_definitions_ready()) {
+        return true;
+    }
+    if (itemDefinitionCount == 0
+        || itemDefinitionCount > state::build_data::items::kDefinitionCapacity) {
+        return false;
+    }
+
+    std::uint32_t requirementTableTag = 0;
+    std::uint32_t requirementTableClass = 0;
+    tables::Array requirementSets{};
+    if (!tables::slot_tag(root, tables::kMaterialRequirementTableSlot, requirementTableTag)
+        || requirementTableTag == 0
+        || tables::package_of(requirementTableTag) == tables::kAbsentPackageId
+        || !reader::read_tag(
+            source, storage.scratch, requirementTableTag, storage.definition, requirementTableClass)
+        || requirementTableClass != tables::kMaterialRequirementTableClass
+        || !tables::find_array_at(std::span<const std::byte>{storage.definition},
+                                  tables::kTableArrayDescriptor,
+                                  requirementSets)
+        || requirementSets.elementClass != tables::kMaterialRequirementSetRowClass
+        || requirementSets.count == 0
+        || requirementSets.count > (std::numeric_limits<std::uint16_t>::max)()) {
+        return false;
+    }
+    const std::span<const std::byte> requirementTable{storage.definition};
+    if (requirementSets.dataOffset > requirementTable.size()
+        || requirementSets.count > (requirementTable.size() - requirementSets.dataOffset)
+                                       / tables::kMaterialRequirementSetRowStride) {
+        return false;
+    }
+
+    std::uint32_t tableTag = 0;
+    std::uint32_t tableClass = 0;
+    tables::Array rows{};
+    if (!tables::slot_tag(root, tables::kCollectibleTableSlot, tableTag) || tableTag == 0
+        || tables::package_of(tableTag) == tables::kAbsentPackageId
+        || !reader::read_tag(source, storage.scratch, tableTag, storage.child, tableClass)
+        || tableClass != tables::kCollectibleTableClass
+        || !tables::find_array_at(
+            std::span<const std::byte>{storage.child}, tables::kTableArrayDescriptor, rows)
+        || rows.elementClass != tables::kCollectibleRowClass || rows.count == 0
+        || rows.count > storage.collectibleRows.size()) {
+        return false;
+    }
+
+    const std::span<const std::byte> table{storage.child};
+    if (rows.dataOffset > table.size()
+        || rows.count > (table.size() - rows.dataOffset) / tables::kCollectibleRowStride) {
+        return false;
+    }
+    for (std::uint64_t row = 0; row < rows.count; ++row) {
+        const std::size_t at =
+            rows.dataOffset + static_cast<std::size_t>(row) * tables::kCollectibleRowStride;
+        std::uint32_t collectibleHash = 0;
+        std::uint16_t itemDefinitionIndex = domain::kUnavailableItemDefinitionIndex;
+        std::uint16_t requirementSetIndex = domain::kUnavailableMaterialRequirementSetIndex;
+        std::memcpy(&collectibleHash,
+                    table.data() + at + tables::kCollectibleHashOffset,
+                    sizeof collectibleHash);
+        std::memcpy(&itemDefinitionIndex,
+                    table.data() + at + tables::kCollectibleItemIndexOffset,
+                    sizeof itemDefinitionIndex);
+        std::memcpy(&requirementSetIndex,
+                    table.data() + at + tables::kCollectibleMaterialRequirementIndexOffset,
+                    sizeof requirementSetIndex);
+        if (itemDefinitionIndex != domain::kUnavailableItemDefinitionIndex
+            && itemDefinitionIndex >= itemDefinitionCount) {
+            return false;
+        }
+        domain::Definition& output = storage.collectibleRows[static_cast<std::size_t>(row)];
+        output = {};
+        output.collectibleHash = collectibleHash;
+        output.collectibleIndex = static_cast<std::uint16_t>(row);
+        output.itemDefinitionIndex = itemDefinitionIndex;
+        if (requirementSetIndex == domain::kUnavailableMaterialRequirementSetIndex) {
+            continue;
+        }
+        if (requirementSetIndex >= requirementSets.count) {
+            return false;
+        }
+        const std::size_t setAt = requirementSets.dataOffset
+                                  + static_cast<std::size_t>(requirementSetIndex)
+                                        * tables::kMaterialRequirementSetRowStride;
+        std::uint32_t requirementSetHash = 0;
+        std::int64_t descriptorRelative = 0;
+        std::memcpy(&requirementSetHash,
+                    requirementTable.data() + setAt + tables::kMaterialRequirementSetHashOffset,
+                    sizeof requirementSetHash);
+        std::memcpy(&descriptorRelative,
+                    requirementTable.data() + setAt
+                        + tables::kMaterialRequirementSetArrayPointerOffset,
+                    sizeof descriptorRelative);
+        const std::size_t pointerAt = setAt + tables::kMaterialRequirementSetArrayPointerOffset;
+        if (requirementSetHash == 0 || descriptorRelative < -static_cast<std::int64_t>(pointerAt)
+            || descriptorRelative
+                   > static_cast<std::int64_t>(requirementTable.size() - pointerAt)) {
+            return false;
+        }
+        const auto descriptorSigned = static_cast<std::int64_t>(pointerAt) + descriptorRelative;
+        if (descriptorSigned < 0) {
+            return false;
+        }
+        tables::Array requirements{};
+        if (!tables::find_array_at(
+                requirementTable, static_cast<std::size_t>(descriptorSigned), requirements)
+            || requirements.elementClass != tables::kMaterialRequirementRowClass
+            || requirements.count == 0 || requirements.count > output.materialRequirements.size()
+            || requirements.dataOffset > requirementTable.size()
+            || requirements.count > (requirementTable.size() - requirements.dataOffset)
+                                        / tables::kMaterialRequirementRowStride) {
+            return false;
+        }
+        output.materialRequirementSetHash = requirementSetHash;
+        output.materialRequirementSetIndex = requirementSetIndex;
+        output.materialRequirementCount = static_cast<std::uint8_t>(requirements.count);
+        for (std::size_t requirement = 0; requirement < requirements.count; ++requirement) {
+            const std::size_t requirementAt =
+                requirements.dataOffset + requirement * tables::kMaterialRequirementRowStride;
+            std::uint32_t nativeItemIndex = 0;
+            std::uint32_t quantity = 0;
+            std::uint8_t deleteOnAction = 0;
+            std::uint8_t omitFromRequirements = 0;
+            std::uint16_t sentinel = 0;
+            std::memcpy(&nativeItemIndex,
+                        requirementTable.data() + requirementAt
+                            + tables::kMaterialRequirementItemIndexOffset,
+                        sizeof nativeItemIndex);
+            std::memcpy(&quantity,
+                        requirementTable.data() + requirementAt
+                            + tables::kMaterialRequirementQuantityOffset,
+                        sizeof quantity);
+            std::memcpy(&deleteOnAction,
+                        requirementTable.data() + requirementAt
+                            + tables::kMaterialRequirementDeleteOffset,
+                        sizeof deleteOnAction);
+            std::memcpy(&omitFromRequirements,
+                        requirementTable.data() + requirementAt
+                            + tables::kMaterialRequirementOmitOffset,
+                        sizeof omitFromRequirements);
+            std::memcpy(&sentinel,
+                        requirementTable.data() + requirementAt
+                            + tables::kMaterialRequirementSentinelOffset,
+                        sizeof sentinel);
+            if (nativeItemIndex >= itemDefinitionCount
+                || nativeItemIndex > (std::numeric_limits<std::uint16_t>::max)() || quantity == 0
+                || deleteOnAction > 1 || omitFromRequirements > 1 || sentinel != 0xFFFFU) {
+                return false;
+            }
+            output.materialRequirements[requirement] = {
+                quantity,
+                static_cast<std::uint16_t>(nativeItemIndex),
+                deleteOnAction != 0,
+                omitFromRequirements != 0,
+            };
+        }
+    }
+    return state::build_data::publish_collectible_definitions(
+        std::span(storage.collectibleRows).first(static_cast<std::size_t>(rows.count)));
+}
+
+} // namespace sunrise::client::content::items::packages

+ 68 - 31
Sunrise/src/client/content/items/packages/package_detail_build.cpp

@@ -13,7 +13,13 @@ namespace {
 
 namespace domain = state::build_data::items::details;
 
-/** Equipment slot for each equippable inventory bucket. */
+/**
+ * Fallback equipment slot for old definitions whose optional equipment block is absent.
+ * The
+ * installed definition's own slot wins when present; unlike a bucket, it can distinguish
+ *
+ * multiple native equipment positions that share one inventory bucket.
+ */
 constexpr std::array<std::pair<std::uint8_t, std::int8_t>, 16> kEquipmentSlotOfBucket{{
     {16, 0},
     {3, 1},
@@ -52,7 +58,8 @@ constexpr std::array<std::pair<std::uint8_t, std::int8_t>, 16> kEquipmentSlotOfB
     detail.maxStackSize = row.maxStackSize;
     detail.instancedDefinitionState = row.instanced ? domain::InstancedDefinitionState::instanced
                                                     : domain::InstancedDefinitionState::stackable;
-    detail.equipmentSlot = equipment_slot(row.bucketId);
+    detail.equipmentSlot =
+        row.equipmentSlot.has_value() ? row.equipmentSlot : equipment_slot(row.bucketId);
     detail.ordinarySocketState =
         row.hasSockets ? domain::OrdinarySocketState::present : domain::OrdinarySocketState::absent;
     detail.ordinarySocketCount = row.socketCount;
@@ -108,16 +115,36 @@ bool collect_authored_hashes(AuthoredHashes& output) noexcept {
     if (!state::account::valid(account)) {
         return false;
     }
+    const auto append = [&output](const state::account::inventory::Item& item) noexcept {
+        if (output.count >= output.values.size()) {
+            return false;
+        }
+        output.values[output.count++] = item.definitionHash;
+        for (std::size_t lane = 0; lane < item.sockets.plugCount; ++lane) {
+            if (!item.sockets.plugs[lane].has_value()) {
+                continue;
+            }
+            if (output.count >= output.values.size()) {
+                return false;
+            }
+            output.values[output.count++] = *item.sockets.plugs[lane];
+        }
+        return true;
+    };
     for (std::size_t character = 0; character < account.characterCount; ++character) {
         for (const auto& item : account.characters[character].equipment.slots) {
-            if (!item.has_value() || output.count >= output.values.size()) {
+            if (!item.has_value()) {
                 continue;
             }
-            output.values[output.count++] = item->definitionHash;
-            for (std::size_t lane = 0; lane < item->sockets.plugCount; ++lane) {
-                if (item->sockets.plugs[lane].has_value() && output.count < output.values.size()) {
-                    output.values[output.count++] = *item->sockets.plugs[lane];
-                }
+            if (!append(*item)) {
+                return false;
+            }
+        }
+        const state::account::inventory::CharacterItems& inventory =
+            account.characters[character].inventory;
+        for (std::size_t item = 0; item < inventory.count; ++item) {
+            if (!append(inventory.values[item])) {
+                return false;
             }
         }
     }
@@ -135,46 +162,56 @@ bool authored(const AuthoredHashes& hashes, std::uint32_t hash) noexcept {
     return std::binary_search(begin, end, hash);
 }
 
-/** Adds one definition index to the requested set. */
-bool request(std::uint16_t definitionIndex,
-             std::span<std::uint16_t> requested,
-             std::size_t& count) noexcept {
-    if (count >= requested.size()) {
-        return false;
+/** @return True when the row's bucket maps to a supported equipment slot. */
+bool equippable(const tables::items::Row& row) noexcept {
+    return equipment_slot(row.bucketId).has_value();
+}
+
+/** Adds one definition index to the deduplicated requested set. */
+void request(std::uint16_t definitionIndex, DetailRequests& requested) noexcept {
+    if (static_cast<std::size_t>(definitionIndex) < requested.size()) {
+        requested.set(definitionIndex);
     }
-    requested[count++] = definitionIndex;
-    return true;
 }
 
 /** Adds every socket lane's initial plug to the requested set. */
-bool append_initial_plugs(const tables::items::Row& row,
-                          std::span<std::uint16_t> requested,
-                          std::size_t& count) noexcept {
+void append_initial_plugs(const tables::items::Row& row,
+                          std::uint64_t itemDefinitionCount,
+                          DetailRequests& requested) noexcept {
     for (std::size_t lane = 0; lane < row.socketCount; ++lane) {
-        if (row.initialPlugs[lane] == tables::items::kUnavailablePlug) {
+        if (row.initialPlugs[lane] == tables::items::kUnavailablePlug
+            || row.initialPlugs[lane] >= itemDefinitionCount) {
             continue;
         }
-        if (!request(row.initialPlugs[lane], requested, count)) {
+        request(row.initialPlugs[lane], requested);
+    }
+}
+
+/** Materializes requested native indices in ascending order. */
+bool materialize_requests(const DetailRequests& requested,
+                          std::span<std::uint16_t> output,
+                          std::size_t& count) noexcept {
+    count = 0;
+    for (std::size_t index = 0; index < requested.size(); ++index) {
+        if (!requested.test(index)) {
+            continue;
+        }
+        if (count >= output.size()) {
+            count = 0;
             return false;
         }
+        output[count++] = static_cast<std::uint16_t>(index);
     }
     return true;
 }
 
-/** @param requested Requested-set storage. @param count Sorted and deduplicated in place. */
-void compact_requested(std::span<std::uint16_t> requested, std::size_t& count) noexcept {
-    auto end = requested.begin() + static_cast<std::ptrdiff_t>(count);
-    std::sort(requested.begin(), end);
-    end = std::unique(requested.begin(), end);
-    count = static_cast<std::size_t>(end - requested.begin());
-}
-
 /** Reads one requested definition and turns it into its cached detail form. */
 bool build_detail(const DetailSource& source,
                   std::uint16_t definitionIndex,
-                  domain::Definition& detail) noexcept {
+                  domain::Definition& detail,
+                  tables::items::Row& item) noexcept {
     tables::IndexRow indexRow{};
-    tables::items::Row item{};
+    item = {};
     item.definitionIndex = definitionIndex;
     if (!tables::index_row(source.table, source.array, definitionIndex, indexRow)
         || !reader::read_tag(

+ 35 - 4
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -30,7 +30,10 @@ namespace {
 /** @return True when every domain owned by the package pass is published. */
 [[nodiscard]] bool package_domains_ready() noexcept {
     return state::build_data::item_definitions_ready()
+           && state::build_data::collectible_definitions_ready()
+           && state::build_data::material_requirement_sets_ready()
            && state::build_data::configured_item_details_ready()
+           && state::build_data::socket_plug_rules_ready()
            && state::build_data::inventory_bucket_descriptors_ready()
            && state::build_data::socket_entry_lists_ready()
            && state::build_data::ability_buckets_ready()
@@ -43,7 +46,10 @@ namespace {
 /** @return True when every item and investment-root domain is published. */
 [[nodiscard]] bool root_domains_ready() noexcept {
     return state::build_data::item_definitions_ready()
+           && state::build_data::collectible_definitions_ready()
+           && state::build_data::material_requirement_sets_ready()
            && state::build_data::configured_item_details_ready()
+           && state::build_data::socket_plug_rules_ready()
            && state::build_data::inventory_bucket_descriptors_ready()
            && state::build_data::socket_entry_lists_ready()
            && state::build_data::ability_buckets_ready()
@@ -101,17 +107,34 @@ bool build() noexcept {
             // Fixed navigation: globals child zero is the investment root, whose slot holds the
             // item table, whose array descriptor sits at a fixed offset.
             std::uint32_t rootTag = 0;
+            std::uint32_t rootClass = 0;
             std::uint32_t tableTag = 0;
             reason = "root";
             if (!tables::child_tag(std::span<const std::byte>{storage.container},
                                    tables::kInvestmentRootChild,
                                    rootTag)
-                || rootTag == 0
-                || !reader::read_tag(source, storage.scratch, rootTag, storage.child)) {
+                || rootTag == 0 || tables::package_of(rootTag) == tables::kAbsentPackageId
+                || !reader::read_tag(source, storage.scratch, rootTag, storage.child, rootClass)
+                || rootClass != tables::kInvestmentRootClass) {
                 continue;
             }
             // The same root names the bucket and socket-list tables.
             storage.root = storage.child;
+            if (!state::build_data::socket_plug_rules_ready()) {
+                std::uint32_t plugSetTag = 0;
+                tables::Array plugSets{};
+                reason = "plug_sets";
+                if (!tables::slot_tag(std::span<const std::byte>{storage.root},
+                                      tables::kPlugSetTableSlot,
+                                      plugSetTag)
+                    || plugSetTag == 0
+                    || !reader::read_tag(source, storage.scratch, plugSetTag, storage.plugSetTable)
+                    || !tables::find_array_at(std::span<const std::byte>{storage.plugSetTable},
+                                              tables::kTableArrayDescriptor,
+                                              plugSets)) {
+                    continue;
+                }
+            }
             (void)build_buckets(source, storage, std::span<const std::byte>{storage.root});
             (void)build_socket_entry_lists(
                 source, storage, std::span<const std::byte>{storage.root});
@@ -150,8 +173,16 @@ bool build() noexcept {
                                             table)
                       && table.elementClass == tables::kItemIndexTableClass;
         }
-        if (located) {
-            (void)build_item_rows(source, storage, table, rowCount, reason);
+        if (located && build_item_rows(source, storage, table, rowCount, reason)) {
+            if (!build_material_requirements(
+                    source, storage, std::span<const std::byte>{storage.root}, table.count)) {
+                reason = "materials";
+            } else if (!build_collectibles(source,
+                                           storage,
+                                           std::span<const std::byte>{storage.root},
+                                           table.count)) {
+                reason = "collectibles";
+            }
         }
     }
     SecureZeroMemory(&keys, sizeof keys);

+ 109 - 23
Sunrise/src/client/content/items/packages/package_item_rows.cpp

@@ -1,11 +1,39 @@
 #include <array>
+#include <new>
 #include <span>
 #include <vector>
 
+#include "../../../../state/build_data/items/details/item_detail_catalog.h"
 #include "../../../../state/build_data/runtime.h"
 #include "internal.h"
+#include "package_socket_plug_build.h"
 
 namespace sunrise::client::content::items::packages {
+namespace {
+
+namespace build_details = state::build_data::items::details;
+namespace build_items = state::build_data::items;
+
+/** @return True when one extracted detail can join the currently published numeric domains. */
+[[nodiscard]] bool publishable_detail(const build_details::Definition& detail) noexcept {
+    const std::size_t itemCount = state::build_data::item_definition_count();
+    const std::size_t socketListCount = state::build_data::socket_entry_list_count();
+    build_items::Definition item{};
+    if (!build_details::valid(std::span<const build_details::Definition>{&detail, 1})
+        || detail.definitionIndex >= itemCount || detail.socketEntryListIndex >= socketListCount
+        || !build_items::find_index(detail.definitionIndex, item)
+        || item.bucketId != detail.bucketId) {
+        return false;
+    }
+    for (const std::uint16_t plugIndex : detail.initialPlugIndices) {
+        if (plugIndex != build_details::kUnavailableItemIndex && plugIndex >= itemCount) {
+            return false;
+        }
+    }
+    return true;
+}
+
+} // namespace
 
 /** Walks the located item index table, then publishes every domain that depends on it. */
 bool build_item_rows(const reader::Source& source,
@@ -15,13 +43,22 @@ bool build_item_rows(const reader::Source& source,
                      const char*& reason) noexcept {
     const bool needDefinitions = !state::build_data::item_definitions_ready();
     const bool needDetails = !state::build_data::configured_item_details_ready();
-    const bool needRows = needDefinitions || needDetails;
+    const bool needSocketPlugs = !state::build_data::socket_plug_rules_ready();
+    const bool needDetailRows = needDetails || needSocketPlugs;
+    const bool needRows = needDefinitions || needDetailRows;
     bool published = !needRows;
+    if (needDetails && !storage.details) {
+        storage.details.reset(new (std::nothrow) build_details::Definition[kDetailCapacity]);
+    }
+    const bool detailStorageReady = !needDetails || static_cast<bool>(storage.details);
     const std::span<const std::byte> container{storage.child};
     reason = "rows";
-    // The requested detail set is gathered during this one walk. An authored row is matched by
-    // hash, because the index table that maps a hash to its index is what this loop is building.
-    const bool haveAuthored = needRows && collect_authored_hashes(storage.authoredHashes);
+    // The detail closure is gathered during this one walk. Collections can name any installed
+    // item row, including profile-owned shaders and modifications, so retain every readable row
+    // rather than only startup-authored/equippable definitions. The fixed request bitset still
+    // bounds this to the installed 16-bit item-table domain.
+    storage.detailRequests.reset();
+    storage.specialPlugCategories.fill(0);
     std::size_t detailCount = 0;
     for (std::uint64_t index = 0; needRows && index < table.count && rowCount < storage.rows.size();
          ++index) {
@@ -37,40 +74,88 @@ bool build_item_rows(const reader::Source& source,
                                                item)) {
             continue;
         }
-        storage.rows[rowCount++] = state::build_data::items::Definition{
-            item.definitionHash, item.definitionIndex, item.bucketId};
-        if (haveAuthored && authored(storage.authoredHashes, item.definitionHash)) {
-            (void)request(item.definitionIndex, storage.requested, detailCount);
-            (void)append_initial_plugs(item, storage.requested, detailCount);
+        storage.rows[rowCount++] =
+            state::build_data::items::Definition{item.definitionHash,
+                                                 item.definitionIndex,
+                                                 item.bucketId,
+                                                 item.insertionMaterialRequirementSetIndex,
+                                                 item.enabledMaterialRequirementSetIndex};
+        if (needSocketPlugs) {
+            storage.specialPlugCategories[item.definitionIndex] =
+                special_plug_category(item.plugCategoryHash);
+        }
+        if (needDetailRows) {
+            request(item.definitionIndex, storage.detailRequests);
+            append_initial_plugs(item, table.count, storage.detailRequests);
         }
     }
+    bool requestsFit = true;
     if (needRows) {
-        compact_requested(storage.requested, detailCount);
-        published = rowCount != 0;
+        requestsFit = !needDetailRows
+                      || materialize_requests(
+                          storage.detailRequests, storage.requestedDetailIndices, detailCount);
+        published = rowCount != 0 && requestsFit && detailStorageReady;
     }
     if (published && needDefinitions) {
         published =
             state::build_data::publish_item_definitions(std::span(storage.rows).first(rowCount));
     }
     if (!published) {
-        reason = "publish";
+        reason = !detailStorageReady ? "detail_storage"
+                 : !requestsFit      ? "detail_capacity"
+                                     : "publish";
+    }
+    SocketPlugBuild socketPlugBuild;
+    const bool socketStorageReady =
+        !needSocketPlugs
+        || socketPlugBuild.prepare(storage.specialPlugCategories,
+                                   std::span(storage.rows).first(rowCount));
+    if (published && !socketStorageReady) {
+        published = false;
+        reason = "socket_storage";
     }
-    // Configured rows and every plug they socket each need a detail record, and they are found
-    // through the table this pass just published.
-    if (published && needDetails) {
+    // Every readable installed row is found through the table this pass just published. One
+    // malformed row is omitted independently so unrelated Collections categories stay usable.
+    if (published && needDetailRows) {
         reason = "details";
         const DetailSource detailSource{
             &source, &storage.scratch, container, table, &storage.definition};
-        for (std::size_t slot = 0; published && slot < detailCount; ++slot) {
-            published = build_detail(detailSource, storage.requested[slot], storage.details[slot]);
-            if (!published) {
-                report_detail_failure(slot, storage.requested[slot]);
+        std::size_t builtDetailCount = 0;
+        for (std::size_t slot = 0; slot < detailCount; ++slot) {
+            build_details::Definition detail{};
+            tables::items::Row item{};
+            if (!build_detail(detailSource, storage.requestedDetailIndices[slot], detail, item)
+                || !publishable_detail(detail)) {
+                report_detail_failure(slot, storage.requestedDetailIndices[slot]);
+                continue;
+            }
+            if (needDetails) {
+                storage.details[builtDetailCount++] = detail;
+            }
+            if (needSocketPlugs) {
+                (void)socketPlugBuild.append(item,
+                                             std::span<const std::byte>{storage.definition},
+                                             std::span<const std::byte>{storage.plugSetTable},
+                                             table.count);
+            }
+        }
+        if (needDetails) {
+            published = state::build_data::publish_configured_item_details(
+                std::span<build_details::Definition>{storage.details.get(), kDetailCapacity}.first(
+                    builtDetailCount));
+            report_detail_count(detailCount, builtDetailCount);
+        }
+        if (published && needSocketPlugs) {
+            const std::size_t rules = socketPlugBuild.rule_count();
+            const std::size_t pools = socketPlugBuild.pool_count();
+            const std::size_t members = socketPlugBuild.member_count();
+            const std::size_t skipped = socketPlugBuild.skipped();
+            reason = "socket_plugs";
+            published = socketPlugBuild.publish();
+            if (published) {
+                report_socket_plug_count(rules, pools, members, skipped);
             }
         }
-        published = published
-                    && state::build_data::publish_configured_item_details(
-                        std::span(storage.details).first(detailCount));
-        report_detail_count(detailCount);
     }
     // Ability buckets read the socket entry list table again and depend on the detail domain, so
     // they run last.
@@ -94,6 +179,7 @@ bool build_item_rows(const reader::Source& source,
     }
     return published && state::build_data::item_definitions_ready()
            && state::build_data::configured_item_details_ready()
+           && state::build_data::socket_plug_rules_ready()
            && state::build_data::ability_buckets_ready();
 }
 

+ 183 - 0
Sunrise/src/client/content/items/packages/package_material_requirement_build.cpp

@@ -0,0 +1,183 @@
+#include <algorithm>
+#include <array>
+#include <cstdio>
+#include <cstring>
+#include <limits>
+
+#include "../../../../core/logging/log.h"
+#include "../../../../state/build_data/material_requirements/material_requirement_catalog.h"
+#include "../../../../state/build_data/runtime.h"
+#include "internal.h"
+
+namespace sunrise::client::content::items::packages {
+namespace {
+
+void report_material_requirements(std::size_t sets,
+                                  std::size_t conditionalRows,
+                                  bool valid,
+                                  bool published) noexcept {
+    std::array<char, 160> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=pkg stage=materials result=%s sets=%zu conditional=%zu "
+                                      "valid=%u",
+                                      published ? "ok" : "fail",
+                                      sets,
+                                      conditionalRows,
+                                      static_cast<unsigned>(valid));
+    if (written > 0) {
+        core::log::write(core::log::Channel::client,
+                         published ? core::log::Level::info : core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(written)});
+    }
+}
+
+void report_material_requirement_failure(const char* reason,
+                                         std::size_t set,
+                                         std::size_t row) noexcept {
+    std::array<char, 144> line{};
+    const int written =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=pkg stage=materials result=fail reason=%s set=%zu row=%zu",
+                      reason,
+                      set,
+                      row);
+    if (written > 0) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(written)});
+    }
+}
+
+} // namespace
+
+bool build_material_requirements(const reader::Source& source,
+                                 Storage& storage,
+                                 std::span<const std::byte> root,
+                                 std::uint64_t itemDefinitionCount) noexcept {
+    namespace domain = state::build_data::material_requirements;
+    if (state::build_data::material_requirement_sets_ready()) {
+        return true;
+    }
+    if (itemDefinitionCount == 0
+        || itemDefinitionCount > state::build_data::items::kDefinitionCapacity) {
+        report_material_requirement_failure("item_count", 0, 0);
+        return false;
+    }
+    std::uint32_t tableTag = 0;
+    std::uint32_t tableClass = 0;
+    tables::Array sets{};
+    if (!tables::slot_tag(root, tables::kMaterialRequirementTableSlot, tableTag) || tableTag == 0
+        || tables::package_of(tableTag) == tables::kAbsentPackageId
+        || !reader::read_tag(source, storage.scratch, tableTag, storage.definition, tableClass)
+        || tableClass != tables::kMaterialRequirementTableClass
+        || !tables::find_array_at(
+            std::span<const std::byte>{storage.definition}, tables::kTableArrayDescriptor, sets)
+        || sets.elementClass != tables::kMaterialRequirementSetRowClass || sets.count == 0
+        || sets.count > storage.materialRequirementRows.size()) {
+        report_material_requirement_failure("table", 0, 0);
+        return false;
+    }
+    const std::span<const std::byte> table{storage.definition};
+    if (sets.dataOffset > table.size()
+        || sets.count
+               > (table.size() - sets.dataOffset) / tables::kMaterialRequirementSetRowStride) {
+        report_material_requirement_failure("table_bounds", 0, 0);
+        return false;
+    }
+    std::size_t conditionalRows = 0;
+    for (std::size_t set = 0; set < sets.count; ++set) {
+        const std::size_t setAt = sets.dataOffset + set * tables::kMaterialRequirementSetRowStride;
+        auto& output = storage.materialRequirementRows[set];
+        output = {};
+        std::int64_t relative = 0;
+        std::memcpy(&output.requirementSetHash,
+                    table.data() + setAt + tables::kMaterialRequirementSetHashOffset,
+                    sizeof output.requirementSetHash);
+        std::memcpy(&relative,
+                    table.data() + setAt + tables::kMaterialRequirementSetArrayPointerOffset,
+                    sizeof relative);
+        const std::size_t pointerAt = setAt + tables::kMaterialRequirementSetArrayPointerOffset;
+        if (output.requirementSetHash == 0 || relative < -static_cast<std::int64_t>(pointerAt)
+            || relative > static_cast<std::int64_t>(table.size() - pointerAt)) {
+            report_material_requirement_failure("set_pointer", set, 0);
+            return false;
+        }
+        const auto descriptor = static_cast<std::int64_t>(pointerAt) + relative;
+        if (descriptor >= 0 && static_cast<std::size_t>(descriptor) <= table.size()
+            && table.size() - static_cast<std::size_t>(descriptor)
+                   >= tables::kTableArrayDescriptor + sizeof(std::uint64_t)) {
+            const auto descriptorBytes =
+                table.subspan(static_cast<std::size_t>(descriptor),
+                              tables::kTableArrayDescriptor + sizeof(std::uint64_t));
+            if (std::all_of(descriptorBytes.begin(), descriptorBytes.end(), [](std::byte value) {
+                    return value == std::byte{};
+                })) {
+                // Native free actions keep a real set hash/index but point at one canonical
+                // all-zero array descriptor. Preserve that empty set instead of treating it as
+                // a malformed nonempty array.
+                output.requirementSetIndex = static_cast<std::uint16_t>(set);
+                output.requirementCount = 0;
+                continue;
+            }
+        }
+        tables::Array requirements{};
+        if (descriptor < 0
+            || !tables::find_array_at(table, static_cast<std::size_t>(descriptor), requirements)
+            || requirements.elementClass != tables::kMaterialRequirementRowClass
+            || requirements.count == 0 || requirements.count > output.requirements.size()
+            || requirements.dataOffset > table.size()
+            || requirements.count > (table.size() - requirements.dataOffset)
+                                        / tables::kMaterialRequirementRowStride) {
+            report_material_requirement_failure("set_array", set, 0);
+            return false;
+        }
+        output.requirementSetIndex = static_cast<std::uint16_t>(set);
+        output.requirementCount = static_cast<std::uint8_t>(requirements.count);
+        for (std::size_t row = 0; row < requirements.count; ++row) {
+            const std::size_t at =
+                requirements.dataOffset + row * tables::kMaterialRequirementRowStride;
+            std::uint32_t itemIndex = 0;
+            std::uint8_t deleted = 0;
+            std::uint8_t omitted = 0;
+            std::uint16_t condition = 0;
+            auto& requirement = output.requirements[row];
+            std::memcpy(&itemIndex,
+                        table.data() + at + tables::kMaterialRequirementItemIndexOffset,
+                        sizeof itemIndex);
+            std::memcpy(&requirement.quantity,
+                        table.data() + at + tables::kMaterialRequirementQuantityOffset,
+                        sizeof requirement.quantity);
+            std::memcpy(&deleted,
+                        table.data() + at + tables::kMaterialRequirementDeleteOffset,
+                        sizeof deleted);
+            std::memcpy(&omitted,
+                        table.data() + at + tables::kMaterialRequirementOmitOffset,
+                        sizeof omitted);
+            std::memcpy(&condition,
+                        table.data() + at + tables::kMaterialRequirementSentinelOffset,
+                        sizeof condition);
+            if (itemIndex >= itemDefinitionCount
+                || itemIndex > (std::numeric_limits<std::uint16_t>::max)() || deleted > 1
+                || omitted > 1) {
+                report_material_requirement_failure("row", set, row);
+                return false;
+            }
+            requirement.itemDefinitionIndex = static_cast<std::uint16_t>(itemIndex);
+            requirement.condition = condition;
+            requirement.deleteOnAction = deleted != 0;
+            requirement.omitFromRequirements = omitted != 0;
+            conditionalRows += condition != domain::kUnconditionalRequirement ? 1U : 0U;
+        }
+    }
+    const auto definitions =
+        std::span(storage.materialRequirementRows).first(static_cast<std::size_t>(sets.count));
+    const bool valid = domain::valid(definitions);
+    const bool published =
+        valid && state::build_data::publish_material_requirement_sets(definitions);
+    report_material_requirements(definitions.size(), conditionalRows, valid, published);
+    return published;
+}
+
+} // namespace sunrise::client::content::items::packages

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

@@ -0,0 +1,275 @@
+#include "package_socket_plug_build.h"
+
+#include <algorithm>
+#include <limits>
+#include <new>
+
+#include "../../../../state/build_data/runtime.h"
+
+namespace sunrise::client::content::items::packages {
+namespace {
+
+/** Sundial/native category families whose socket seed expands to every plug in that family. */
+constexpr std::array<std::uint32_t, 3> kExpandableCategories{
+    0xB134761EU,
+    0x87727F34U,
+    0x6C863692U,
+};
+/** Tracker sockets synthesize these three safe plug choices by socket type. */
+constexpr std::array<std::uint32_t, 3> kTrackerPlugHashes{
+    2'285'418'970U,
+    2'302'094'943U,
+    38'912'240U,
+};
+/** Native ordinary socket type whose choices are the synthetic tracker set. */
+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;
+
+/** 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. */
+[[nodiscard]] bool visit_member(void* opaque, std::uint32_t itemDefinitionIndex) noexcept {
+    auto& context = *static_cast<VisitorContext*>(opaque);
+    return context.build != nullptr
+           && context.build->add(itemDefinitionIndex, context.itemDefinitionCount);
+}
+
+} // namespace
+
+/** 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) {
+        if (kExpandableCategories[index] == categoryHash) {
+            return static_cast<std::uint8_t>(index + 1);
+        }
+    }
+    return 0;
+}
+
+/** Allocates the bounded build state and indexes expansion/tracker plug definitions. */
+bool SocketPlugBuild::prepare(
+    std::span<const std::uint8_t> specialCategories,
+    std::span<const state::build_data::items::Definition> itemDefinitions) noexcept {
+    release();
+    if (specialCategories.size() < itemDefinitions.size()
+        || itemDefinitions.size() > state::build_data::items::kDefinitionCapacity) {
+        return false;
+    }
+    rules_.reset(new (std::nothrow) socket_plugs::Rule[socket_plugs::kRuleCapacity]);
+    pools_.reset(new (std::nothrow) socket_plugs::Pool[socket_plugs::kPoolCapacity]);
+    members_.reset(new (std::nothrow) socket_plugs::Member[socket_plugs::kMemberCapacity]);
+    candidates_.reset(new (std::nothrow)
+                          socket_plugs::Member[state::build_data::items::kDefinitionCapacity]);
+    categoryMembers_.reset(
+        new (std::nothrow)
+            socket_plugs::Member[kCategoryCount * state::build_data::items::kDefinitionCapacity]);
+    lookup_.reset(new (std::nothrow) PoolLookup[kLookupCapacity]);
+    if (!rules_ || !pools_ || !members_ || !candidates_ || !categoryMembers_ || !lookup_) {
+        release();
+        return false;
+    }
+    pools_[socket_plugs::kEmptyPoolIndex] = {};
+    poolCount_ = 1;
+    for (std::size_t item = 0; item < itemDefinitions.size(); ++item) {
+        const std::uint8_t category = specialCategories[item];
+        if (category != 0 && category <= kCategoryCount) {
+            const std::size_t family = category - 1;
+            categoryMembers_[family * state::build_data::items::kDefinitionCapacity
+                             + categoryCounts_[family]++] = static_cast<std::uint16_t>(item);
+        }
+        for (const std::uint32_t trackerHash : kTrackerPlugHashes) {
+            if (itemDefinitions[item].definitionHash != trackerHash) {
+                continue;
+            }
+            if (trackerCount_ >= trackerMembers_.size()) {
+                release();
+                return false;
+            }
+            trackerMembers_[trackerCount_++] = static_cast<std::uint16_t>(item);
+        }
+    }
+    return true;
+}
+
+/** Appends one package member after enforcing the installed item-table bound. */
+bool SocketPlugBuild::add(std::uint32_t itemDefinitionIndex,
+                          std::size_t itemDefinitionCount) noexcept {
+    if (!candidates_ || itemDefinitionIndex >= itemDefinitionCount
+        || itemDefinitionIndex >= state::build_data::items::kDefinitionCapacity
+        || candidateCount_ >= state::build_data::items::kDefinitionCapacity) {
+        return false;
+    }
+    candidates_[candidateCount_++] = static_cast<std::uint16_t>(itemDefinitionIndex);
+    return true;
+}
+
+/** Expands special category seeds, sorts/deduplicates, then interns one exact pool. */
+bool SocketPlugBuild::intern(std::uint32_t& poolIndex) noexcept {
+    poolIndex = socket_plugs::kEmptyPoolIndex;
+    if (!candidates_ || !categoryMembers_ || !lookup_) {
+        return false;
+    }
+    std::sort(candidates_.get(), candidates_.get() + candidateCount_);
+    candidateCount_ = static_cast<std::size_t>(
+        std::unique(candidates_.get(), candidates_.get() + candidateCount_) - candidates_.get());
+    std::array<bool, kCategoryCount> expand{};
+    // Category codes were indexed by native item definition index during prepare().
+    for (std::size_t family = 0; family < kCategoryCount; ++family) {
+        const auto* familyMembers =
+            categoryMembers_.get() + family * state::build_data::items::kDefinitionCapacity;
+        for (std::size_t seed = 0; seed < candidateCount_ && !expand[family]; ++seed) {
+            expand[family] = std::binary_search(
+                familyMembers, familyMembers + categoryCounts_[family], candidates_[seed]);
+        }
+    }
+    for (std::size_t family = 0; family < kCategoryCount; ++family) {
+        if (!expand[family]) {
+            continue;
+        }
+        if (categoryCounts_[family]
+            > state::build_data::items::kDefinitionCapacity - candidateCount_) {
+            return false;
+        }
+        const auto* first =
+            categoryMembers_.get() + family * state::build_data::items::kDefinitionCapacity;
+        std::copy_n(first, categoryCounts_[family], candidates_.get() + candidateCount_);
+        candidateCount_ += categoryCounts_[family];
+    }
+    std::sort(candidates_.get(), candidates_.get() + candidateCount_);
+    candidateCount_ = static_cast<std::size_t>(
+        std::unique(candidates_.get(), candidates_.get() + candidateCount_) - candidates_.get());
+    if (candidateCount_ == 0) {
+        return true;
+    }
+
+    std::uint64_t fingerprint = kHashOffsetBasis;
+    for (std::size_t member = 0; member < candidateCount_; ++member) {
+        std::uint16_t value = candidates_[member];
+        for (std::size_t byte = 0; byte < sizeof value; ++byte) {
+            fingerprint ^= static_cast<std::uint8_t>(value);
+            fingerprint *= kHashPrime;
+            value >>= 8U;
+        }
+    }
+    fingerprint ^= candidateCount_;
+    fingerprint *= kHashPrime;
+    static_assert((kLookupCapacity & (kLookupCapacity - 1)) == 0);
+    const std::size_t start = static_cast<std::size_t>(fingerprint) & (kLookupCapacity - 1);
+    for (std::size_t probe = 0; probe < kLookupCapacity; ++probe) {
+        PoolLookup& slot = lookup_[(start + probe) & (kLookupCapacity - 1)];
+        if (slot.poolIndex == UINT32_MAX) {
+            if (poolCount_ >= socket_plugs::kPoolCapacity
+                || candidateCount_ > socket_plugs::kMemberCapacity - memberCount_) {
+                return false;
+            }
+            poolIndex = static_cast<std::uint32_t>(poolCount_);
+            pools_[poolCount_++] = {static_cast<std::uint32_t>(memberCount_),
+                                    static_cast<std::uint32_t>(candidateCount_)};
+            std::copy_n(candidates_.get(), candidateCount_, members_.get() + memberCount_);
+            memberCount_ += candidateCount_;
+            slot = {fingerprint, poolIndex};
+            return true;
+        }
+        if (slot.fingerprint != fingerprint || slot.poolIndex >= poolCount_) {
+            continue;
+        }
+        const socket_plugs::Pool& pool = pools_[slot.poolIndex];
+        if (pool.memberCount == candidateCount_
+            && std::equal(candidates_.get(),
+                          candidates_.get() + candidateCount_,
+                          members_.get() + pool.memberOffset)) {
+            poolIndex = slot.poolIndex;
+            return true;
+        }
+    }
+    return false;
+}
+
+/** Extracts every exact ordinary socket pool of one installed item definition. */
+bool SocketPlugBuild::append(const tables::items::Row& item,
+                             std::span<const std::byte> itemDefinition,
+                             std::span<const std::byte> plugSetTable,
+                             std::size_t itemDefinitionCount) noexcept {
+    if (!rules_ || item.definitionIndex >= itemDefinitionCount
+        || item.socketCount > socket_plugs::kLaneCapacity) {
+        return false;
+    }
+    bool complete = true;
+    for (std::uint8_t lane = 0; lane < item.socketCount; ++lane) {
+        candidateCount_ = 0;
+        VisitorContext visitor{this, itemDefinitionCount};
+        bool laneValid = tables::items::visit_allowed_plugs(
+            itemDefinition, plugSetTable, lane, visit_member, &visitor);
+        if (laneValid && item.initialPlugs[lane] != tables::items::kUnavailablePlug) {
+            laneValid = add(item.initialPlugs[lane], itemDefinitionCount);
+        }
+        if (laneValid && item.socketTypes[lane] == kTrackerSocketType) {
+            for (std::size_t tracker = 0; tracker < trackerCount_ && laneValid; ++tracker) {
+                laneValid = add(trackerMembers_[tracker], itemDefinitionCount);
+            }
+        }
+        std::uint32_t poolIndex = socket_plugs::kEmptyPoolIndex;
+        laneValid = laneValid && intern(poolIndex);
+        if (!laneValid || ruleCount_ >= socket_plugs::kRuleCapacity) {
+            ++skipped_;
+            complete = false;
+            continue;
+        }
+        rules_[ruleCount_++] = {item.definitionIndex, lane, 0, poolIndex};
+    }
+    return complete;
+}
+
+/** Publishes the bounded relation and releases all transient interning memory. */
+bool SocketPlugBuild::publish() noexcept {
+    const bool published =
+        rules_ && pools_ && members_
+        && state::build_data::publish_socket_plug_rules(std::span(rules_.get(), ruleCount_),
+                                                        std::span(pools_.get(), poolCount_),
+                                                        std::span(members_.get(), memberCount_));
+    release();
+    return published;
+}
+
+/** Reports how many lanes failed closed during extraction. */
+std::size_t SocketPlugBuild::skipped() const noexcept {
+    return skipped_;
+}
+
+std::size_t SocketPlugBuild::rule_count() const noexcept {
+    return ruleCount_;
+}
+
+std::size_t SocketPlugBuild::pool_count() const noexcept {
+    return poolCount_;
+}
+
+std::size_t SocketPlugBuild::member_count() const noexcept {
+    return memberCount_;
+}
+
+/** Drops all heap-backed extraction scratch and resets every count. */
+void SocketPlugBuild::release() noexcept {
+    rules_.reset();
+    pools_.reset();
+    members_.reset();
+    candidates_.reset();
+    categoryMembers_.reset();
+    lookup_.reset();
+    categoryCounts_ = {};
+    trackerMembers_ = {};
+    trackerCount_ = 0;
+    ruleCount_ = 0;
+    poolCount_ = 0;
+    memberCount_ = 0;
+    candidateCount_ = 0;
+    skipped_ = 0;
+}
+
+} // namespace sunrise::client::content::items::packages

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

@@ -0,0 +1,86 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <span>
+
+#include "../../../../middleware/content/packages/tables/items.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 tables = middleware::content::packages::tables;
+namespace socket_plugs = state::build_data::items::socket_plugs;
+
+/** Fixed-size, heap-backed interning state for one installed package pass. */
+class SocketPlugBuild final {
+public:
+    SocketPlugBuild() = default;
+    ~SocketPlugBuild() = default;
+    SocketPlugBuild(const SocketPlugBuild&) = delete;
+    SocketPlugBuild& operator=(const SocketPlugBuild&) = delete;
+
+    /** Allocates bounded scratch and indexes the three native expandable plug categories. */
+    [[nodiscard]] bool
+    prepare(std::span<const std::uint8_t> specialCategories,
+            std::span<const state::build_data::items::Definition> itemDefinitions) noexcept;
+
+    /** Extracts, expands, interns, and records every declared socket lane of one base item. */
+    [[nodiscard]] bool append(const tables::items::Row& item,
+                              std::span<const std::byte> itemDefinition,
+                              std::span<const std::byte> plugSetTable,
+                              std::size_t itemDefinitionCount) noexcept;
+
+    /** Publishes the completed exact relation, then releases its transient scratch. */
+    [[nodiscard]] bool publish() noexcept;
+
+    /** @return Socket lanes skipped because their package lists were malformed or over capacity. */
+    [[nodiscard]] std::size_t skipped() const noexcept;
+    [[nodiscard]] std::size_t rule_count() const noexcept;
+    [[nodiscard]] std::size_t pool_count() const noexcept;
+    [[nodiscard]] std::size_t member_count() const noexcept;
+
+    /** Package-list visitor entry point; accepts only an in-range bounded native index. */
+    [[nodiscard]] bool add(std::uint32_t itemDefinitionIndex,
+                           std::size_t itemDefinitionCount) noexcept;
+
+private:
+    struct PoolLookup {
+        std::uint64_t fingerprint{};
+        std::uint32_t poolIndex{UINT32_MAX};
+    };
+
+    static constexpr std::size_t kCategoryCount = 3;
+    static constexpr std::size_t kLookupCapacity = 1U << 19U;
+
+    std::unique_ptr<socket_plugs::Rule[]> rules_{};
+    std::unique_ptr<socket_plugs::Pool[]> pools_{};
+    std::unique_ptr<socket_plugs::Member[]> members_{};
+    std::unique_ptr<socket_plugs::Member[]> candidates_{};
+    std::unique_ptr<socket_plugs::Member[]> categoryMembers_{};
+    std::unique_ptr<PoolLookup[]> lookup_{};
+    std::array<std::size_t, kCategoryCount> categoryCounts_{};
+    std::array<socket_plugs::Member, 3> trackerMembers_{};
+    std::size_t trackerCount_{};
+    std::size_t ruleCount_{};
+    std::size_t poolCount_{};
+    std::size_t memberCount_{};
+    std::size_t candidateCount_{};
+    std::size_t skipped_{};
+
+    /** Expands native category families, canonicalizes, and interns the current candidate. */
+    [[nodiscard]] bool intern(std::uint32_t& poolIndex) noexcept;
+    /** Releases every transient allocation and count. */
+    void release() noexcept;
+};
+
+/**
+ * Classifies the only three plug-category hashes whose native socket rules expand by category.
+ * @return 1..3 for a supported expansion family, or zero otherwise.
+ */
+[[nodiscard]] std::uint8_t special_plug_category(std::uint32_t categoryHash) noexcept;
+
+} // namespace sunrise::client::content::items::packages

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

@@ -30,6 +30,43 @@ inline constexpr std::uint16_t kAbsentPackageId = 0xFFFFU;
 
 /** Element class of the item index table inside the investment container. */
 inline constexpr std::uint32_t kItemIndexTableClass = 0x80807BE8U;
+/** The investment root holds the installed collectible definition table at this slot. */
+inline constexpr std::size_t kCollectibleTableSlot = 19;
+/** Definition class recorded for the installed investment root tag. */
+inline constexpr std::uint32_t kInvestmentRootClass = 0x80807D84U;
+/** Definition class recorded for the installed collectible table tag. */
+inline constexpr std::uint32_t kCollectibleTableClass = 0x8080306DU;
+/** Element class recorded by the collectible table's inline array header. */
+inline constexpr std::uint32_t kCollectibleRowClass = 0x80803475U;
+/** One collectible row in this installed build occupies 184 bytes. */
+inline constexpr std::size_t kCollectibleRowStride = 0xB8;
+/** Authored DestinyCollectibleDefinition hash inside one collectible row. */
+inline constexpr std::size_t kCollectibleHashOffset = 0x28;
+/** Native item-definition index granted by one collectible row. */
+inline constexpr std::size_t kCollectibleItemIndexOffset = 0x2C;
+/** Native material-requirement-set ordinal carried by one collectible row. */
+inline constexpr std::size_t kCollectibleMaterialRequirementIndexOffset = 0x9A;
+/** The investment root holds the material-requirement-set table at this slot. */
+inline constexpr std::size_t kMaterialRequirementTableSlot = 96;
+/** Installed material-requirement-set table and row classes. */
+inline constexpr std::uint32_t kMaterialRequirementTableClass = 0x80807ACEU;
+inline constexpr std::uint32_t kMaterialRequirementSetRowClass = 0x80807AD4U;
+inline constexpr std::uint32_t kMaterialRequirementRowClass = 0x80807AD7U;
+/** One set row is a hash followed by a self-relative pointer to an array descriptor. */
+inline constexpr std::size_t kMaterialRequirementSetRowStride = 0x10;
+inline constexpr std::size_t kMaterialRequirementSetHashOffset = 0;
+inline constexpr std::size_t kMaterialRequirementSetArrayPointerOffset = 8;
+/** One requirement row is item index, quantity, two flags, then a native sentinel. */
+inline constexpr std::size_t kMaterialRequirementRowStride = 0x0C;
+inline constexpr std::size_t kMaterialRequirementItemIndexOffset = 0;
+inline constexpr std::size_t kMaterialRequirementQuantityOffset = 4;
+inline constexpr std::size_t kMaterialRequirementDeleteOffset = 8;
+inline constexpr std::size_t kMaterialRequirementOmitOffset = 9;
+inline constexpr std::size_t kMaterialRequirementSentinelOffset = 10;
+/** Plug item definitions name insertion/enabled requirement-set ordinals at fixed offsets. */
+inline constexpr std::size_t kInsertionMaterialRequirementSetIndexOffset = 0x1E8;
+inline constexpr std::size_t kEnabledMaterialRequirementSetIndexOffset = 0x200;
+inline constexpr std::uint16_t kUnavailableMaterialRequirementSetIndex = 0xFFFFU;
 /** Element class of an item's ordinary socket array. */
 inline constexpr std::uint32_t kOrdinarySocketClass = 0x808077C4U;
 /** One ordinary socket entry is 80 bytes. */
@@ -131,6 +168,8 @@ slot_tag(std::span<const std::byte> blob, std::size_t index, std::uint32_t& tag)
 
 /** The investment root holds the item index table at this slot. */
 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 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. */

+ 135 - 0
Sunrise/src/middleware/content/packages/tables/item_definition_reader.cpp

@@ -26,6 +26,21 @@ constexpr std::size_t kSocketTypeOffset = 0;
 constexpr std::size_t kSocketPlugOffset = 2;
 /** Fixed fields end after the instanced predicate. */
 constexpr std::size_t kFixedFieldEnd = kInstancedOffset + 1;
+/** Optional plug category used to expand three native reusable plug families. */
+constexpr std::size_t kPlugCategoryOffset = 392;
+/** Embedded reusable-list array descriptor inside one 80-byte ordinary socket entry. */
+constexpr std::size_t kEmbeddedPlugListOffset = 64;
+/** Reusable and randomized shared plug-set row indices inside one socket entry. */
+constexpr std::size_t kReusablePlugSetIndexOffset = 12;
+constexpr std::size_t kRandomizedPlugSetIndexOffset = 32;
+/** One shared plug-set table row is 24 bytes and carries its member descriptor at byte 8. */
+constexpr std::size_t kPlugSetRowStride = 24;
+constexpr std::size_t kPlugSetMemberDescriptorOffset = 8;
+/** Both embedded and shared plug member rows name an item index first and occupy 32 bytes. */
+constexpr std::size_t kPlugMemberStride = 32;
+constexpr std::size_t kPlugMemberIndexOffset = 0;
+/** Native arrays use 16-bit definition indices even though their serialized field is 32-bit. */
+constexpr std::uint64_t kMaximumPlugMemberCount = 65535;
 
 /** @param blob Source bytes. @param offset Field offset. @param value Receives the field. */
 template <typename Value>
@@ -133,6 +148,61 @@ void read_sockets(std::span<const std::byte> definition, Row& row) noexcept {
     }
 }
 
+/** Walks one checked array of 32-byte plug rows. */
+[[nodiscard]] bool visit_plug_array(std::span<const std::byte> blob,
+                                    const Array& array,
+                                    AllowedPlugVisitor visitor,
+                                    void* context) noexcept {
+    if (array.count > kMaximumPlugMemberCount || array.dataOffset > blob.size()
+        || array.count > (blob.size() - array.dataOffset) / kPlugMemberStride) {
+        return false;
+    }
+    for (std::uint64_t index = 0; index < array.count; ++index) {
+        std::uint32_t itemDefinitionIndex = 0;
+        const std::size_t row =
+            array.dataOffset + static_cast<std::size_t>(index) * kPlugMemberStride;
+        if (!read(blob, row + kPlugMemberIndexOffset, itemDefinitionIndex)
+            || !visitor(context, itemDefinitionIndex)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Walks one reusable or randomized shared plug-set row, when the socket declares it. */
+[[nodiscard]] bool visit_shared_plug_set(std::span<const std::byte> definition,
+                                         std::size_t socketEntry,
+                                         std::size_t setIndexOffset,
+                                         std::span<const std::byte> plugSetTable,
+                                         const Array& sets,
+                                         AllowedPlugVisitor visitor,
+                                         void* context) noexcept {
+    std::uint16_t setIndex = kUnavailablePlug;
+    if (!read(definition, socketEntry + setIndexOffset, setIndex)) {
+        return false;
+    }
+    if (setIndex == kUnavailablePlug) {
+        return true;
+    }
+    if (setIndex >= sets.count || sets.dataOffset > plugSetTable.size()
+        || sets.count > (plugSetTable.size() - sets.dataOffset) / kPlugSetRowStride) {
+        return false;
+    }
+    const std::size_t descriptor = sets.dataOffset
+                                   + static_cast<std::size_t>(setIndex) * kPlugSetRowStride
+                                   + kPlugSetMemberDescriptorOffset;
+    std::uint64_t memberCount = 0;
+    if (!read(plugSetTable, descriptor, memberCount)) {
+        return false;
+    }
+    if (memberCount == 0) {
+        return true;
+    }
+    Array members{};
+    return find_array_at(plugSetTable, descriptor, members)
+           && visit_plug_array(plugSetTable, members, visitor, context);
+}
+
 /** The block header carries its own self-relative pointer to the entries at byte 8. */
 constexpr std::size_t kStatDataMember = 8;
 /** One stat entry is 40 blob bytes. */
@@ -192,6 +262,8 @@ bool read_definition(std::span<const std::byte> definition, Row& row) noexcept {
     row = {};
     row.definitionHash = hash;
     row.definitionIndex = index;
+    row.insertionMaterialRequirementSetIndex = kUnavailableMaterialRequirementSetIndex;
+    row.enabledMaterialRequirementSetIndex = kUnavailableMaterialRequirementSetIndex;
     std::fill(std::begin(row.initialPlugs), std::end(row.initialPlugs), kUnavailablePlug);
     std::fill(std::begin(row.socketTypes), std::end(row.socketTypes), kUnavailableSocketType);
     if (definition.size() < kFixedFieldEnd) {
@@ -204,6 +276,14 @@ bool read_definition(std::span<const std::byte> definition, Row& row) noexcept {
         return false;
     }
     row.instanced = instanced != 0;
+    // Short legacy definitions simply do not declare a plug category.
+    (void)read(definition, kPlugCategoryOffset, row.plugCategoryHash);
+    (void)read(definition,
+               kInsertionMaterialRequirementSetIndexOffset,
+               row.insertionMaterialRequirementSetIndex);
+    (void)read(definition,
+               kEnabledMaterialRequirementSetIndexOffset,
+               row.enabledMaterialRequirementSetIndex);
     read_stats(definition, row);
     read_appearance(definition, row);
     read_socket_entry_list(definition, row);
@@ -212,4 +292,59 @@ bool read_definition(std::span<const std::byte> definition, Row& row) noexcept {
     return true;
 }
 
+/** Visits every list-backed allowed plug for one exact ordinary socket lane. */
+bool visit_allowed_plugs(std::span<const std::byte> definition,
+                         std::span<const std::byte> plugSetTable,
+                         std::uint8_t lane,
+                         AllowedPlugVisitor visitor,
+                         void* context) noexcept {
+    if (visitor == nullptr || lane >= kSocketCapacity) {
+        return false;
+    }
+    std::int64_t socketBlockRelative = 0;
+    if (!read(definition, kSocketBlockOffset, socketBlockRelative) || socketBlockRelative == 0) {
+        return false;
+    }
+    const std::int64_t socketBlock =
+        static_cast<std::int64_t>(kSocketBlockOffset) + socketBlockRelative;
+    if (socketBlock < 0 || static_cast<std::uint64_t>(socketBlock) >= definition.size()) {
+        return false;
+    }
+    Array sockets{};
+    if (!find_array_at(definition, static_cast<std::size_t>(socketBlock), sockets)
+        || sockets.elementClass != kOrdinarySocketClass || lane >= sockets.count
+        || sockets.dataOffset > definition.size()
+        || sockets.count > (definition.size() - sockets.dataOffset) / kSocketEntryStride) {
+        return false;
+    }
+    Array sets{};
+    if (!find_array_at(plugSetTable, kTableArrayDescriptor, sets)) {
+        return false;
+    }
+    const std::size_t socketEntry =
+        sockets.dataOffset + static_cast<std::size_t>(lane) * kSocketEntryStride;
+    Array embedded{};
+    std::uint64_t embeddedCount = 0;
+    if (!read(definition, socketEntry + kEmbeddedPlugListOffset, embeddedCount)
+        || (embeddedCount != 0
+            && (!find_array_at(definition, socketEntry + kEmbeddedPlugListOffset, embedded)
+                || !visit_plug_array(definition, embedded, visitor, context)))) {
+        return false;
+    }
+    return visit_shared_plug_set(definition,
+                                 socketEntry,
+                                 kReusablePlugSetIndexOffset,
+                                 plugSetTable,
+                                 sets,
+                                 visitor,
+                                 context)
+           && visit_shared_plug_set(definition,
+                                    socketEntry,
+                                    kRandomizedPlugSetIndexOffset,
+                                    plugSetTable,
+                                    sets,
+                                    visitor,
+                                    context);
+}
+
 } // namespace sunrise::middleware::content::packages::tables::items

+ 31 - 0
Sunrise/src/middleware/content/packages/tables/items.h

@@ -11,6 +11,8 @@ namespace sunrise::middleware::content::packages::tables::items {
 inline constexpr std::size_t kSocketCapacity = 12;
 /** All bits set marks a socket lane with no initial plug. */
 inline constexpr std::uint16_t kUnavailablePlug = 0xFFFF;
+/** All bits set marks an item definition with no insertion or enable requirement set. */
+inline constexpr std::uint16_t kUnavailableMaterialRequirementSetIndex = 0xFFFFU;
 
 /** Declared stat contributions one definition carries. Shipped rows stay far below this. */
 inline constexpr std::size_t kStatCapacity = 64;
@@ -47,6 +49,11 @@ struct Row {
     std::uint8_t socketCount{};
     std::uint16_t initialPlugs[kSocketCapacity]{};
     std::uint16_t socketTypes[kSocketCapacity]{};
+    /** Plug category used by a few native sockets to expand a seed into its whole safe family. */
+    std::uint32_t plugCategoryHash{};
+    /** Native material sets used when this definition is inserted or enabled as a plug. */
+    std::uint16_t insertionMaterialRequirementSetIndex{kUnavailableMaterialRequirementSetIndex};
+    std::uint16_t enabledMaterialRequirementSetIndex{kUnavailableMaterialRequirementSetIndex};
     std::uint8_t statCount{};
     std::uint8_t statRows[kStatCapacity]{};
     std::int32_t statValues[kStatCapacity]{};
@@ -77,4 +84,28 @@ void read_appearance(std::span<const std::byte> definition, Row& row) noexcept;
  */
 [[nodiscard]] bool read_definition(std::span<const std::byte> definition, Row& row) noexcept;
 
+/** Visitor called for each native item-definition index an ordinary socket list names. */
+using AllowedPlugVisitor = bool (*)(void* context, std::uint32_t itemDefinitionIndex) noexcept;
+
+/**
+ * Visits the embedded, reusable, and randomized plug-list members declared for one socket
+ * lane.
+ * The initial plug is a separate fixed field and is intentionally left to the caller.
+ *
+ * @param definition Whole base-item definition bytes.
+ * @param plugSetTable Whole shared plug-set
+ * definition table from investment-root slot 51.
+ * @param lane Ordinary socket lane to inspect.
+ *
+ * @param visitor Required bounded consumer.
+ * @param context Opaque consumer state.
+ * @return
+ * True when every referenced array is structurally valid and accepted by the visitor.
+ */
+[[nodiscard]] bool visit_allowed_plugs(std::span<const std::byte> definition,
+                                       std::span<const std::byte> plugSetTable,
+                                       std::uint8_t lane,
+                                       AllowedPlugVisitor visitor,
+                                       void* context) noexcept;
+
 } // namespace sunrise::middleware::content::packages::tables::items

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

@@ -8,10 +8,13 @@
 #include "../content/content_catalog.h"
 #include "abilities/ability_bucket_catalog.h"
 #include "cache/internal.h"
+#include "collectibles/collectible_catalog.h"
 #include "constants/investment_constant_catalog.h"
 #include "hash_names/hash_name_catalog.h"
 #include "inventory/buckets/inventory_bucket_catalog.h"
 #include "items/details/item_detail_catalog.h"
+#include "items/socket_plugs/socket_plug_catalog.h"
+#include "material_requirements/material_requirement_catalog.h"
 #include "progressions/progression_catalog.h"
 #include "runtime.h"
 #include "runtime/build_data_catalog_runtime.h"
@@ -91,11 +94,15 @@ bool initialize(void* module, std::uint64_t configuredEquipmentHash) noexcept {
     };
     if (status != cache::LoadStatus::loaded || !constants::replace(cachedConstants)
         || !content::replace(domains.named) || !content::seal() || !items::replace(domains.items)
+        || !collectibles::replace(domains.collectibles)
+        || !material_requirements::replace(domains.materialRequirementSets)
         || !inventory::buckets::replace(domains.inventoryBuckets)
         || !socket_entry_lists::replace(domains.socketEntryLists)
         // The per-entry tables are what the subclass selection reads. Without them a cache hit
         // 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)
         || !abilities::replace(domains.abilityBuckets)
         || !progressions::replace(domains.progressions)
         // The layouts are what activity message 1 reads. Without them a cache hit makes the

+ 13 - 1
Sunrise/src/state/build_data/cache/read/cache_file_reader.cpp

@@ -11,7 +11,9 @@ namespace {
 
 /** @return True when every required domain is nonempty. */
 [[nodiscard]] bool required_domains_present(const records::DomainCounts& counts) noexcept {
-    return counts.named != 0 && counts.items != 0 && counts.inventoryBuckets != 0
+    return counts.named != 0 && counts.items != 0 && counts.collectibles != 0
+           && counts.materialRequirementSets != 0 && counts.socketPlugRules != 0
+           && counts.socketPlugPools != 0 && counts.inventoryBuckets != 0
            && counts.socketEntryLists != 0 && counts.progressions != 0 && counts.scenarios != 0;
 }
 
@@ -19,7 +21,12 @@ namespace {
 [[nodiscard]] bool counts_fit(const records::DomainCounts& counts,
                               records::MutableDomains output) noexcept {
     return counts.named <= output.named.size() && counts.items <= output.items.size()
+           && counts.collectibles <= output.collectibles.size()
+           && counts.materialRequirementSets <= output.materialRequirementSets.size()
            && counts.itemDetails <= output.itemDetails.size()
+           && counts.socketPlugRules <= output.socketPlugRules.size()
+           && counts.socketPlugPools <= output.socketPlugPools.size()
+           && counts.socketPlugMembers <= output.socketPlugMembers.size()
            && counts.inventoryBuckets <= output.inventoryBuckets.size()
            && counts.socketEntryLists <= output.socketEntryLists.size()
            && counts.socketEntryTables <= output.socketEntryTables.size()
@@ -37,7 +44,12 @@ namespace {
     return {
         header.namedCount,
         header.itemCount,
+        header.collectibleCount,
+        header.materialRequirementSetCount,
         header.itemDetailCount,
+        header.socketPlugRuleCount,
+        header.socketPlugPoolCount,
+        header.socketPlugMemberCount,
         header.inventoryBucketCount,
         header.socketEntryListCount,
         header.socketEntryTableCount,

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

@@ -62,7 +62,18 @@ void clear(records::MutableDomains output) noexcept {
     }
     std::fill(output.named.begin(), output.named.end(), content::Definition{});
     std::fill(output.items.begin(), output.items.end(), items::Definition{});
+    std::fill(output.collectibles.begin(), output.collectibles.end(), collectibles::Definition{});
+    std::fill(output.materialRequirementSets.begin(),
+              output.materialRequirementSets.end(),
+              material_requirements::Definition{});
     std::fill(output.itemDetails.begin(), output.itemDetails.end(), items::details::Definition{});
+    std::fill(
+        output.socketPlugRules.begin(), output.socketPlugRules.end(), items::socket_plugs::Rule{});
+    std::fill(
+        output.socketPlugPools.begin(), output.socketPlugPools.end(), items::socket_plugs::Pool{});
+    std::fill(output.socketPlugMembers.begin(),
+              output.socketPlugMembers.end(),
+              items::socket_plugs::Member{});
     std::fill(output.inventoryBuckets.begin(),
               output.inventoryBuckets.end(),
               inventory::buckets::Descriptor{});
@@ -86,7 +97,13 @@ bool expected_size(const records::DomainCounts& counts, std::uint64_t& size) noe
     size = sizeof(records::Header);
     return add_records(counts.named, sizeof(records::NamedRecord), size)
            && add_records(counts.items, sizeof(records::ItemRecord), size)
+           && add_records(counts.collectibles, sizeof(records::CollectibleRecord), size)
+           && add_records(
+               counts.materialRequirementSets, sizeof(records::MaterialRequirementSetRecord), size)
            && add_records(counts.itemDetails, sizeof(records::ItemDetailRecord), size)
+           && add_records(counts.socketPlugRules, sizeof(records::SocketPlugRuleRecord), size)
+           && add_records(counts.socketPlugPools, sizeof(records::SocketPlugPoolRecord), size)
+           && add_records(counts.socketPlugMembers, sizeof(records::SocketPlugMemberRecord), size)
            && add_records(counts.inventoryBuckets, sizeof(records::InventoryBucketRecord), size)
            && add_records(counts.socketEntryLists, sizeof(records::SocketEntryListRecord), size)
            && add_records(counts.socketEntryTables, sizeof(records::SocketEntryTableRecord), size)
@@ -110,9 +127,25 @@ bool read_payload(HANDLE file,
         read_domain<records::NamedRecord>(file, output.named.first(counts.named), checksum);
     valid =
         valid && read_domain<records::ItemRecord>(file, output.items.first(counts.items), checksum);
+    valid = valid
+            && read_domain<records::CollectibleRecord>(
+                file, output.collectibles.first(counts.collectibles), checksum);
+    valid =
+        valid
+        && read_domain<records::MaterialRequirementSetRecord>(
+            file, output.materialRequirementSets.first(counts.materialRequirementSets), checksum);
     valid = valid
             && read_domain<records::ItemDetailRecord>(
                 file, output.itemDetails.first(counts.itemDetails), checksum);
+    valid = valid
+            && read_domain<records::SocketPlugRuleRecord>(
+                file, output.socketPlugRules.first(counts.socketPlugRules), checksum);
+    valid = valid
+            && read_domain<records::SocketPlugPoolRecord>(
+                file, output.socketPlugPools.first(counts.socketPlugPools), checksum);
+    valid = valid
+            && read_domain<records::SocketPlugMemberRecord>(
+                file, output.socketPlugMembers.first(counts.socketPlugMembers), checksum);
     valid = valid
             && read_domain<records::InventoryBucketRecord>(
                 file, output.inventoryBuckets.first(counts.inventoryBuckets), checksum);
@@ -150,7 +183,12 @@ bool read_payload(HANDLE file,
         constants,
         output.named.first(counts.named),
         output.items.first(counts.items),
+        output.collectibles.first(counts.collectibles),
+        output.materialRequirementSets.first(counts.materialRequirementSets),
         output.itemDetails.first(counts.itemDetails),
+        output.socketPlugRules.first(counts.socketPlugRules),
+        output.socketPlugPools.first(counts.socketPlugPools),
+        output.socketPlugMembers.first(counts.socketPlugMembers),
         output.inventoryBuckets.first(counts.inventoryBuckets),
         output.socketEntryLists.first(counts.socketEntryLists),
         output.socketEntryTables.first(counts.socketEntryTables),

+ 69 - 3
Sunrise/src/state/build_data/cache/records/cache_detail_links.cpp

@@ -1,11 +1,46 @@
+#include <algorithm>
+
 #include "../../items/details/item_detail_catalog.h"
+#include "../../items/socket_plugs/socket_plug_catalog.h"
 #include "validation.h"
 
 namespace sunrise::state::build_data::cache::records {
 
+/** Checks collectible ordinals and their optional links into the installed item table. */
+bool valid_collectible_links(std::span<const collectibles::Definition> collectibleDefinitions,
+                             std::span<const items::Definition> itemDefinitions) noexcept {
+    if (collectibleDefinitions.empty() || itemDefinitions.empty()
+        || !collectibles::valid(collectibleDefinitions)) {
+        return false;
+    }
+    for (std::size_t index = 0; index < collectibleDefinitions.size(); ++index) {
+        const collectibles::Definition& definition = collectibleDefinitions[index];
+        if (definition.collectibleIndex != index
+            || (definition.itemDefinitionIndex != collectibles::kUnavailableItemDefinitionIndex
+                && (definition.itemDefinitionIndex >= itemDefinitions.size()
+                    || itemDefinitions[definition.itemDefinitionIndex].definitionIndex
+                           != definition.itemDefinitionIndex))) {
+            return false;
+        }
+        for (std::size_t requirementIndex = 0;
+             requirementIndex < definition.materialRequirementCount;
+             ++requirementIndex) {
+            const collectibles::MaterialRequirement& requirement =
+                definition.materialRequirements[requirementIndex];
+            if (requirement.itemDefinitionIndex >= itemDefinitions.size()
+                || itemDefinitions[requirement.itemDefinitionIndex].definitionIndex
+                       != requirement.itemDefinitionIndex) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
 /**
- * Checks every configured detail reference against the other numeric domains.
- * @param details Item details that already passed their own checks.
+ * Checks every supported detail reference against the other numeric domains.
+ * @param details Item
+ * details that already passed their own checks.
  * @param itemDefinitions Complete dense item rows.
  * @param inventoryBuckets Complete bucket-routing rows in bucket order.
  * @param socketEntryLists Complete dense socket-list rows.
@@ -25,7 +60,7 @@ bool valid_item_detail_links(
     if (!items::details::valid(details)) {
         return false;
     }
-    // The domain holds equipped items and the plugs they socket. Only the equipped rows can be
+    // The domain holds equippable items and the plugs they socket. Only the base rows can be
     // equipped, and the loadout resolver already requires that there, so it is not a domain rule.
     for (const items::details::Definition& detail : details) {
         if (detail.definitionIndex >= itemDefinitions.size()
@@ -43,4 +78,35 @@ bool valid_item_detail_links(
     return true;
 }
 
+/** Checks every socket target lane and allowed plug against the numeric item domains. */
+bool 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 {
+    if (itemDefinitions.empty() || details.empty()
+        || !items::socket_plugs::valid(rules, pools, members)) {
+        return false;
+    }
+    for (const items::socket_plugs::Rule& rule : rules) {
+        if (rule.itemDefinitionIndex >= itemDefinitions.size()) {
+            return false;
+        }
+        const auto found = std::lower_bound(
+            details.begin(),
+            details.end(),
+            rule.itemDefinitionIndex,
+            [](const items::details::Definition& detail, std::uint16_t definitionIndex) {
+                return detail.definitionIndex < definitionIndex;
+            });
+        if (found == details.end() || found->definitionIndex != rule.itemDefinitionIndex
+            || rule.lane >= found->ordinarySocketCount) {
+            return false;
+        }
+    }
+    return std::all_of(members.begin(), members.end(), [&itemDefinitions](const auto member) {
+        return member < itemDefinitions.size();
+    });
+}
+
 } // namespace sunrise::state::build_data::cache::records

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

@@ -8,6 +8,8 @@
 #include "../../hash_names/hash_name_catalog.h"
 #include "../../inventory/buckets/inventory_bucket_catalog.h"
 #include "../../items/details/item_detail_catalog.h"
+#include "../../items/socket_plugs/socket_plug_catalog.h"
+#include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/progression_catalog.h"
 #include "../../scenarios/scenario_catalog.h"
 #include "../../socket_entry_lists/socket_entry_list_catalog.h"
@@ -70,6 +72,19 @@ namespace {
     return left.definitionIndex < right.definitionIndex;
 }
 
+/** @return Native collectible-index order. */
+[[nodiscard]] bool collectible_less(const collectibles::Definition& left,
+                                    const collectibles::Definition& right) noexcept {
+    return left.collectibleIndex < right.collectibleIndex;
+}
+
+/** @return Native requirement-set ordinal order. */
+[[nodiscard]] bool
+material_requirement_less(const material_requirements::Definition& left,
+                          const material_requirements::Definition& right) noexcept {
+    return left.requirementSetIndex < right.requirementSetIndex;
+}
+
 /** @return Native-index order for socket-list rows. */
 [[nodiscard]] bool socket_less(const socket_entry_lists::Definition& left,
                                const socket_entry_lists::Definition& right) noexcept {
@@ -111,7 +126,12 @@ template <typename Value, typename Less>
 /** @return True when every count fits the fixed storage. */
 [[nodiscard]] bool counts_fit(MutableDomains domains, const DomainCounts& counts) noexcept {
     return counts.named <= domains.named.size() && counts.items <= domains.items.size()
+           && counts.collectibles <= domains.collectibles.size()
+           && counts.materialRequirementSets <= domains.materialRequirementSets.size()
            && counts.itemDetails <= domains.itemDetails.size()
+           && counts.socketPlugRules <= domains.socketPlugRules.size()
+           && counts.socketPlugPools <= domains.socketPlugPools.size()
+           && counts.socketPlugMembers <= domains.socketPlugMembers.size()
            && counts.inventoryBuckets <= domains.inventoryBuckets.size()
            && counts.socketEntryLists <= domains.socketEntryLists.size()
            && counts.socketEntryTables <= domains.socketEntryTables.size()
@@ -133,12 +153,30 @@ bool canonicalize(MutableDomains domains, const DomainCounts& counts) noexcept {
     }
     const auto named = domains.named.first(counts.named);
     const auto items = domains.items.first(counts.items);
+    const auto collectibles = domains.collectibles.first(counts.collectibles);
+    const auto materialRequirementSets =
+        domains.materialRequirementSets.first(counts.materialRequirementSets);
     const auto itemDetails = domains.itemDetails.first(counts.itemDetails);
+    const auto socketPlugRules = domains.socketPlugRules.first(counts.socketPlugRules);
     const auto inventoryBuckets = domains.inventoryBuckets.first(counts.inventoryBuckets);
     const auto socketEntryLists = domains.socketEntryLists.first(counts.socketEntryLists);
     std::sort(named.begin(), named.end(), named_less);
     std::sort(items.begin(), items.end(), item_less);
+    std::sort(collectibles.begin(), collectibles.end(), collectible_less);
+    std::sort(
+        materialRequirementSets.begin(), materialRequirementSets.end(), material_requirement_less);
     std::sort(itemDetails.begin(), itemDetails.end(), detail_less);
+    // Rules are published in exact item/lane order. Pools and members are an indexed relation,
+    // so reordering either would invalidate every pool reference and range.
+    if (!std::is_sorted(socketPlugRules.begin(),
+                        socketPlugRules.end(),
+                        [](const auto& left, const auto& right) {
+                            return left.itemDefinitionIndex < right.itemDefinitionIndex
+                                   || (left.itemDefinitionIndex == right.itemDefinitionIndex
+                                       && left.lane < right.lane);
+                        })) {
+        return false;
+    }
     std::sort(inventoryBuckets.begin(), inventoryBuckets.end(), bucket_less);
     const auto abilityBuckets = domains.abilityBuckets.first(counts.abilityBuckets);
     const auto socketEntryTables = domains.socketEntryTables.first(counts.socketEntryTables);
@@ -153,14 +191,22 @@ bool canonicalize(MutableDomains domains, const DomainCounts& counts) noexcept {
 /** Checks the structure rules, the sort order, and every cross-domain item reference. */
 bool valid_domains(Domains domains) noexcept {
     if (domains.constants.extracted != 1U || domains.named.empty() || domains.items.empty()
+        || domains.collectibles.empty() || domains.materialRequirementSets.empty()
+        || domains.socketPlugRules.empty() || domains.socketPlugPools.empty()
         || domains.inventoryBuckets.empty() || domains.socketEntryLists.empty()
         || !std::all_of(domains.named.begin(), domains.named.end(), valid_name)
         || !strictly_ordered(domains.named, named_less) || !items::valid(domains.items)
+        || !collectibles::valid(domains.collectibles)
+        || !strictly_ordered(domains.collectibles, collectible_less)
+        || !material_requirements::valid(domains.materialRequirementSets)
+        || !strictly_ordered(domains.materialRequirementSets, material_requirement_less)
         || !inventory::buckets::valid(domains.inventoryBuckets)
         || !strictly_ordered(domains.inventoryBuckets, bucket_less)
         || !socket_entry_lists::valid(domains.socketEntryLists)
         || !socket_entry_lists::valid_entry_tables(domains.socketEntryTables)
         || !strictly_ordered(domains.itemDetails, detail_less)
+        || !items::socket_plugs::valid(
+            domains.socketPlugRules, domains.socketPlugPools, domains.socketPlugMembers)
         || !abilities::valid(domains.abilityBuckets)
         || !strictly_ordered(domains.abilityBuckets, ability_less)
         || !progressions::valid(domains.progressions)
@@ -178,13 +224,30 @@ bool valid_domains(Domains domains) noexcept {
             return false;
         }
     }
+    for (const material_requirements::Definition& definition : domains.materialRequirementSets) {
+        for (std::size_t index = 0; index < definition.requirementCount; ++index) {
+            const std::uint16_t itemIndex = definition.requirements[index].itemDefinitionIndex;
+            if (static_cast<std::size_t>(itemIndex) >= domains.items.size()
+                || domains.items[itemIndex].definitionIndex != itemIndex) {
+                return false;
+            }
+        }
+    }
     for (std::size_t index = 0; index < domains.socketEntryLists.size(); ++index) {
         if (domains.socketEntryLists[index].definitionIndex != index) {
             return false;
         }
     }
-    return valid_item_detail_links(
-        domains.itemDetails, domains.items, domains.inventoryBuckets, domains.socketEntryLists);
+    return valid_collectible_links(domains.collectibles, domains.items)
+           && valid_item_detail_links(domains.itemDetails,
+                                      domains.items,
+                                      domains.inventoryBuckets,
+                                      domains.socketEntryLists)
+           && valid_socket_plug_links(domains.socketPlugRules,
+                                      domains.socketPlugPools,
+                                      domains.socketPlugMembers,
+                                      domains.items,
+                                      domains.itemDetails);
 }
 
 } // namespace sunrise::state::build_data::cache::records

+ 111 - 1
Sunrise/src/state/build_data/cache/records/cache_record_codec.cpp

@@ -60,6 +60,8 @@ bool encode(const items::Definition& value, ItemRecord& record) noexcept {
         value.definitionIndex,
         value.bucketId,
         kReservedFieldValue,
+        value.insertionMaterialRequirementSetIndex,
+        value.enabledMaterialRequirementSetIndex,
     };
     return true;
 }
@@ -70,7 +72,115 @@ bool decode(const ItemRecord& record, items::Definition& value) noexcept {
     if (record.reserved != kReservedFieldValue) {
         return false;
     }
-    value = {record.definitionHash, record.definitionIndex, record.bucketId};
+    value = {record.definitionHash,
+             record.definitionIndex,
+             record.bucketId,
+             record.insertionMaterialRequirementSetIndex,
+             record.enabledMaterialRequirementSetIndex};
+    return true;
+}
+
+/** Encodes one collectible ordinal and its optional item link. */
+bool encode(const collectibles::Definition& value, CollectibleRecord& record) noexcept {
+    if (value.materialRequirementCount > value.materialRequirements.size()) {
+        return false;
+    }
+    record = {};
+    record.collectibleHash = value.collectibleHash;
+    record.materialRequirementSetHash = value.materialRequirementSetHash;
+    record.collectibleIndex = value.collectibleIndex;
+    record.itemDefinitionIndex = value.itemDefinitionIndex;
+    record.materialRequirementSetIndex = value.materialRequirementSetIndex;
+    record.materialRequirementCount = value.materialRequirementCount;
+    for (std::size_t index = 0; index < value.materialRequirements.size(); ++index) {
+        const collectibles::MaterialRequirement& requirement = value.materialRequirements[index];
+        record.materialRequirements[index] = {
+            requirement.quantity,
+            requirement.itemDefinitionIndex,
+            material_requirements::kUnconditionalRequirement,
+            static_cast<std::uint8_t>(requirement.deleteOnAction),
+            static_cast<std::uint8_t>(requirement.omitFromRequirements),
+        };
+    }
+    return record.materialRequirementCount <= record.materialRequirements.size();
+}
+
+/** Decodes one collectible row; the complete-domain validator checks both indices. */
+bool decode(const CollectibleRecord& record, collectibles::Definition& value) noexcept {
+    value = {};
+    if (record.reserved != kReservedFieldValue
+        || record.materialRequirementCount > record.materialRequirements.size()) {
+        return false;
+    }
+    value.collectibleHash = record.collectibleHash;
+    value.materialRequirementSetHash = record.materialRequirementSetHash;
+    value.collectibleIndex = record.collectibleIndex;
+    value.itemDefinitionIndex = record.itemDefinitionIndex;
+    value.materialRequirementSetIndex = record.materialRequirementSetIndex;
+    value.materialRequirementCount = record.materialRequirementCount;
+    for (std::size_t index = 0; index < record.materialRequirements.size(); ++index) {
+        const MaterialRequirementRecord& requirement = record.materialRequirements[index];
+        if (requirement.condition != material_requirements::kUnconditionalRequirement
+            || requirement.deleteOnAction > 1 || requirement.omitFromRequirements > 1) {
+            return false;
+        }
+        value.materialRequirements[index] = {
+            requirement.quantity,
+            requirement.itemDefinitionIndex,
+            requirement.deleteOnAction != 0,
+            requirement.omitFromRequirements != 0,
+        };
+    }
+    return true;
+}
+
+/** Encodes one installed action-cost set with canonical flags and unused rows. */
+bool encode(const material_requirements::Definition& value,
+            MaterialRequirementSetRecord& record) noexcept {
+    if (value.requirementCount > value.requirements.size()) {
+        return false;
+    }
+    record = {};
+    record.requirementSetHash = value.requirementSetHash;
+    record.requirementSetIndex = value.requirementSetIndex;
+    record.requirementCount = value.requirementCount;
+    for (std::size_t index = 0; index < value.requirements.size(); ++index) {
+        const material_requirements::Requirement& requirement = value.requirements[index];
+        record.requirements[index] = {
+            requirement.quantity,
+            requirement.itemDefinitionIndex,
+            requirement.condition,
+            static_cast<std::uint8_t>(requirement.deleteOnAction),
+            static_cast<std::uint8_t>(requirement.omitFromRequirements),
+        };
+    }
+    return true;
+}
+
+/** Decodes one installed action-cost set after checking every packed boolean. */
+bool decode(const MaterialRequirementSetRecord& record,
+            material_requirements::Definition& value) noexcept {
+    value = {};
+    if (record.reserved != kReservedFieldValue
+        || record.requirementCount > record.requirements.size()) {
+        return false;
+    }
+    value.requirementSetHash = record.requirementSetHash;
+    value.requirementSetIndex = record.requirementSetIndex;
+    value.requirementCount = record.requirementCount;
+    for (std::size_t index = 0; index < record.requirements.size(); ++index) {
+        const MaterialRequirementRecord& requirement = record.requirements[index];
+        if (requirement.deleteOnAction > 1 || requirement.omitFromRequirements > 1) {
+            return false;
+        }
+        value.requirements[index] = {
+            requirement.quantity,
+            requirement.itemDefinitionIndex,
+            requirement.condition,
+            requirement.deleteOnAction != 0,
+            requirement.omitFromRequirements != 0,
+        };
+    }
     return true;
 }
 

+ 53 - 0
Sunrise/src/state/build_data/cache/records/cache_socket_plug_record_codec.cpp

@@ -0,0 +1,53 @@
+#include "codec.h"
+
+namespace sunrise::state::build_data::cache::records {
+
+/** Encodes one exact item/lane rule only when its canonical padding is zero. */
+bool encode(const items::socket_plugs::Rule& value, SocketPlugRuleRecord& record) noexcept {
+    record = {};
+    if (value.reserved != 0) {
+        return false;
+    }
+    record.itemDefinitionIndex = value.itemDefinitionIndex;
+    record.lane = value.lane;
+    record.poolIndex = value.poolIndex;
+    return true;
+}
+
+/** Decodes one exact item/lane rule after checking its reserved byte. */
+bool decode(const SocketPlugRuleRecord& record, items::socket_plugs::Rule& value) noexcept {
+    value = {};
+    if (record.reserved != 0) {
+        return false;
+    }
+    value.itemDefinitionIndex = record.itemDefinitionIndex;
+    value.lane = record.lane;
+    value.poolIndex = record.poolIndex;
+    return true;
+}
+
+/** Encodes one pool range. Cross-row contiguity is checked at the domain boundary. */
+bool encode(const items::socket_plugs::Pool& value, SocketPlugPoolRecord& record) noexcept {
+    record = {value.memberOffset, value.memberCount};
+    return true;
+}
+
+/** Decodes one pool range. Cross-row contiguity is checked at the domain boundary. */
+bool decode(const SocketPlugPoolRecord& record, items::socket_plugs::Pool& value) noexcept {
+    value = {record.memberOffset, record.memberCount};
+    return true;
+}
+
+/** Encodes one native plug-definition index. */
+bool encode(items::socket_plugs::Member value, SocketPlugMemberRecord& record) noexcept {
+    record = {value};
+    return true;
+}
+
+/** Decodes one native plug-definition index. */
+bool decode(const SocketPlugMemberRecord& record, items::socket_plugs::Member& value) noexcept {
+    value = record.itemDefinitionIndex;
+    return true;
+}
+
+} // namespace sunrise::state::build_data::cache::records

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

@@ -17,6 +17,20 @@ namespace sunrise::state::build_data::cache::records {
 /** @param value Receives the runtime row. @return True when the disk row is in standard form. */
 [[nodiscard]] bool decode(const ItemRecord& record, items::Definition& value) noexcept;
 
+/** @param record Receives the packed disk row. @return Always true. */
+[[nodiscard]] bool encode(const collectibles::Definition& value,
+                          CollectibleRecord& record) noexcept;
+
+/** @param value Receives the runtime row. @return Always true. */
+[[nodiscard]] bool decode(const CollectibleRecord& record,
+                          collectibles::Definition& value) noexcept;
+
+/** Exact dense action-cost set codecs. */
+[[nodiscard]] bool encode(const material_requirements::Definition& value,
+                          MaterialRequirementSetRecord& record) noexcept;
+[[nodiscard]] bool decode(const MaterialRequirementSetRecord& record,
+                          material_requirements::Definition& value) noexcept;
+
 /** @param record Receives the packed disk row. @return True when the state is a known one. */
 [[nodiscard]] bool encode(const items::details::Definition& value,
                           ItemDetailRecord& record) noexcept;
@@ -25,6 +39,24 @@ namespace sunrise::state::build_data::cache::records {
 [[nodiscard]] bool decode(const ItemDetailRecord& record,
                           items::details::Definition& value) noexcept;
 
+/** Exact ordinary-socket rule codecs. */
+[[nodiscard]] bool encode(const items::socket_plugs::Rule& value,
+                          SocketPlugRuleRecord& record) noexcept;
+[[nodiscard]] bool decode(const SocketPlugRuleRecord& record,
+                          items::socket_plugs::Rule& value) noexcept;
+
+/** Deduplicated socket-pool range codecs. */
+[[nodiscard]] bool encode(const items::socket_plugs::Pool& value,
+                          SocketPlugPoolRecord& record) noexcept;
+[[nodiscard]] bool decode(const SocketPlugPoolRecord& record,
+                          items::socket_plugs::Pool& value) noexcept;
+
+/** Flat allowed-plug member codecs. */
+[[nodiscard]] bool encode(items::socket_plugs::Member value,
+                          SocketPlugMemberRecord& record) noexcept;
+[[nodiscard]] bool decode(const SocketPlugMemberRecord& record,
+                          items::socket_plugs::Member& value) noexcept;
+
 /** @param record Receives the packed disk row. @return Always true. */
 [[nodiscard]] bool encode(const inventory::buckets::Descriptor& value,
                           InventoryBucketRecord& record) noexcept;

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

@@ -5,10 +5,13 @@
 
 #include "../../../content/content_catalog.h"
 #include "../../abilities/definition.h"
+#include "../../collectibles/collectible_catalog.h"
 #include "../../hash_names/definition.h"
 #include "../../inventory/buckets/definition.h"
 #include "../../items/details/definition.h"
 #include "../../items/item_catalog.h"
+#include "../../items/socket_plugs/definition.h"
+#include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/definition.h"
 #include "../../scenarios/definition.h"
 #include "../../socket_entry_lists/definition.h"
@@ -21,7 +24,12 @@ namespace sunrise::state::build_data::cache::records {
 struct DomainCounts {
     std::size_t named{};
     std::size_t items{};
+    std::size_t collectibles{};
+    std::size_t materialRequirementSets{};
     std::size_t itemDetails{};
+    std::size_t socketPlugRules{};
+    std::size_t socketPlugPools{};
+    std::size_t socketPlugMembers{};
     std::size_t inventoryBuckets{};
     std::size_t socketEntryLists{};
     std::size_t socketEntryTables{};
@@ -40,7 +48,12 @@ struct MutableDomains {
     InvestmentConstants* constants{};
     std::span<content::Definition> named;
     std::span<items::Definition> items;
+    std::span<collectibles::Definition> collectibles;
+    std::span<material_requirements::Definition> materialRequirementSets;
     std::span<items::details::Definition> itemDetails;
+    std::span<items::socket_plugs::Rule> socketPlugRules;
+    std::span<items::socket_plugs::Pool> socketPlugPools;
+    std::span<items::socket_plugs::Member> socketPlugMembers;
     std::span<inventory::buckets::Descriptor> inventoryBuckets;
     std::span<socket_entry_lists::Definition> socketEntryLists;
     std::span<socket_entry_lists::EntryTable> socketEntryTables;
@@ -58,7 +71,12 @@ struct Domains {
     InvestmentConstants constants{};
     std::span<const content::Definition> named;
     std::span<const items::Definition> items;
+    std::span<const collectibles::Definition> collectibles;
+    std::span<const material_requirements::Definition> materialRequirementSets;
     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 inventory::buckets::Descriptor> inventoryBuckets;
     std::span<const socket_entry_lists::Definition> socketEntryLists;
     std::span<const socket_entry_lists::EntryTable> socketEntryTables;

+ 85 - 4
Sunrise/src/state/build_data/cache/records/format.h

@@ -7,11 +7,14 @@
 
 #include "../../../content/content_catalog.h"
 #include "../../abilities/definition.h"
+#include "../../collectibles/collectible_catalog.h"
 #include "../../constants/definition.h"
 #include "../../definition.h"
 #include "../../hash_names/definition.h"
 #include "../../items/details/definition.h"
 #include "../../items/item_catalog.h"
+#include "../../items/socket_plugs/definition.h"
+#include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/definition.h"
 #include "../../scenarios/definition.h"
 #include "../../spawn_sets/definition.h"
@@ -24,7 +27,7 @@ inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', '
  * Current build-data cache format. An older cache is rebuilt rather than read, so a bump needs
  * no other edit. Bump it whenever a domain's stored shape changes.
  */
-inline constexpr std::uint32_t kCacheFormatVersion = 23;
+inline constexpr std::uint32_t kCacheFormatVersion = 31;
 /** 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. */
@@ -62,7 +65,12 @@ struct Header {
     std::uint64_t configuredEquipmentHash{};
     std::uint32_t namedCount{};
     std::uint32_t itemCount{};
+    std::uint32_t collectibleCount{};
+    std::uint32_t materialRequirementSetCount{};
     std::uint32_t itemDetailCount{};
+    std::uint32_t socketPlugRuleCount{};
+    std::uint32_t socketPlugPoolCount{};
+    std::uint32_t socketPlugMemberCount{};
     std::uint32_t inventoryBucketCount{};
     std::uint32_t socketEntryListCount{};
     std::uint32_t socketEntryTableCount{};
@@ -94,9 +102,48 @@ struct ItemRecord {
     std::uint8_t bucketId{items::kUnresolvedBucketId};
     /** Must be zero, so the packed item row matches across compilers. */
     std::uint8_t reserved{};
+    std::uint16_t insertionMaterialRequirementSetIndex{
+        items::kUnavailableMaterialRequirementSetIndex};
+    std::uint16_t enabledMaterialRequirementSetIndex{
+        items::kUnavailableMaterialRequirementSetIndex};
 };
 
-/** Disk form of the configured item fields instance generation uses. */
+/** Disk form of one material charged by a native Collections acquisition. */
+struct MaterialRequirementRecord {
+    std::uint32_t quantity{};
+    std::uint16_t itemDefinitionIndex{collectibles::kUnavailableItemDefinitionIndex};
+    std::uint16_t condition{material_requirements::kUnconditionalRequirement};
+    std::uint8_t deleteOnAction{};
+    std::uint8_t omitFromRequirements{};
+};
+
+/** Disk form of one native collectible ordinal, item link, and installed acquisition cost. */
+struct CollectibleRecord {
+    std::uint32_t collectibleHash{};
+    std::uint32_t materialRequirementSetHash{};
+    std::uint16_t collectibleIndex{};
+    std::uint16_t itemDefinitionIndex{collectibles::kUnavailableItemDefinitionIndex};
+    std::uint16_t materialRequirementSetIndex{
+        collectibles::kUnavailableMaterialRequirementSetIndex};
+    std::uint8_t materialRequirementCount{};
+    /** Must be zero, so unused bytes have one canonical representation. */
+    std::uint8_t reserved{};
+    std::array<MaterialRequirementRecord, collectibles::kMaterialRequirementCapacity>
+        materialRequirements{};
+};
+
+/** Disk form of one dense installed action-cost set. */
+struct MaterialRequirementSetRecord {
+    std::uint32_t requirementSetHash{};
+    std::uint16_t requirementSetIndex{material_requirements::kUnavailableSetIndex};
+    std::uint8_t requirementCount{};
+    /** Must be zero, so unused bytes have one canonical representation. */
+    std::uint8_t reserved{};
+    std::array<MaterialRequirementRecord, material_requirements::kRequirementCapacity>
+        requirements{};
+};
+
+/** Disk form of the supported item fields instance generation uses. */
 struct ItemDetailRecord {
     std::uint16_t definitionIndex{};
     std::uint8_t bucketId{};
@@ -125,6 +172,26 @@ struct ItemDetailRecord {
     std::array<std::uint16_t, items::details::kRenderOverrideCapacity> overrideValues{};
 };
 
+/** Disk form of one exact item/lane-to-deduplicated-pool rule. */
+struct SocketPlugRuleRecord {
+    std::uint16_t itemDefinitionIndex{};
+    std::uint8_t lane{};
+    /** Must be zero so all unused bytes have one canonical value. */
+    std::uint8_t reserved{};
+    std::uint32_t poolIndex{};
+};
+
+/** Disk form of one contiguous range in the flat allowed-plug member bank. */
+struct SocketPlugPoolRecord {
+    std::uint32_t memberOffset{};
+    std::uint32_t memberCount{};
+};
+
+/** Disk form of one native item-definition index allowed as a plug. */
+struct SocketPlugMemberRecord {
+    std::uint16_t itemDefinitionIndex{};
+};
+
 /** Disk form of one inventory-bucket array-routing descriptor. */
 struct InventoryBucketRecord {
     std::uint8_t bucketId{};
@@ -263,7 +330,7 @@ static_assert(sizeof(Prefix) == kCacheMagic.size() + sizeof(std::uint32_t));
 static_assert(sizeof(InvestmentConstants)
               == constants::kCharacterStatRowCount + 2 * sizeof(std::uint8_t));
 static_assert(sizeof(Header)
-              == kCacheMagic.size() + 16 * sizeof(std::uint32_t) + 2 * sizeof(std::uint64_t)
+              == kCacheMagic.size() + 21 * sizeof(std::uint32_t) + 2 * sizeof(std::uint64_t)
                      + sizeof(InvestmentConstants));
 static_assert(sizeof(HashNameRecord)
               == hash_names::kNameLength + sizeof(std::uint32_t) + 4 * sizeof(std::uint8_t));
@@ -297,7 +364,17 @@ static_assert(sizeof(NamedRecord)
               == content::kDefinitionNameCapacity + 2 * sizeof(std::uint16_t)
                      + 2 * sizeof(std::uint32_t));
 static_assert(sizeof(ItemRecord)
-              == sizeof(std::uint32_t) + 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(MaterialRequirementRecord)
+              == sizeof(std::uint32_t) + 2 * sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t));
+static_assert(sizeof(CollectibleRecord)
+              == 2 * sizeof(std::uint32_t) + 3 * sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t)
+                     + collectibles::kMaterialRequirementCapacity
+                           * sizeof(MaterialRequirementRecord));
+static_assert(sizeof(MaterialRequirementSetRecord)
+              == sizeof(std::uint32_t) + sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t)
+                     + material_requirements::kRequirementCapacity
+                           * sizeof(MaterialRequirementRecord));
 static_assert(sizeof(ItemDetailRecord)
               == 4 * sizeof(std::uint16_t) + 8 * sizeof(std::uint8_t) + sizeof(std::int32_t)
                      + sizeof(std::uint32_t)
@@ -306,6 +383,10 @@ static_assert(sizeof(ItemDetailRecord)
                      + items::details::kSandboxPerkCapacity * sizeof(std::uint16_t)
                      + items::details::kRenderOverrideCapacity
                            * (2 * sizeof(std::uint8_t) + sizeof(std::uint16_t)));
+static_assert(sizeof(SocketPlugRuleRecord)
+              == sizeof(std::uint16_t) + 2 * sizeof(std::uint8_t) + sizeof(std::uint32_t));
+static_assert(sizeof(SocketPlugPoolRecord) == 2 * sizeof(std::uint32_t));
+static_assert(sizeof(SocketPlugMemberRecord) == sizeof(std::uint16_t));
 static_assert(sizeof(InventoryBucketRecord)
               == 2 * sizeof(std::uint8_t) + 2 * sizeof(std::uint16_t));
 static_assert(sizeof(SocketEntryListRecord)

+ 24 - 0
Sunrise/src/state/build_data/cache/records/validation.h

@@ -26,6 +26,30 @@ valid_item_detail_links(std::span<const items::details::Definition> details,
                         std::span<const inventory::buckets::Descriptor> inventoryBuckets,
                         std::span<const socket_entry_lists::Definition> socketEntryLists) noexcept;
 
+/**
+ * Checks exact socket rules and members against the complete item and detail domains.
+ *
+ * @return True when every target lane exists and every plug index names an installed item row.
+ */
+[[nodiscard]] bool
+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;
+
+/**
+ * Checks every collectible item index against the complete dense item table.
+ * @param
+ * collectibles Complete dense collectible rows.
+ * @param itemDefinitions Complete dense item
+ * rows.
+ * @return True when each row has its own ordinal and every available item link exists.
+ */
+[[nodiscard]] bool
+valid_collectible_links(std::span<const collectibles::Definition> collectibles,
+                        std::span<const items::Definition> itemDefinitions) noexcept;
+
 /** @param domains Complete sorted domains. @return True when every domain passes its checks. */
 [[nodiscard]] bool valid_domains(Domains domains) noexcept;
 

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

@@ -18,7 +18,12 @@ namespace {
     /** The header uses unsigned 32-bit row counts for every domain. */
     constexpr std::size_t kMaximumCount = (std::numeric_limits<std::uint32_t>::max)();
     return domains.named.size() <= kMaximumCount && domains.items.size() <= kMaximumCount
+           && domains.collectibles.size() <= kMaximumCount
+           && domains.materialRequirementSets.size() <= kMaximumCount
            && domains.itemDetails.size() <= kMaximumCount
+           && domains.socketPlugRules.size() <= kMaximumCount
+           && domains.socketPlugPools.size() <= kMaximumCount
+           && domains.socketPlugMembers.size() <= kMaximumCount
            && domains.inventoryBuckets.size() <= kMaximumCount
            && domains.socketEntryLists.size() <= kMaximumCount
            && domains.socketEntryTables.size() <= kMaximumCount

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

@@ -65,7 +65,13 @@ bool payload_checksum(records::Domains domains, std::uint64_t& checksum) noexcep
     checksum = records::checksum_value(records::kChecksumOffsetBasis, domains.constants);
     return checksum_domain<records::NamedRecord>(domains.named, checksum)
            && checksum_domain<records::ItemRecord>(domains.items, checksum)
+           && checksum_domain<records::CollectibleRecord>(domains.collectibles, checksum)
+           && checksum_domain<records::MaterialRequirementSetRecord>(
+               domains.materialRequirementSets, checksum)
            && checksum_domain<records::ItemDetailRecord>(domains.itemDetails, checksum)
+           && checksum_domain<records::SocketPlugRuleRecord>(domains.socketPlugRules, checksum)
+           && checksum_domain<records::SocketPlugPoolRecord>(domains.socketPlugPools, checksum)
+           && checksum_domain<records::SocketPlugMemberRecord>(domains.socketPlugMembers, checksum)
            && checksum_domain<records::InventoryBucketRecord>(domains.inventoryBuckets, checksum)
            && checksum_domain<records::SocketEntryListRecord>(domains.socketEntryLists, checksum)
            && checksum_domain<records::SocketEntryTableRecord>(domains.socketEntryTables, checksum)
@@ -82,7 +88,13 @@ bool payload_checksum(records::Domains domains, std::uint64_t& checksum) noexcep
 bool write_payload(HANDLE file, records::Domains domains) noexcept {
     return write_domain<records::NamedRecord>(file, domains.named)
            && write_domain<records::ItemRecord>(file, domains.items)
+           && write_domain<records::CollectibleRecord>(file, domains.collectibles)
+           && write_domain<records::MaterialRequirementSetRecord>(file,
+                                                                  domains.materialRequirementSets)
            && write_domain<records::ItemDetailRecord>(file, domains.itemDetails)
+           && write_domain<records::SocketPlugRuleRecord>(file, domains.socketPlugRules)
+           && write_domain<records::SocketPlugPoolRecord>(file, domains.socketPlugPools)
+           && write_domain<records::SocketPlugMemberRecord>(file, domains.socketPlugMembers)
            && write_domain<records::InventoryBucketRecord>(file, domains.inventoryBuckets)
            && write_domain<records::SocketEntryListRecord>(file, domains.socketEntryLists)
            && write_domain<records::SocketEntryTableRecord>(file, domains.socketEntryTables)

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

@@ -108,7 +108,12 @@ enum class WriteStatus {
         build.configuredEquipmentHash,
         static_cast<std::uint32_t>(domains.named.size()),
         static_cast<std::uint32_t>(domains.items.size()),
+        static_cast<std::uint32_t>(domains.collectibles.size()),
+        static_cast<std::uint32_t>(domains.materialRequirementSets.size()),
         static_cast<std::uint32_t>(domains.itemDetails.size()),
+        static_cast<std::uint32_t>(domains.socketPlugRules.size()),
+        static_cast<std::uint32_t>(domains.socketPlugPools.size()),
+        static_cast<std::uint32_t>(domains.socketPlugMembers.size()),
         static_cast<std::uint32_t>(domains.inventoryBuckets.size()),
         static_cast<std::uint32_t>(domains.socketEntryLists.size()),
         static_cast<std::uint32_t>(domains.socketEntryTables.size()),

+ 73 - 0
Sunrise/src/state/build_data/collectibles/collectible_build_data_runtime.cpp

@@ -0,0 +1,73 @@
+#include "../runtime.h"
+#include "../runtime/persistence/publication_transaction.h"
+#include "collectible_catalog.h"
+
+namespace sunrise::state::build_data {
+namespace {
+
+/** @return True when every available collectible link names a published item row. */
+[[nodiscard]] bool
+valid_publication(std::span<const collectibles::Definition> definitions) noexcept {
+    if (!item_definitions_ready() || !collectibles::valid(definitions)) {
+        return false;
+    }
+    const std::size_t itemCount = items::count();
+    for (const collectibles::Definition& definition : definitions) {
+        items::Definition item{};
+        if (definition.itemDefinitionIndex != collectibles::kUnavailableItemDefinitionIndex
+            && (definition.itemDefinitionIndex >= itemCount
+                || !items::find_index(definition.itemDefinitionIndex, item)
+                || item.definitionIndex != definition.itemDefinitionIndex)) {
+            return false;
+        }
+        for (std::size_t index = 0; index < definition.materialRequirementCount; ++index) {
+            const collectibles::MaterialRequirement& requirement =
+                definition.materialRequirements[index];
+            if (requirement.itemDefinitionIndex >= itemCount
+                || !items::find_index(requirement.itemDefinitionIndex, item)
+                || item.definitionIndex != requirement.itemDefinitionIndex) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+} // namespace
+
+/** @return True when the whole native collectible table is published. */
+bool collectible_definitions_ready() noexcept {
+    return collectibles::count() != 0;
+}
+
+/** Publishes one complete dense collectible-to-item table. */
+bool publish_collectible_definitions(
+    std::span<const collectibles::Definition> definitions) noexcept {
+    runtime::persistence::Transaction transaction;
+    return transaction.active() && valid_publication(definitions)
+           && transaction.finish(collectibles::replace(definitions), collectibles::clear);
+}
+
+/** Resolves the protocol's collectible ordinal to the installed item-table ordinal. */
+bool find_collectible_item_definition_index(std::uint16_t collectibleIndex,
+                                            std::uint16_t& itemDefinitionIndex) noexcept {
+    itemDefinitionIndex = collectibles::kUnavailableItemDefinitionIndex;
+    collectibles::Definition definition{};
+    if (!collectible_definitions_ready() || !collectibles::find(collectibleIndex, definition)
+        || definition.collectibleIndex != collectibleIndex
+        || definition.itemDefinitionIndex == collectibles::kUnavailableItemDefinitionIndex) {
+        return false;
+    }
+    itemDefinitionIndex = definition.itemDefinitionIndex;
+    return true;
+}
+
+/** Resolves one full collectible row, including its native material requirements. */
+bool find_collectible_definition(std::uint16_t collectibleIndex,
+                                 collectibles::Definition& definition) noexcept {
+    definition = {};
+    return collectible_definitions_ready() && collectibles::find(collectibleIndex, definition)
+           && definition.collectibleIndex == collectibleIndex;
+}
+
+} // namespace sunrise::state::build_data

+ 105 - 0
Sunrise/src/state/build_data/collectibles/collectible_catalog.cpp

@@ -0,0 +1,105 @@
+#include "collectible_catalog.h"
+
+#include <array>
+
+#include "../table.h"
+
+namespace sunrise::state::build_data::collectibles {
+namespace {
+
+Lock g_lock;
+Table<Definition, kDefinitionCapacity> g_definitions;
+
+} // namespace
+
+/** Clears the table while no reader can observe a partial replacement. */
+void clear() noexcept {
+    const Lock::Exclusive guard(g_lock);
+    g_definitions.clear();
+}
+
+/** Checks that the native indices cover one complete dense range, in any input order. */
+bool valid(std::span<const Definition> definitions) noexcept {
+    if (definitions.empty() || definitions.size() > kDefinitionCapacity) {
+        return false;
+    }
+    std::array<bool, kDefinitionCapacity> occupied{};
+    for (const Definition& definition : definitions) {
+        if (definition.collectibleIndex >= definitions.size()
+            || occupied[definition.collectibleIndex]
+            || definition.materialRequirementCount > definition.materialRequirements.size()) {
+            return false;
+        }
+        const bool hasRequirements = definition.materialRequirementCount != 0;
+        if (hasRequirements
+                != (definition.materialRequirementSetIndex
+                    != kUnavailableMaterialRequirementSetIndex)
+            || hasRequirements != (definition.materialRequirementSetHash != 0)) {
+            return false;
+        }
+        for (std::size_t index = 0; index < definition.materialRequirements.size(); ++index) {
+            const MaterialRequirement& requirement = definition.materialRequirements[index];
+            if (index < definition.materialRequirementCount) {
+                if (requirement.itemDefinitionIndex == kUnavailableItemDefinitionIndex
+                    || requirement.quantity == 0) {
+                    return false;
+                }
+                for (std::size_t prior = 0; prior < index; ++prior) {
+                    if (definition.materialRequirements[prior].itemDefinitionIndex
+                        == requirement.itemDefinitionIndex) {
+                        return false;
+                    }
+                }
+            } else if (requirement.itemDefinitionIndex != kUnavailableItemDefinitionIndex
+                       || requirement.quantity != 0 || requirement.deleteOnAction
+                       || requirement.omitFromRequirements) {
+                return false;
+            }
+        }
+        occupied[definition.collectibleIndex] = true;
+    }
+    return true;
+}
+
+/** Places every validated row at the native index the request protocol uses. */
+bool replace(std::span<const Definition> definitions) noexcept {
+    if (!valid(definitions)) {
+        return false;
+    }
+    const Lock::Exclusive guard(g_lock);
+    const std::span<Definition> storage = g_definitions.reset(definitions.size());
+    if (storage.size() != definitions.size()) {
+        return false;
+    }
+    for (const Definition& definition : definitions) {
+        storage[definition.collectibleIndex] = definition;
+    }
+    return true;
+}
+
+/** Finds a row only when it is inside the published dense table. */
+bool find(std::uint16_t collectibleIndex, Definition& definition) noexcept {
+    definition = {};
+    definition.itemDefinitionIndex = kUnavailableItemDefinitionIndex;
+    const Lock::Shared guard(g_lock);
+    const std::span<const Definition> rows = g_definitions.rows();
+    const bool found = static_cast<std::size_t>(collectibleIndex) < rows.size();
+    if (found) {
+        definition = rows[collectibleIndex];
+    }
+    return found;
+}
+
+/** Copies the dense rows without exposing catalog storage. */
+bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_definitions.snapshot(output, count);
+}
+
+/** @return Number of published rows, read under the catalog lock. */
+std::size_t count() noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_definitions.count();
+}
+
+} // namespace sunrise::state::build_data::collectibles

+ 56 - 0
Sunrise/src/state/build_data/collectibles/collectible_catalog.h

@@ -0,0 +1,56 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+namespace sunrise::state::build_data::collectibles {
+
+/** The Collections protocol carries a present bit followed by a 15-bit native row index. */
+inline constexpr std::size_t kDefinitionCapacity = 1U << 15U;
+/** Some collectible rows deliberately do not resolve to an inventory item. */
+inline constexpr std::uint16_t kUnavailableItemDefinitionIndex = 0xFFFFU;
+/** A collectible with no acquisition charge carries this native requirement-set sentinel. */
+inline constexpr std::uint16_t kUnavailableMaterialRequirementSetIndex = 0xFFFFU;
+/** Installed requirement sets contain at most six material rows. */
+inline constexpr std::size_t kMaterialRequirementCapacity = 6;
+
+/** One native material row attached to a Collections acquisition. */
+struct MaterialRequirement {
+    std::uint32_t quantity{};
+    std::uint16_t itemDefinitionIndex{kUnavailableItemDefinitionIndex};
+    bool deleteOnAction{};
+    bool omitFromRequirements{};
+};
+
+/** One installed-build collectible row and the item-definition row it grants. */
+struct Definition {
+    std::uint32_t collectibleHash{};
+    std::uint32_t materialRequirementSetHash{};
+    std::uint16_t collectibleIndex{};
+    std::uint16_t itemDefinitionIndex{kUnavailableItemDefinitionIndex};
+    std::uint16_t materialRequirementSetIndex{kUnavailableMaterialRequirementSetIndex};
+    std::uint8_t materialRequirementCount{};
+    std::array<MaterialRequirement, kMaterialRequirementCapacity> materialRequirements{};
+};
+
+/** Clears every generated collectible mapping. */
+void clear() noexcept;
+
+/** @return True when every native collectible index appears exactly once. */
+[[nodiscard]] bool valid(std::span<const Definition> definitions) noexcept;
+
+/** Replaces the complete dense collectible table in one publication. */
+[[nodiscard]] bool replace(std::span<const Definition> definitions) noexcept;
+
+/** Finds one collectible by the native 15-bit index carried by the request. */
+[[nodiscard]] bool find(std::uint16_t collectibleIndex, Definition& definition) noexcept;
+
+/** Copies every row in native collectible-index order. */
+[[nodiscard]] bool snapshot(std::span<Definition> output, std::size_t& count) noexcept;
+
+/** @return Number of installed-build collectible mappings. */
+[[nodiscard]] std::size_t count() noexcept;
+
+} // namespace sunrise::state::build_data::collectibles

+ 8 - 5
Sunrise/src/state/build_data/items/details/definition.h

@@ -8,11 +8,14 @@
 namespace sunrise::state::build_data::items::details {
 
 /**
- * Configured equipment plus every plug those items socket. 16 slots on 3 characters is 48 items,
- * and each item sockets up to 12 plugs whose own details the character record needs. The size
- * covers both sets with duplicates removed.
+ * Installed item definitions used by Collections, character instances, profile stacks, and
+ *
+ * their native initial plugs. The
+ * supported installed build carries 15,424 total item rows, so
+ * the next power of two is also a
+ * formal upper bound for the deduplicated detail closure.
  */
-inline constexpr std::size_t kDefinitionCapacity = 768;
+inline constexpr std::size_t kDefinitionCapacity = 16384;
 /** Family item instances have 12 fixed ordinary socket lanes. */
 inline constexpr std::size_t kInitialPlugCapacity = 12;
 /** Native equipment ids run from 0 to 19. */
@@ -80,7 +83,7 @@ unavailable_plug_indices() noexcept {
     return result;
 }
 
-/** Installed-build fields required to generate one configured item instance. */
+/** Installed-build fields required to generate one supported item instance. */
 struct Definition {
     std::uint16_t definitionIndex{};
     /** The definition's own hash, which the character record collects for its overflow bank. */

+ 25 - 8
Sunrise/src/state/build_data/items/details/item_detail_catalog.cpp

@@ -4,6 +4,8 @@
 #include <array>
 #include <bitset>
 #include <limits>
+#include <memory>
+#include <new>
 #include <span>
 
 #include "../../table.h"
@@ -19,7 +21,8 @@ constexpr std::size_t kNativeDefinitionIndexCapacity =
 constexpr std::uint16_t kEmptyLookupRow = (std::numeric_limits<std::uint16_t>::max)();
 
 Lock g_lock;
-Table<Definition, kDefinitionCapacity> g_definitions;
+std::unique_ptr<Definition[]> g_definitions;
+std::size_t g_definitionCount{};
 // Native definition index to detail row, rebuilt with the table under the same exclusive hold.
 std::array<std::uint16_t, kNativeDefinitionIndexCapacity> g_lookup{};
 
@@ -82,7 +85,8 @@ static_assert(kDefinitionCapacity < kEmptyLookupRow);
 /** Clears every generated configured item detail under the catalog lock. */
 void clear() noexcept {
     const Lock::Exclusive guard(g_lock);
-    g_definitions.clear();
+    g_definitions.reset();
+    g_definitionCount = 0;
     std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
 }
 
@@ -108,14 +112,19 @@ bool replace(std::span<const Definition> definitions) noexcept {
         return false;
     }
 
-    const Lock::Exclusive guard(g_lock);
-    std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
-    if (!g_definitions.replace(definitions)) {
+    std::unique_ptr<Definition[]> staged{new (std::nothrow) Definition[definitions.size()]};
+    if (!staged) {
         return false;
     }
+    std::copy(definitions.begin(), definitions.end(), staged.get());
+
+    const Lock::Exclusive guard(g_lock);
+    std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
     for (std::size_t index = 0; index < definitions.size(); ++index) {
         g_lookup[definitions[index].definitionIndex] = static_cast<std::uint16_t>(index);
     }
+    g_definitions = std::move(staged);
+    g_definitionCount = definitions.size();
     return true;
 }
 
@@ -123,7 +132,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
 bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
     definition = {};
     const Lock::Shared guard(g_lock);
-    const std::span<const Definition> rows = g_definitions.rows();
+    const std::span<const Definition> rows{g_definitions.get(), g_definitionCount};
     const std::uint16_t row = g_lookup[definitionIndex];
     const bool found = row != kEmptyLookupRow && row < rows.size();
     if (found) {
@@ -135,13 +144,21 @@ bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
 /** Copies details in publication order, without exposing the catalog storage. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
     const Lock::Shared guard(g_lock);
-    return g_definitions.snapshot(output, count);
+    count = 0;
+    if (output.size() < g_definitionCount) {
+        return false;
+    }
+    if (g_definitionCount != 0) {
+        std::copy_n(g_definitions.get(), g_definitionCount, output.begin());
+    }
+    count = g_definitionCount;
+    return true;
 }
 
 /** @return Number of configured item details, read under the lock. */
 std::size_t count() noexcept {
     const Lock::Shared guard(g_lock);
-    return g_definitions.count();
+    return g_definitionCount;
 }
 
 } // namespace sunrise::state::build_data::items::details

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

@@ -29,4 +29,12 @@ bool find_item_definition_hash(std::uint32_t definitionHash,
     return item_definitions_ready() && items::find_hash(definitionHash, definition);
 }
 
+/** Finds one installed item by the native index carried by a Collections request. */
+bool find_item_definition_index(std::uint16_t definitionIndex,
+                                items::Definition& definition) noexcept {
+    definition = {};
+    return item_definitions_ready() && items::find_index(definitionIndex, definition)
+           && definition.definitionIndex == definitionIndex;
+}
+
 } // namespace sunrise::state::build_data

+ 4 - 0
Sunrise/src/state/build_data/items/item_catalog.h

@@ -10,12 +10,16 @@ namespace sunrise::state::build_data::items {
 inline constexpr std::size_t kDefinitionCapacity = 32768;
 /** All bucket bits set mark a valid item row whose inventory bucket was not found. */
 inline constexpr std::uint8_t kUnresolvedBucketId = 0xFF;
+/** A plug with no authored insertion/enabled price carries all set-index bits. */
+inline constexpr std::uint16_t kUnavailableMaterialRequirementSetIndex = 0xFFFFU;
 
 /** One installed-build item identity, used to look up authored definition hashes. */
 struct Definition {
     std::uint32_t definitionHash{};
     std::uint16_t definitionIndex{};
     std::uint8_t bucketId{kUnresolvedBucketId};
+    std::uint16_t insertionMaterialRequirementSetIndex{kUnavailableMaterialRequirementSetIndex};
+    std::uint16_t enabledMaterialRequirementSetIndex{kUnavailableMaterialRequirementSetIndex};
 };
 
 /** Clears every generated item mapping. */

+ 39 - 0
Sunrise/src/state/build_data/items/socket_plugs/definition.h

@@ -0,0 +1,39 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "../details/definition.h"
+
+namespace sunrise::state::build_data::items::socket_plugs {
+
+/** Ordinary item instances expose at most 12 socket lanes. */
+inline constexpr std::size_t kLaneCapacity = details::kInitialPlugCapacity;
+/** At most one exact pool rule is retained for each installed item and ordinary socket lane. */
+inline constexpr std::size_t kRuleCapacity = details::kDefinitionCapacity * kLaneCapacity;
+/** Pool zero is the shared empty pool, in addition to at most one unique pool per rule. */
+inline constexpr std::size_t kPoolCapacity = kRuleCapacity + 1;
+/** Four million 16-bit members bound the deduplicated installed-build relation to 8 MiB. */
+inline constexpr std::size_t kMemberCapacity = 1U << 22U;
+/** Pool zero is always the canonical empty pool. */
+inline constexpr std::uint32_t kEmptyPoolIndex = 0;
+
+/** One installed item socket and the exact deduplicated plug pool it accepts. */
+struct Rule {
+    std::uint16_t itemDefinitionIndex{};
+    std::uint8_t lane{};
+    /** Must remain zero so the runtime and packed forms are deterministic. */
+    std::uint8_t reserved{};
+    std::uint32_t poolIndex{};
+};
+
+/** One contiguous range in the flat, sorted plug-definition index bank. */
+struct Pool {
+    std::uint32_t memberOffset{};
+    std::uint32_t memberCount{};
+};
+
+/** Native item-definition index of one allowed plug. */
+using Member = std::uint16_t;
+
+} // namespace sunrise::state::build_data::items::socket_plugs

+ 79 - 0
Sunrise/src/state/build_data/items/socket_plugs/socket_plug_build_data_runtime.cpp

@@ -0,0 +1,79 @@
+#include "../../runtime.h"
+#include "../../runtime/persistence/publication_transaction.h"
+#include "../details/item_detail_catalog.h"
+#include "../item_catalog.h"
+#include "socket_plug_catalog.h"
+
+namespace sunrise::state::build_data {
+namespace {
+
+/** Checks every exact socket relation link against the already-published item details. */
+[[nodiscard]] bool
+valid_socket_plug_publication(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 (!items::socket_plugs::valid(rules, pools, members) || items::count() == 0
+        || !configured_item_details_ready()) {
+        return false;
+    }
+    for (const items::socket_plugs::Rule& rule : rules) {
+        items::details::Definition detail{};
+        if (rule.itemDefinitionIndex >= items::count()
+            || !items::details::find(rule.itemDefinitionIndex, detail)
+            || rule.lane >= detail.ordinarySocketCount) {
+            return false;
+        }
+    }
+    for (const items::socket_plugs::Member member : members) {
+        if (member >= items::count()) {
+            return false;
+        }
+    }
+    return true;
+}
+
+} // namespace
+
+/** @return True when the installed exact ordinary-socket relation is in State. */
+bool socket_plug_rules_ready() noexcept {
+    return items::socket_plugs::rule_count() != 0;
+}
+
+/** Publishes the complete exact socket relation in one persistence transaction. */
+bool publish_socket_plug_rules(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 {
+    runtime::persistence::Transaction transaction;
+    if (!transaction.active() || !valid_socket_plug_publication(rules, pools, members)) {
+        return false;
+    }
+    return transaction.finish(items::socket_plugs::replace(rules, pools, members),
+                              items::socket_plugs::clear);
+}
+
+/** Answers one exact installed item/lane/plug compatibility query. */
+bool is_socket_plug_allowed(std::uint16_t itemDefinitionIndex,
+                            std::uint8_t lane,
+                            std::uint16_t plugDefinitionIndex) noexcept {
+    return socket_plug_rules_ready()
+           && items::socket_plugs::allowed(itemDefinitionIndex, lane, plugDefinitionIndex);
+}
+
+/** Answers whether one installed profile row is a materializable socket action source. */
+bool is_profile_action_source(std::uint16_t itemDefinitionIndex, std::uint8_t bucketId) noexcept {
+    constexpr std::uint8_t kModBucketId = 13;
+    constexpr std::uint8_t kShaderBucketId = 14;
+    items::details::Definition detail{};
+    inventory::buckets::Descriptor bucket{};
+    if ((bucketId != kModBucketId && bucketId != kShaderBucketId) || !socket_plug_rules_ready()
+        || !find_configured_item_detail(itemDefinitionIndex, detail)
+        || detail.definitionIndex != itemDefinitionIndex || detail.bucketId != bucketId
+        || !find_inventory_bucket_descriptor(bucketId, bucket)
+        || bucket.arraySelector != inventory::buckets::ArraySelector::profile) {
+        return false;
+    }
+    return detail.instancedDefinitionState == items::details::InstancedDefinitionState::stackable
+           && items::socket_plugs::contains(itemDefinitionIndex);
+}
+
+} // namespace sunrise::state::build_data

+ 129 - 0
Sunrise/src/state/build_data/items/socket_plugs/socket_plug_catalog.cpp

@@ -0,0 +1,129 @@
+#include "socket_plug_catalog.h"
+
+#include <algorithm>
+
+#include "../../table.h"
+
+namespace sunrise::state::build_data::items::socket_plugs {
+namespace {
+
+Lock g_lock;
+Table<Rule, kRuleCapacity> g_rules;
+Table<Pool, kPoolCapacity> g_pools;
+Table<Member, kMemberCapacity> g_members;
+
+/** @return True when the first rule's item/lane key is strictly before the second. */
+[[nodiscard]] bool rule_less(const Rule& left, const Rule& right) noexcept {
+    return left.itemDefinitionIndex < right.itemDefinitionIndex
+           || (left.itemDefinitionIndex == right.itemDefinitionIndex && left.lane < right.lane);
+}
+
+} // namespace
+
+/** Clears the complete socket-plug catalog under one exclusive hold. */
+void clear() noexcept {
+    const Lock::Exclusive guard(g_lock);
+    g_rules.clear();
+    g_pools.clear();
+    g_members.clear();
+}
+
+/** Checks counts, strict rule order, contiguous pools, and sorted unique pool members. */
+bool valid(std::span<const Rule> rules,
+           std::span<const Pool> pools,
+           std::span<const Member> members) noexcept {
+    if (rules.empty() || rules.size() > kRuleCapacity || pools.empty()
+        || pools.size() > kPoolCapacity || members.size() > kMemberCapacity
+        || pools.front().memberOffset != 0 || pools.front().memberCount != 0) {
+        return false;
+    }
+    for (std::size_t index = 0; index < rules.size(); ++index) {
+        const Rule& rule = rules[index];
+        if (rule.reserved != 0 || rule.lane >= kLaneCapacity || rule.poolIndex >= pools.size()
+            || (index != 0 && !rule_less(rules[index - 1], rule))) {
+            return false;
+        }
+    }
+    std::size_t expectedOffset = 0;
+    for (std::size_t index = 0; index < pools.size(); ++index) {
+        const Pool& pool = pools[index];
+        if (pool.memberOffset != expectedOffset || (index != 0 && pool.memberCount == 0)
+            || pool.memberCount > members.size() - expectedOffset) {
+            return false;
+        }
+        const auto range = members.subspan(expectedOffset, pool.memberCount);
+        if (!std::is_sorted(range.begin(), range.end())
+            || std::adjacent_find(range.begin(), range.end()) != range.end()) {
+            return false;
+        }
+        expectedOffset += pool.memberCount;
+    }
+    return expectedOffset == members.size();
+}
+
+/** Replaces all three arrays while no reader can observe a partial relation. */
+bool replace(std::span<const Rule> rules,
+             std::span<const Pool> pools,
+             std::span<const Member> members) noexcept {
+    if (!valid(rules, pools, members)) {
+        return false;
+    }
+    const Lock::Exclusive guard(g_lock);
+    return g_rules.replace(rules) && g_pools.replace(pools) && g_members.replace(members);
+}
+
+/** Performs an exact item/lane rule lookup followed by a binary search in its plug pool. */
+bool allowed(std::uint16_t itemDefinitionIndex,
+             std::uint8_t lane,
+             std::uint16_t plugDefinitionIndex) noexcept {
+    if (lane >= kLaneCapacity) {
+        return false;
+    }
+    const Lock::Shared guard(g_lock);
+    const auto rules = g_rules.rows();
+    const auto pools = g_pools.rows();
+    const auto members = g_members.rows();
+    const Rule key{itemDefinitionIndex, lane, 0, 0};
+    const auto found = std::lower_bound(rules.begin(), rules.end(), key, rule_less);
+    if (found == rules.end() || found->itemDefinitionIndex != itemDefinitionIndex
+        || found->lane != lane || found->poolIndex >= pools.size()) {
+        return false;
+    }
+    const Pool& pool = pools[found->poolIndex];
+    if (pool.memberOffset > members.size()
+        || pool.memberCount > members.size() - pool.memberOffset) {
+        return false;
+    }
+    const auto range = members.subspan(pool.memberOffset, pool.memberCount);
+    return std::binary_search(range.begin(), range.end(), plugDefinitionIndex);
+}
+
+/** Answers whether one definition occurs in any installed ordinary-socket plug pool. */
+bool contains(Member plugDefinitionIndex) noexcept {
+    const Lock::Shared guard(g_lock);
+    const auto members = g_members.rows();
+    return std::find(members.begin(), members.end(), plugDefinitionIndex) != members.end();
+}
+
+/** Copies the three related arrays under the same shared hold. */
+bool snapshot(std::span<Rule> rules,
+              std::size_t& ruleCount,
+              std::span<Pool> pools,
+              std::size_t& poolCount,
+              std::span<Member> members,
+              std::size_t& memberCount) noexcept {
+    ruleCount = 0;
+    poolCount = 0;
+    memberCount = 0;
+    const Lock::Shared guard(g_lock);
+    return g_rules.snapshot(rules, ruleCount) && g_pools.snapshot(pools, poolCount)
+           && g_members.snapshot(members, memberCount);
+}
+
+/** Reports the published rule count under the catalog lock. */
+std::size_t rule_count() noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_rules.count();
+}
+
+} // namespace sunrise::state::build_data::items::socket_plugs

+ 55 - 0
Sunrise/src/state/build_data/items/socket_plugs/socket_plug_catalog.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+#include "definition.h"
+
+namespace sunrise::state::build_data::items::socket_plugs {
+
+/** Clears every exact ordinary-socket rule, pool, and member under one catalog lock. */
+void clear() noexcept;
+
+/**
+ * Checks the complete three-array relation independently of other build-data domains.
+ * @param rules Strictly item/lane-ordered socket rules.
+ * @param pools Contiguous member ranges, beginning with the canonical empty pool.
+ * @param members Sorted unique members inside each pool range.
+ * @return True when every count, key, range, and pool reference is canonical.
+ */
+[[nodiscard]] bool valid(std::span<const Rule> rules,
+                         std::span<const Pool> pools,
+                         std::span<const Member> members) noexcept;
+
+/** Replaces the complete relation atomically after validating all three arrays. */
+[[nodiscard]] bool replace(std::span<const Rule> rules,
+                           std::span<const Pool> pools,
+                           std::span<const Member> members) noexcept;
+
+/**
+ * Answers whether one exact plug is allowed in one installed item's ordinary socket lane.
+ * Missing items and lanes fail closed.
+ */
+[[nodiscard]] bool allowed(std::uint16_t itemDefinitionIndex,
+                           std::uint8_t lane,
+                           std::uint16_t plugDefinitionIndex) noexcept;
+
+/**
+ * Answers whether one definition occurs in any installed ordinary-socket plug pool.
+ * The flat pool bank is small enough that this boot/acquisition query remains bounded.
+ */
+[[nodiscard]] bool contains(Member plugDefinitionIndex) noexcept;
+
+/** Copies the complete relation while holding its single shared lock. */
+[[nodiscard]] bool snapshot(std::span<Rule> rules,
+                            std::size_t& ruleCount,
+                            std::span<Pool> pools,
+                            std::size_t& poolCount,
+                            std::span<Member> members,
+                            std::size_t& memberCount) noexcept;
+
+/** @return Published socket-rule row count. */
+[[nodiscard]] std::size_t rule_count() noexcept;
+
+} // namespace sunrise::state::build_data::items::socket_plugs

+ 40 - 0
Sunrise/src/state/build_data/material_requirements/material_requirement_build_data_runtime.cpp

@@ -0,0 +1,40 @@
+#include "../runtime.h"
+#include "../runtime/persistence/publication_transaction.h"
+#include "material_requirement_catalog.h"
+
+namespace sunrise::state::build_data {
+namespace {
+
+[[nodiscard]] bool valid_material_publication(
+    std::span<const material_requirements::Definition> definitions) noexcept {
+    // The package extractor has already checked every native material index against the exact
+    // dense item-table count used to publish this catalog. Re-probing the live item lookup here
+    // made publication depend on a second catalog boundary even though no stronger invariant was
+    // established. Keep this publication gate structural; the complete cache validator repeats
+    // the cross-domain bounds check before anything is persisted or restored.
+    return item_definitions_ready() && material_requirements::valid(definitions);
+}
+
+} // namespace
+
+bool material_requirement_sets_ready() noexcept {
+    return material_requirements::count() != 0;
+}
+
+bool publish_material_requirement_sets(
+    std::span<const material_requirements::Definition> definitions) noexcept {
+    runtime::persistence::Transaction transaction;
+    return transaction.active() && valid_material_publication(definitions)
+           && transaction.finish(material_requirements::replace(definitions),
+                                 material_requirements::clear);
+}
+
+bool find_material_requirement_set(std::uint16_t requirementSetIndex,
+                                   material_requirements::Definition& definition) noexcept {
+    definition = {};
+    return material_requirement_sets_ready()
+           && material_requirements::find(requirementSetIndex, definition)
+           && definition.requirementSetIndex == requirementSetIndex;
+}
+
+} // namespace sunrise::state::build_data

+ 92 - 0
Sunrise/src/state/build_data/material_requirements/material_requirement_catalog.cpp

@@ -0,0 +1,92 @@
+#include "material_requirement_catalog.h"
+
+#include <array>
+
+#include "../table.h"
+
+namespace sunrise::state::build_data::material_requirements {
+namespace {
+
+Lock g_lock;
+Table<Definition, kDefinitionCapacity> g_definitions;
+
+} // namespace
+
+void clear() noexcept {
+    const Lock::Exclusive guard(g_lock);
+    g_definitions.clear();
+}
+
+bool valid(std::span<const Definition> definitions) noexcept {
+    if (definitions.empty() || definitions.size() > kDefinitionCapacity) {
+        return false;
+    }
+    std::array<bool, kDefinitionCapacity> occupied{};
+    for (const Definition& definition : definitions) {
+        if (definition.requirementSetHash == 0
+            || definition.requirementSetIndex >= definitions.size()
+            || occupied[definition.requirementSetIndex]
+            || definition.requirementCount > definition.requirements.size()) {
+            return false;
+        }
+        for (std::size_t index = 0; index < definition.requirements.size(); ++index) {
+            const Requirement& requirement = definition.requirements[index];
+            if (index < definition.requirementCount) {
+                if (requirement.itemDefinitionIndex == kUnavailableItemDefinitionIndex) {
+                    return false;
+                }
+                for (std::size_t prior = 0; prior < index; ++prior) {
+                    if (definition.requirements[prior].itemDefinitionIndex
+                        == requirement.itemDefinitionIndex) {
+                        return false;
+                    }
+                }
+            } else if (requirement.itemDefinitionIndex != kUnavailableItemDefinitionIndex
+                       || requirement.quantity != 0
+                       || requirement.condition != kUnconditionalRequirement
+                       || requirement.deleteOnAction || requirement.omitFromRequirements) {
+                return false;
+            }
+        }
+        occupied[definition.requirementSetIndex] = true;
+    }
+    return true;
+}
+
+bool replace(std::span<const Definition> definitions) noexcept {
+    if (!valid(definitions)) {
+        return false;
+    }
+    const Lock::Exclusive guard(g_lock);
+    const std::span<Definition> storage = g_definitions.reset(definitions.size());
+    if (storage.size() != definitions.size()) {
+        return false;
+    }
+    for (const Definition& definition : definitions) {
+        storage[definition.requirementSetIndex] = definition;
+    }
+    return true;
+}
+
+bool find(std::uint16_t requirementSetIndex, Definition& definition) noexcept {
+    definition = {};
+    const Lock::Shared guard(g_lock);
+    const std::span<const Definition> rows = g_definitions.rows();
+    const bool found = static_cast<std::size_t>(requirementSetIndex) < rows.size();
+    if (found) {
+        definition = rows[requirementSetIndex];
+    }
+    return found;
+}
+
+bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_definitions.snapshot(output, count);
+}
+
+std::size_t count() noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_definitions.count();
+}
+
+} // namespace sunrise::state::build_data::material_requirements

+ 45 - 0
Sunrise/src/state/build_data/material_requirements/material_requirement_catalog.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+namespace sunrise::state::build_data::material_requirements {
+
+/** Installed build currently carries 229 dense sets; keep bounded headroom. */
+inline constexpr std::size_t kDefinitionCapacity = 512;
+/** Widest installed material-requirement set contains six rows. */
+inline constexpr std::size_t kRequirementCapacity = 6;
+/** All item-index bits set mean the row does not name an installed item. */
+inline constexpr std::uint16_t kUnavailableItemDefinitionIndex = 0xFFFFU;
+/** All requirement-index bits set mean an item/action declares no requirement set. */
+inline constexpr std::uint16_t kUnavailableSetIndex = 0xFFFFU;
+/** All condition bits set mark a requirement row that applies without a native variant gate. */
+inline constexpr std::uint16_t kUnconditionalRequirement = 0xFFFFU;
+
+/** One exact native material row. Quantity zero is authored by free cosmetic requirements. */
+struct Requirement {
+    std::uint32_t quantity{};
+    std::uint16_t itemDefinitionIndex{kUnavailableItemDefinitionIndex};
+    std::uint16_t condition{kUnconditionalRequirement};
+    bool deleteOnAction{};
+    bool omitFromRequirements{};
+};
+
+/** One dense native requirement set, addressed by its installed ordinal. Count zero is free. */
+struct Definition {
+    std::uint32_t requirementSetHash{};
+    std::uint16_t requirementSetIndex{kUnavailableSetIndex};
+    std::uint8_t requirementCount{};
+    std::array<Requirement, kRequirementCapacity> requirements{};
+};
+
+void clear() noexcept;
+[[nodiscard]] bool valid(std::span<const Definition> definitions) noexcept;
+[[nodiscard]] bool replace(std::span<const Definition> definitions) noexcept;
+[[nodiscard]] bool find(std::uint16_t requirementSetIndex, Definition& definition) noexcept;
+[[nodiscard]] bool snapshot(std::span<Definition> output, std::size_t& count) noexcept;
+[[nodiscard]] std::size_t count() noexcept;
+
+} // namespace sunrise::state::build_data::material_requirements

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

@@ -6,12 +6,15 @@
 #include <string_view>
 
 #include "abilities/definition.h"
+#include "collectibles/collectible_catalog.h"
 #include "constants/definition.h"
 #include "definition.h"
 #include "hash_names/definition.h"
 #include "inventory/buckets/definition.h"
 #include "items/details/definition.h"
 #include "items/item_catalog.h"
+#include "items/socket_plugs/definition.h"
+#include "material_requirements/material_requirement_catalog.h"
 #include "progressions/definition.h"
 #include "scenarios/definition.h"
 #include "socket_entry_lists/definition.h"
@@ -79,6 +82,56 @@ publish_item_definitions(std::span<const items::Definition> definitions) noexcep
 [[nodiscard]] bool find_item_definition_hash(std::uint32_t definitionHash,
                                              items::Definition& definition) noexcept;
 
+/**
+ * Finds one installed item by the native dense definition index used by Collections requests.
+
+ * * @param definitionIndex Native item-definition row index.
+ * @param definition Receives the
+ * exact installed mapping.
+ * @return True when the complete table is ready and contains the
+ * requested row.
+ */
+[[nodiscard]] bool find_item_definition_index(std::uint16_t definitionIndex,
+                                              items::Definition& definition) noexcept;
+
+/** @return True when the whole dense collectible definition table is in State. */
+[[nodiscard]] bool collectible_definitions_ready() noexcept;
+
+/**
+ * Publishes the installed collectible ordinal-to-item table in one step.
+ * @param definitions Complete dense rows extracted from the investment root's collectible table.
+ * @return True when the rows pass the checks and any needed cache write succeeds.
+ */
+[[nodiscard]] bool
+publish_collectible_definitions(std::span<const collectibles::Definition> definitions) noexcept;
+
+/**
+ * Resolves the native 15-bit collectible index carried by a Collections acquire request.
+ * @param collectibleIndex Native collectible row ordinal.
+ * @param itemDefinitionIndex Receives the installed item-definition row, or the unavailable
+ * sentinel on failure.
+ * @return True when both the complete table and an item link exist for this collectible.
+ */
+[[nodiscard]] bool
+find_collectible_item_definition_index(std::uint16_t collectibleIndex,
+                                       std::uint16_t& itemDefinitionIndex) noexcept;
+
+/** Finds one complete installed collectible row, including its acquisition material set. */
+[[nodiscard]] bool find_collectible_definition(std::uint16_t collectibleIndex,
+                                               collectibles::Definition& definition) noexcept;
+
+/** @return True when every installed material-requirement set is available by native ordinal. */
+[[nodiscard]] bool material_requirement_sets_ready() noexcept;
+
+/** Publishes the complete dense native material-requirement table. */
+[[nodiscard]] bool publish_material_requirement_sets(
+    std::span<const material_requirements::Definition> definitions) noexcept;
+
+/** Resolves one authored material-requirement set without embedding any prices in code. */
+[[nodiscard]] bool
+find_material_requirement_set(std::uint16_t requirementSetIndex,
+                              material_requirements::Definition& definition) noexcept;
+
 /** @return True when a complete configured-detail domain, empty or not, is published. */
 [[nodiscard]] bool configured_item_details_ready() noexcept;
 
@@ -101,6 +154,37 @@ publish_configured_item_details(std::span<const items::details::Definition> defi
 [[nodiscard]] bool find_configured_item_detail(std::uint16_t definitionIndex,
                                                items::details::Definition& definition) noexcept;
 
+/** @return True when the exact installed ordinary-socket plug relation is in State. */
+[[nodiscard]] bool socket_plug_rules_ready() noexcept;
+
+/**
+ * Publishes exact per-item, per-lane plug pools extracted from the installed packages.
+ * @param rules Strictly item/lane-ordered rules.
+ * @param pools Deduplicated contiguous pool ranges, beginning with the empty pool.
+ * @param members Flat sorted plug-definition indices.
+ * @return True when the relation and its item/detail links validate and any cache write succeeds.
+ */
+[[nodiscard]] bool
+publish_socket_plug_rules(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;
+
+/**
+ * Answers whether one installed plug definition is valid for one exact ordinary socket lane.
+ * Missing or malformed relations fail closed.
+ */
+[[nodiscard]] bool is_socket_plug_allowed(std::uint16_t itemDefinitionIndex,
+                                          std::uint8_t lane,
+                                          std::uint16_t plugDefinitionIndex) noexcept;
+
+/**
+ * Answers whether one profile definition needs an item-instance resident so the native socket
+ * action route can materialize it. Only stackable installed socket plugs in the supported mod and
+ * shader profile buckets qualify; currency/material/intrinsic rows do not.
+ */
+[[nodiscard]] bool is_profile_action_source(std::uint16_t itemDefinitionIndex,
+                                            std::uint8_t bucketId) noexcept;
+
 /** @return True when the whole progression definition table is in State. */
 [[nodiscard]] bool progression_definitions_ready() noexcept;
 

+ 6 - 0
Sunrise/src/state/build_data/runtime/build_data_catalog_runtime.cpp

@@ -2,10 +2,13 @@
 
 #include "../../content/content_catalog.h"
 #include "../abilities/ability_bucket_catalog.h"
+#include "../collectibles/collectible_catalog.h"
 #include "../constants/investment_constant_catalog.h"
 #include "../hash_names/hash_name_catalog.h"
 #include "../inventory/buckets/inventory_bucket_catalog.h"
 #include "../items/details/item_detail_catalog.h"
+#include "../items/socket_plugs/socket_plug_catalog.h"
+#include "../material_requirements/material_requirement_catalog.h"
 #include "../progressions/progression_catalog.h"
 #include "../runtime.h"
 #include "../scenarios/scenario_catalog.h"
@@ -267,8 +270,11 @@ void clear_catalogs() noexcept {
     content::clear();
     named::clear();
     items::clear();
+    collectibles::clear();
+    material_requirements::clear();
     items::details::clear();
     details::clear();
+    items::socket_plugs::clear();
     inventory::buckets::clear();
     socket_entry_lists::clear();
     rollback_ability_publication();

+ 143 - 29
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp

@@ -3,16 +3,20 @@
 #include <Windows.h>
 
 #include <algorithm>
+#include <new>
 #include <span>
 
 #include "../../../../core/ui/busy/busy.h"
 #include "../../abilities/ability_bucket_catalog.h"
 #include "../../cache/internal.h"
 #include "../../cache/records/validation.h"
+#include "../../collectibles/collectible_catalog.h"
 #include "../../constants/investment_constant_catalog.h"
 #include "../../hash_names/hash_name_catalog.h"
 #include "../../inventory/buckets/inventory_bucket_catalog.h"
 #include "../../items/details/item_detail_catalog.h"
+#include "../../items/socket_plugs/socket_plug_catalog.h"
+#include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/progression_catalog.h"
 #include "../../runtime.h"
 #include "../../scenarios/scenario_catalog.h"
@@ -26,6 +30,15 @@ namespace {
 
 Context g_context;
 
+/** Lazily allocates one bounded cache-snapshot bank and exposes all rows on success. */
+template <typename Value, std::size_t Capacity>
+[[nodiscard]] std::span<Value> ensure_scratch(std::unique_ptr<Value[]>& storage) noexcept {
+    if (!storage) {
+        storage.reset(new (std::nothrow) Value[Capacity]);
+    }
+    return {storage.get(), storage ? Capacity : 0};
+}
+
 /** @param value Published runtime constants. @return The packed header form. */
 [[nodiscard]] cache::records::InvestmentConstants
 to_record(const constants::InvestmentConstants& value) noexcept {
@@ -47,7 +60,16 @@ to_record(const constants::InvestmentConstants& value) noexcept {
     *scratch.constants = to_record(constants::snapshot());
     return content::snapshot(scratch.named, counts.named)
            && items::snapshot(scratch.items, counts.items)
+           && collectibles::snapshot(scratch.collectibles, counts.collectibles)
+           && material_requirements::snapshot(scratch.materialRequirementSets,
+                                              counts.materialRequirementSets)
            && items::details::snapshot(scratch.itemDetails, counts.itemDetails)
+           && items::socket_plugs::snapshot(scratch.socketPlugRules,
+                                            counts.socketPlugRules,
+                                            scratch.socketPlugPools,
+                                            counts.socketPlugPools,
+                                            scratch.socketPlugMembers,
+                                            counts.socketPlugMembers)
            && inventory::buckets::snapshot(scratch.inventoryBuckets, counts.inventoryBuckets)
            && socket_entry_lists::snapshot(scratch.socketEntryLists, counts.socketEntryLists)
            && socket_entry_lists::snapshot_entry_tables(scratch.socketEntryTables,
@@ -73,28 +95,85 @@ Context& context() noexcept {
 bool all_domains_ready() noexcept {
     constants::InvestmentConstants published{};
     return runtime::named::ready() && item_definitions_ready() && configured_item_details_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);
+           && collectible_definitions_ready() && socket_plug_rules_ready()
+           && material_requirement_sets_ready() && inventory_bucket_descriptors_ready()
+           && socket_entry_lists_ready() && ability_buckets_ready()
+           && progression_definitions_ready() && scenario_layouts_ready() && spawn_sets_ready()
+           && hash_names_ready() && constants::find(published);
 }
 
 /** Gives mutable views over every fixed snapshot buffer. */
 cache::records::MutableDomains scratch_domains(Context& state) noexcept {
+    const auto named = ensure_scratch<content::Definition, content::kDefinitionCatalogCapacity>(
+        state.namedScratch);
+    const auto items =
+        ensure_scratch<build_data::items::Definition, build_data::items::kDefinitionCapacity>(
+            state.itemScratch);
+    const auto collectibles =
+        ensure_scratch<build_data::collectibles::Definition,
+                       build_data::collectibles::kDefinitionCapacity>(state.collectibleScratch);
+    const auto materialRequirementSets = ensure_scratch<material_requirements::Definition,
+                                                        material_requirements::kDefinitionCapacity>(
+        state.materialRequirementSetScratch);
+    const auto itemDetails =
+        ensure_scratch<build_data::items::details::Definition,
+                       build_data::items::details::kDefinitionCapacity>(state.itemDetailScratch);
+    const auto socketPlugRules =
+        ensure_scratch<build_data::items::socket_plugs::Rule,
+                       build_data::items::socket_plugs::kRuleCapacity>(state.socketPlugRuleScratch);
+    const auto socketPlugPools =
+        ensure_scratch<build_data::items::socket_plugs::Pool,
+                       build_data::items::socket_plugs::kPoolCapacity>(state.socketPlugPoolScratch);
+    const auto socketPlugMembers = ensure_scratch<build_data::items::socket_plugs::Member,
+                                                  build_data::items::socket_plugs::kMemberCapacity>(
+        state.socketPlugMemberScratch);
+    const auto inventoryBuckets =
+        ensure_scratch<inventory::buckets::Descriptor, inventory::buckets::kDescriptorCapacity>(
+            state.inventoryBucketScratch);
+    const auto socketEntryLists =
+        ensure_scratch<socket_entry_lists::Definition, socket_entry_lists::kDefinitionCapacity>(
+            state.socketEntryListScratch);
+    const auto socketEntryTables =
+        ensure_scratch<socket_entry_lists::EntryTable, socket_entry_lists::kEntryTableCapacity>(
+            state.socketEntryTableScratch);
+    const auto abilityBuckets =
+        ensure_scratch<abilities::Definition, abilities::kDefinitionCapacity>(
+            state.abilityBucketScratch);
+    const auto progressions =
+        ensure_scratch<progressions::Definition, progressions::kDefinitionCapacity>(
+            state.progressionScratch);
+    const auto scenarios = ensure_scratch<scenarios::Definition, scenarios::kDefinitionCapacity>(
+        state.scenarioScratch);
+    const auto rosterGroups =
+        ensure_scratch<scenarios::RosterGroup, scenarios::kRosterGroupCapacity>(
+            state.rosterGroupScratch);
+    const auto spawnStems =
+        ensure_scratch<spawn_sets::Stem, spawn_sets::kStemCapacity>(state.spawnStemScratch);
+    const auto spawnNameHashes =
+        ensure_scratch<spawn_sets::NameHash, spawn_sets::kNameHashCapacity>(
+            state.spawnNameHashScratch);
+    const auto hashNames =
+        ensure_scratch<hash_names::Name, hash_names::kNameCapacity>(state.hashNameScratch);
     return {
         &state.constantsScratch,
-        state.namedScratch,
-        state.itemScratch,
-        state.itemDetailScratch,
-        state.inventoryBucketScratch,
-        state.socketEntryListScratch,
-        state.socketEntryTableScratch,
-        state.abilityBucketScratch,
-        state.progressionScratch,
-        state.scenarioScratch,
-        state.rosterGroupScratch,
-        state.spawnStemScratch,
-        state.spawnNameHashScratch,
-        state.hashNameScratch,
+        named,
+        items,
+        collectibles,
+        materialRequirementSets,
+        itemDetails,
+        socketPlugRules,
+        socketPlugPools,
+        socketPlugMembers,
+        inventoryBuckets,
+        socketEntryLists,
+        socketEntryTables,
+        abilityBuckets,
+        progressions,
+        scenarios,
+        rosterGroups,
+        spawnStems,
+        spawnNameHashes,
+        hashNames,
     };
 }
 
@@ -103,7 +182,20 @@ void clear_locked(Context& state) noexcept {
     const cache::records::MutableDomains scratch = scratch_domains(state);
     std::fill(scratch.named.begin(), scratch.named.end(), content::Definition{});
     std::fill(scratch.items.begin(), scratch.items.end(), items::Definition{});
+    std::fill(scratch.collectibles.begin(), scratch.collectibles.end(), collectibles::Definition{});
+    std::fill(scratch.materialRequirementSets.begin(),
+              scratch.materialRequirementSets.end(),
+              material_requirements::Definition{});
     std::fill(scratch.itemDetails.begin(), scratch.itemDetails.end(), items::details::Definition{});
+    std::fill(scratch.socketPlugRules.begin(),
+              scratch.socketPlugRules.end(),
+              items::socket_plugs::Rule{});
+    std::fill(scratch.socketPlugPools.begin(),
+              scratch.socketPlugPools.end(),
+              items::socket_plugs::Pool{});
+    std::fill(scratch.socketPlugMembers.begin(),
+              scratch.socketPlugMembers.end(),
+              items::socket_plugs::Member{});
     std::fill(scratch.inventoryBuckets.begin(),
               scratch.inventoryBuckets.end(),
               inventory::buckets::Descriptor{});
@@ -134,21 +226,43 @@ void clear_locked(Context& state) noexcept {
 /** Gives read-only views over the used rows of one canonical snapshot. */
 cache::records::Domains occupied_domains(Context& state,
                                          const cache::records::DomainCounts& counts) noexcept {
+    const std::span<const items::details::Definition> itemDetails{state.itemDetailScratch.get(),
+                                                                  counts.itemDetails};
+    const std::span<const items::socket_plugs::Rule> socketPlugRules{
+        state.socketPlugRuleScratch.get(), counts.socketPlugRules};
+    const std::span<const items::socket_plugs::Pool> socketPlugPools{
+        state.socketPlugPoolScratch.get(), counts.socketPlugPools};
+    const std::span<const items::socket_plugs::Member> socketPlugMembers{
+        state.socketPlugMemberScratch.get(), counts.socketPlugMembers};
     return {
         state.constantsScratch,
-        std::span(state.namedScratch).first(counts.named),
-        std::span(state.itemScratch).first(counts.items),
-        std::span(state.itemDetailScratch).first(counts.itemDetails),
-        std::span(state.inventoryBucketScratch).first(counts.inventoryBuckets),
-        std::span(state.socketEntryListScratch).first(counts.socketEntryLists),
-        std::span(state.socketEntryTableScratch).first(counts.socketEntryTables),
-        std::span(state.abilityBucketScratch).first(counts.abilityBuckets),
-        std::span(state.progressionScratch).first(counts.progressions),
-        std::span(state.scenarioScratch).first(counts.scenarios),
-        std::span(state.rosterGroupScratch).first(counts.rosterGroups),
-        std::span(state.spawnStemScratch).first(counts.spawnStems),
-        std::span(state.spawnNameHashScratch).first(counts.spawnNameHashes),
-        std::span(state.hashNameScratch).first(counts.hashNames),
+        std::span<const content::Definition>{state.namedScratch.get(), counts.named},
+        std::span<const build_data::items::Definition>{state.itemScratch.get(), counts.items},
+        std::span<const build_data::collectibles::Definition>{state.collectibleScratch.get(),
+                                                              counts.collectibles},
+        std::span<const material_requirements::Definition>{
+            state.materialRequirementSetScratch.get(), counts.materialRequirementSets},
+        itemDetails,
+        socketPlugRules,
+        socketPlugPools,
+        socketPlugMembers,
+        std::span<const inventory::buckets::Descriptor>{state.inventoryBucketScratch.get(),
+                                                        counts.inventoryBuckets},
+        std::span<const socket_entry_lists::Definition>{state.socketEntryListScratch.get(),
+                                                        counts.socketEntryLists},
+        std::span<const socket_entry_lists::EntryTable>{state.socketEntryTableScratch.get(),
+                                                        counts.socketEntryTables},
+        std::span<const abilities::Definition>{state.abilityBucketScratch.get(),
+                                               counts.abilityBuckets},
+        std::span<const progressions::Definition>{state.progressionScratch.get(),
+                                                  counts.progressions},
+        std::span<const scenarios::Definition>{state.scenarioScratch.get(), counts.scenarios},
+        std::span<const scenarios::RosterGroup>{state.rosterGroupScratch.get(),
+                                                counts.rosterGroups},
+        std::span<const spawn_sets::Stem>{state.spawnStemScratch.get(), counts.spawnStems},
+        std::span<const spawn_sets::NameHash>{state.spawnNameHashScratch.get(),
+                                              counts.spawnNameHashes},
+        std::span<const hash_names::Name>{state.hashNameScratch.get(), counts.hashNames},
     };
 }
 

+ 22 - 16
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.h

@@ -3,17 +3,21 @@
 #include <Windows.h>
 
 #include <array>
+#include <memory>
 
 #include "../../../../core/filesystem/path.h"
 #include "../../../content/content_catalog.h"
 #include "../../abilities/definition.h"
 #include "../../cache/records/domains.h"
+#include "../../collectibles/collectible_catalog.h"
 #include "../../constants/definition.h"
 #include "../../definition.h"
 #include "../../hash_names/definition.h"
 #include "../../inventory/buckets/definition.h"
 #include "../../items/details/definition.h"
 #include "../../items/item_catalog.h"
+#include "../../items/socket_plugs/definition.h"
+#include "../../material_requirements/material_requirement_catalog.h"
 #include "../../progressions/definition.h"
 #include "../../scenarios/definition.h"
 #include "../../socket_entry_lists/definition.h"
@@ -24,22 +28,24 @@ namespace sunrise::state::build_data::runtime::persistence {
 /** Fixed cache paths, identity, and canonical snapshot storage guarded by one State lock. */
 struct Context {
     SRWLOCK lock{SRWLOCK_INIT};
-    std::array<content::Definition, content::kDefinitionCatalogCapacity> namedScratch{};
-    std::array<items::Definition, items::kDefinitionCapacity> itemScratch{};
-    std::array<items::details::Definition, items::details::kDefinitionCapacity> itemDetailScratch{};
-    std::array<inventory::buckets::Descriptor, inventory::buckets::kDescriptorCapacity>
-        inventoryBucketScratch{};
-    std::array<socket_entry_lists::Definition, socket_entry_lists::kDefinitionCapacity>
-        socketEntryListScratch{};
-    std::array<socket_entry_lists::EntryTable, socket_entry_lists::kEntryTableCapacity>
-        socketEntryTableScratch{};
-    std::array<abilities::Definition, abilities::kDefinitionCapacity> abilityBucketScratch{};
-    std::array<progressions::Definition, progressions::kDefinitionCapacity> progressionScratch{};
-    std::array<scenarios::Definition, scenarios::kDefinitionCapacity> scenarioScratch{};
-    std::array<scenarios::RosterGroup, scenarios::kRosterGroupCapacity> rosterGroupScratch{};
-    std::array<spawn_sets::Stem, spawn_sets::kStemCapacity> spawnStemScratch{};
-    std::array<spawn_sets::NameHash, spawn_sets::kNameHashCapacity> spawnNameHashScratch{};
-    std::array<hash_names::Name, hash_names::kNameCapacity> hashNameScratch{};
+    std::unique_ptr<content::Definition[]> namedScratch{};
+    std::unique_ptr<items::Definition[]> itemScratch{};
+    std::unique_ptr<collectibles::Definition[]> collectibleScratch{};
+    std::unique_ptr<material_requirements::Definition[]> materialRequirementSetScratch{};
+    std::unique_ptr<items::details::Definition[]> itemDetailScratch{};
+    std::unique_ptr<items::socket_plugs::Rule[]> socketPlugRuleScratch{};
+    std::unique_ptr<items::socket_plugs::Pool[]> socketPlugPoolScratch{};
+    std::unique_ptr<items::socket_plugs::Member[]> socketPlugMemberScratch{};
+    std::unique_ptr<inventory::buckets::Descriptor[]> inventoryBucketScratch{};
+    std::unique_ptr<socket_entry_lists::Definition[]> socketEntryListScratch{};
+    std::unique_ptr<socket_entry_lists::EntryTable[]> socketEntryTableScratch{};
+    std::unique_ptr<abilities::Definition[]> abilityBucketScratch{};
+    std::unique_ptr<progressions::Definition[]> progressionScratch{};
+    std::unique_ptr<scenarios::Definition[]> scenarioScratch{};
+    std::unique_ptr<scenarios::RosterGroup[]> rosterGroupScratch{};
+    std::unique_ptr<spawn_sets::Stem[]> spawnStemScratch{};
+    std::unique_ptr<spawn_sets::NameHash[]> spawnNameHashScratch{};
+    std::unique_ptr<hash_names::Name[]> hashNameScratch{};
     cache::records::InvestmentConstants constantsScratch{};
     core::path::Buffer cacheDirectory;
     core::path::Buffer cachePath;