Kaynağa Gözat

Tighten reward definition ownership and validation

Zach 1 ay önce
ebeveyn
işleme
8f35d7aacc

+ 1 - 0
Sunrise/Sunrise.vcxproj

@@ -2281,6 +2281,7 @@
     <ClCompile Include="src\client\hooks\network\investment\investment_socket_menu_routing.cpp" />
     <ClInclude Include="src\client\hooks\network\investment\socket_row_relocation.h" />
     <ClInclude Include="src\middleware\content\packages\tables\unlock_expression.h" />
+    <ClInclude Include="src\middleware\content\packages\tables\unlock_opcode.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode1801.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode1821.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode2400.h" />

+ 12 - 0
Sunrise/Sunrise.vcxproj.filters

@@ -67,4 +67,16 @@
     <None Include="vendor\lua\README.md"><Filter>Vendor\Lua</Filter></None>
     <None Include="vendor\lua\UPSTREAM_README"><Filter>Vendor\Lua</Filter></None>
   </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="src\client\content\items\packages\package_reward_build.cpp" />
+    <ClInclude Include="src\client\content\items\packages\package_reward_build.h" />
+    <ClInclude Include="src\middleware\content\packages\tables\field_reader.h" />
+    <ClCompile Include="src\state\build_data\cache\records\cache_reward_records.cpp" />
+    <ClInclude Include="src\state\build_data\rewards\definition.h" />
+    <ClCompile Include="src\state\build_data\rewards\reward_catalog.cpp" />
+    <ClInclude Include="src\state\build_data\rewards\reward_catalog.h" />
+    <ClCompile Include="src\state\rewards\reward_resolver.cpp" />
+    <ClInclude Include="src\state\rewards\reward_resolver.h" />
+    <ClInclude Include="src\middleware\content\packages\tables\unlock_opcode.h" />
+  </ItemGroup>
 </Project>

+ 141 - 107
Sunrise/src/client/content/items/packages/package_reward_build.cpp

@@ -32,31 +32,56 @@ constexpr std::uint32_t kModifierClass = 0x80807490U;
 constexpr std::uint32_t kSocketClass = 0x80803062U;
 constexpr std::uint32_t kWrapperClass = 0x808077CCU;
 constexpr std::uint32_t kSelectionClass = 0x808077CFU;
-constexpr std::uint32_t kFlagTableClass = 0x80807D49U, kFlagRowClass = 0x80807D4FU;
-constexpr std::uint32_t kValueTableClass = 0x80807C92U, kValueRowClass = 0x80807C96U;
-constexpr std::uint32_t kExpressionTableClass = 0x80807C49U, kExpressionRowClass = 0x80807C4FU;
+constexpr std::uint32_t kFlagTableClass = 0x80807D49U;
+constexpr std::uint32_t kFlagRowClass = 0x80807D4FU;
+constexpr std::uint32_t kValueTableClass = 0x80807C92U;
+constexpr std::uint32_t kValueRowClass = 0x80807C96U;
+constexpr std::uint32_t kExpressionTableClass = 0x80807C49U;
+constexpr std::uint32_t kExpressionRowClass = 0x80807C4FU;
 constexpr std::uint32_t kConditionClass = 0x80807D2FU;
-constexpr std::size_t kPoolStride = 24, kEntryStride = 80, kModifierStride = 24;
-constexpr std::size_t kBindingStride = 8, kExpressionRowStride = 24;
-constexpr std::size_t kSocketStride = 12, kSelectionStride = 12;
+constexpr std::size_t kPoolStride = 24;
+constexpr std::size_t kEntryStride = 80;
+constexpr std::size_t kModifierStride = 24;
+constexpr std::size_t kBindingStride = 8;
+constexpr std::size_t kExpressionRowStride = 24;
+constexpr std::size_t kSocketStride = 12;
+constexpr std::size_t kSelectionStride = 12;
 /** Item headers hold a relative wrapper pointer and an acquired-unlock slot. */
 constexpr std::size_t kWrapperField = 0x58;
 constexpr std::size_t kAcquiredFlagField = 0xDA;
 
 /** Byte offsets inside the serialized reward rows. */
-constexpr std::size_t kEntryQuantityOffset = 4, kEntryPoolOffset = 8;
-constexpr std::size_t kEntryCategoryOffset = 20, kEntryWeightOffset = 24;
-constexpr std::size_t kEntryBucketOffset = 28, kEntryConditionOffset = 32;
-constexpr std::size_t kEntryModifiersOffset = 48, kEntrySocketsOffset = 64;
-constexpr std::size_t kModifierValueIndexOffset = 16, kModifierValueOffset = 20;
-constexpr std::size_t kPoolEntriesOffset = 8, kExpressionBodyOffset = 8;
-constexpr std::size_t kWrapperSelectionsOffset = 8, kWrapperFlagsOffset = 24;
+constexpr std::size_t kEntryQuantityOffset = 4;
+constexpr std::size_t kEntryPoolOffset = 8;
+constexpr std::size_t kEntryCategoryOffset = 20;
+constexpr std::size_t kEntryWeightOffset = 24;
+constexpr std::size_t kEntryBucketOffset = 28;
+constexpr std::size_t kEntryConditionOffset = 32;
+constexpr std::size_t kEntryModifiersOffset = 48;
+constexpr std::size_t kEntrySocketsOffset = 64;
+constexpr std::size_t kModifierValueIndexOffset = 16;
+constexpr std::size_t kModifierValueOffset = 20;
+constexpr std::size_t kPoolEntriesOffset = 8;
+constexpr std::size_t kExpressionBodyOffset = 8;
+constexpr std::size_t kWrapperSelectionsOffset = 8;
+constexpr std::size_t kWrapperFlagsOffset = 24;
 constexpr std::size_t kSelectionCountOffset = 4;
-constexpr std::size_t kSocketPlugOffset = 2, kSocketPlugSetOffset = 4;
-constexpr std::size_t kSocketRollSetOffset = 6, kSocketSelectionOffset = 8;
+constexpr std::size_t kSocketPlugOffset = 2;
+constexpr std::size_t kSocketPlugSetOffset = 4;
+constexpr std::size_t kSocketRollSetOffset = 6;
+constexpr std::size_t kSocketSelectionOffset = 8;
 /** Relative definition bodies are preceded by their four-byte schema class. */
 constexpr std::size_t kDefinitionClassPrefixSize = sizeof(std::uint32_t);
 
+// Class rows name a default finisher whose use gate supplies the computed class flag.
+constexpr std::size_t kClassSlot = 12;
+constexpr std::size_t kClassStride = 32;
+constexpr std::size_t kDefaultFinisherOffset = 24;
+constexpr std::uint32_t kClassTable = 0x808075BEU;
+constexpr std::uint32_t kClassRow = 0x808074FAU;
+constexpr std::size_t kUseConditionPointer = 24;
+constexpr std::uint32_t kUseConditionClass = 0x80802980U;
+
 template <typename T> bool append(std::vector<T>& bank, T value, std::size_t capacity) noexcept {
     if (bank.size() >= capacity) {
         return false;
@@ -94,12 +119,42 @@ bool read_reward_entry(std::span<const std::byte> blob,
            && std::isfinite(entry.weight) && entry.weight >= 0;
 }
 
+/** Reads the single flag that gates use of the class's default finisher. */
+bool read_class_flag(std::span<const std::byte> blob, std::uint16_t& output) noexcept {
+    std::int64_t relative = 0;
+    if (!tables::read(blob, kUseConditionPointer, relative) || relative <= 0
+        || static_cast<std::uint64_t>(relative) > blob.size()
+        || kUseConditionPointer > blob.size() - static_cast<std::size_t>(relative)) {
+        return false;
+    }
+    const auto at = kUseConditionPointer + static_cast<std::size_t>(relative);
+    std::uint32_t cls = 0;
+    tables::Array expression{};
+    if (!tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kUseConditionClass
+        || !tables::read_array(
+            blob, at, kExpressionClass, tables::kUnlockInstructionStride, expression)
+        || expression.count != 1) {
+        return false;
+    }
+    std::uint32_t opcode = 0;
+    std::uint32_t flag = domain::kAbsent;
+    if (!tables::read(blob, expression.dataOffset, opcode)
+        || opcode != static_cast<std::uint32_t>(tables::UnlockOpcode::flag)
+        || !tables::read(
+            blob, expression.dataOffset + tables::kUnlockInstructionOperandOffset, flag)
+        || flag >= domain::kAbsent) {
+        return false;
+    }
+    output = static_cast<std::uint16_t>(flag);
+    return true;
+}
+
 } // namespace
 
 bool RewardBuild::entry(std::span<const std::byte> blob, std::size_t at) noexcept {
     domain::Entry out{};
     if (!read_reward_entry(blob, at, out)
-        || !conditions.read(blob, at + kEntryConditionOffset, instructions, out.condition)) {
+        || !conditions.read(blob, at + kEntryConditionOffset, instructions_, out.condition)) {
         return false;
     }
     tables::Array rows{};
@@ -107,46 +162,44 @@ bool RewardBuild::entry(std::span<const std::byte> blob, std::size_t at) noexcep
             blob, at + kEntryModifiersOffset, kModifierClass, kModifierStride, rows)) {
         return false;
     }
-    out.modifiers = {static_cast<std::uint32_t>(modifiers.size()),
+    out.modifiers = {static_cast<std::uint32_t>(modifiers_.size()),
                      static_cast<std::uint32_t>(rows.count)};
     for (std::size_t i = 0; i < rows.count; ++i) {
         const auto offset = rows.dataOffset + i * kModifierStride;
         domain::Modifier modifier{};
-        if (!conditions.read(blob, offset, instructions, modifier.condition)
+        if (!conditions.read(blob, offset, instructions_, modifier.condition)
             || !tables::read(blob, offset + kModifierValueIndexOffset, modifier.valueIndex)
             || !tables::read(blob, offset + kModifierValueOffset, modifier.value)
             || !std::isfinite(modifier.value)
-            || !append(modifiers, modifier, domain::kModifierCapacity)) {
+            || !append(modifiers_, modifier, domain::kModifierCapacity)) {
             return false;
         }
     }
     std::array<domain::SocketOverride, domain::kSocketsPerItem> overrides{};
     std::size_t count = 0;
     if (!read_reward_sockets(blob, at + kEntrySocketsOffset, overrides, count)
-        || count > domain::kSocketOverrideCapacity - sockets.size()) {
+        || count > domain::kSocketOverrideCapacity - sockets_.size()) {
         return false;
     }
-    out.sockets = {static_cast<std::uint32_t>(sockets.size()), static_cast<std::uint32_t>(count)};
+    out.sockets = {static_cast<std::uint32_t>(sockets_.size()), static_cast<std::uint32_t>(count)};
     for (std::size_t i = 0; i < count; ++i) {
         if (overrides[i].socketType == domain::kAbsent
-            || !append(sockets, overrides[i], domain::kSocketOverrideCapacity)) {
+            || !append(sockets_, overrides[i], domain::kSocketOverrideCapacity)) {
             return false;
         }
     }
-    return append(entries, out, domain::kEntryCapacity);
+    return append(entries_, out, domain::kEntryCapacity);
 }
 
 void RewardConditions::load_class_flags(const reader::Source& source,
                                         reader::Scratch& scratch,
                                         std::span<const std::byte> root) noexcept {
     classFlags_.fill(domain::kAbsent);
-    // Class rows name a default finisher whose use gate supplies the computed class flag.
-    constexpr std::size_t kClassSlot = 12, kClassStride = 32, kDefaultFinisherOffset = 24;
-    constexpr std::uint32_t kClassTable = 0x808075BEU, kClassRow = 0x808074FAU;
-    constexpr std::size_t kUseConditionPointer = 24;
-    constexpr std::uint32_t kUseConditionClass = 0x80802980U;
-    std::vector<std::byte> classes, index, blob;
-    tables::Array classRows{}, itemRows{};
+    std::vector<std::byte> classes;
+    std::vector<std::byte> index;
+    std::vector<std::byte> blob;
+    tables::Array classRows{};
+    tables::Array itemRows{};
     if (!root_table(source, scratch, root, kClassSlot, classes, kClassTable)
         || !tables::read_array(
             classes, tables::kTableArrayDescriptor, kClassRow, kClassStride, classRows)
@@ -165,18 +218,13 @@ void RewardConditions::load_class_flags(const reader::Source& source,
     for (std::size_t i = 0; i < classFlags_.size(); ++i) {
         std::uint16_t itemIndex = domain::kAbsent;
         tables::IndexRow item{};
-        tables::Array condition{};
-        std::uint32_t cls = 0, opcode = 0, flag = domain::kAbsent;
-        std::int64_t relative = 0;
+        std::uint32_t cls = 0;
         if (!tables::read(std::span<const std::byte>{classes},
                           classRows.dataOffset + i * kClassStride + kDefaultFinisherOffset,
                           itemIndex)
             || !tables::index_row(index, itemRows, itemIndex, item)
             || !reader::read_tag(source, scratch, item.targetTag, blob, cls)
-            || cls != tables::kItemDefinitionClass
-            || !tables::read(std::span<const std::byte>{blob}, kUseConditionPointer, relative)
-            || relative <= 0 || static_cast<std::uint64_t>(relative) > blob.size()
-            || kUseConditionPointer > blob.size() - static_cast<std::size_t>(relative)) {
+            || cls != tables::kItemDefinitionClass) {
             core::log::writef(
                 core::log::Channel::client,
                 core::log::Level::warn,
@@ -184,20 +232,7 @@ void RewardConditions::load_class_flags(const reader::Source& source,
                 i);
             continue;
         }
-        const auto at = kUseConditionPointer + static_cast<std::size_t>(relative);
-        if (tables::read(std::span<const std::byte>{blob}, at - kDefinitionClassPrefixSize, cls)
-            && cls == kUseConditionClass
-            && tables::read_array(
-                blob, at, kExpressionClass, tables::kUnlockInstructionStride, condition)
-            && condition.count == 1
-            && tables::read(std::span<const std::byte>{blob}, condition.dataOffset, opcode)
-            && opcode == static_cast<std::uint32_t>(tables::UnlockOpcode::flag)
-            && tables::read(std::span<const std::byte>{blob},
-                            condition.dataOffset + tables::kUnlockInstructionOperandOffset,
-                            flag)
-            && flag < domain::kAbsent) {
-            classFlags_[i] = static_cast<std::uint16_t>(flag);
-        } else {
+        if (!read_class_flag(blob, classFlags_[i])) {
             core::log::writef(core::log::Channel::client,
                               core::log::Level::warn,
                               "ev=pkg stage=class_flags class=%zu result=skip reason=use_condition",
@@ -229,10 +264,9 @@ bool RewardConditions::load(const reader::Source& source,
 }
 
 bool RewardConditions::bind(domain::Instruction& instruction) const noexcept {
-    const auto opcode =
-        static_cast<middleware::content::packages::tables::UnlockOpcode>(instruction.opcode);
-    const bool flag = opcode == middleware::content::packages::tables::UnlockOpcode::flag;
-    if (!flag && opcode != middleware::content::packages::tables::UnlockOpcode::loadValue) {
+    const auto opcode = static_cast<tables::UnlockOpcode>(instruction.opcode);
+    const bool flag = opcode == tables::UnlockOpcode::flag;
+    if (!flag && opcode != tables::UnlockOpcode::loadValue) {
         return true;
     }
     const auto& rows = flag ? flagRows_ : valueRows_;
@@ -245,13 +279,13 @@ bool RewardConditions::bind(domain::Instruction& instruction) const noexcept {
     if (!tables::read(blob, at, hash)) {
         return false;
     }
-    using B = domain::BankRead;
+    using Read = domain::BankRead;
     if (flag) {
         for (std::size_t characterClass = 0; characterClass < classFlags_.size();
              ++characterClass) {
             if (classFlags_[characterClass] != domain::kAbsent
                 && instruction.operand == classFlags_[characterClass]) {
-                instruction = {static_cast<std::uint32_t>(B::characterClass),
+                instruction = {static_cast<std::uint32_t>(Read::characterClass),
                                static_cast<std::uint32_t>(characterClass)};
                 return true;
             }
@@ -263,19 +297,21 @@ bool RewardConditions::bind(domain::Instruction& instruction) const noexcept {
     const auto characterIndex =
         bank_index(character, static_cast<std::int32_t>(instruction.operand));
     if (accountIndex != kUnmappedSlot) {
-        instruction = {static_cast<std::uint32_t>(flag ? B::accountFlag : B::accountValue),
+        instruction = {static_cast<std::uint32_t>(flag ? Read::accountFlag : Read::accountValue),
                        accountIndex};
     } else if (flag
                && bank_index(maps_->profileFlag, static_cast<std::int32_t>(instruction.operand))
                       != kUnmappedSlot) {
         instruction = {
-            static_cast<std::uint32_t>(B::profileFlag),
+            static_cast<std::uint32_t>(Read::profileFlag),
             bank_index(maps_->profileFlag, static_cast<std::int32_t>(instruction.operand))};
     } else if (characterIndex != kUnmappedSlot) {
-        instruction = {static_cast<std::uint32_t>(flag ? B::characterFlag : B::characterValue),
-                       characterIndex};
+        instruction = {
+            static_cast<std::uint32_t>(flag ? Read::characterFlag : Read::characterValue),
+            characterIndex};
     } else {
-        instruction = {static_cast<std::uint32_t>(flag ? B::externalFlag : B::externalValue), hash};
+        instruction = {static_cast<std::uint32_t>(flag ? Read::externalFlag : Read::externalValue),
+                       hash};
     }
     return true;
 }
@@ -300,8 +336,8 @@ bool RewardConditions::append_expression(std::span<const std::byte> blob,
                              instruction.operand)) {
             return false;
         }
-        if (static_cast<middleware::content::packages::tables::UnlockOpcode>(instruction.opcode)
-            == middleware::content::packages::tables::UnlockOpcode::expression) {
+        if (static_cast<tables::UnlockOpcode>(instruction.opcode)
+            == tables::UnlockOpcode::expression) {
             const auto before = bank.size();
             if (instruction.operand >= expressionRows_.count
                 || !append_expression(expressions_,
@@ -357,9 +393,7 @@ bool RewardConditions::read_list(std::span<const std::byte> blob,
         if (i != 0
             && !append(instructions,
                        domain::Instruction{
-                           static_cast<std::uint32_t>(
-                               middleware::content::packages::tables::UnlockOpcode::logicalAnd),
-                           0},
+                           static_cast<std::uint32_t>(tables::UnlockOpcode::logicalAnd), 0},
                        output.size())) {
             return false;
         }
@@ -402,11 +436,11 @@ bool RewardBuild::load(const reader::Source& source,
                        std::span<const std::byte> root,
                        const SlotMaps& maps) noexcept {
     loaded_ = false;
-    pools.clear();
-    entries.clear();
-    instructions.clear();
-    modifiers.clear();
-    sockets.clear();
+    pools_.clear();
+    entries_.clear();
+    instructions_.clear();
+    modifiers_.clear();
+    sockets_.clear();
     std::vector<std::byte> blob;
     tables::Array rows{};
     if (!conditions.load(source, scratch, root, maps)
@@ -424,32 +458,33 @@ bool RewardBuild::load(const reader::Source& source,
         if (tables::read(blob, at, pool.definitionHash) && pool.definitionHash != 0
             && tables::read_array(
                 blob, at + kPoolEntriesOffset, kEntryClass, kEntryStride, members)) {
-            pool.entries.first = static_cast<std::uint32_t>(entries.size());
+            pool.entries.first = static_cast<std::uint32_t>(entries_.size());
             for (std::size_t j = 0; j < members.count; ++j) {
-                const auto beforeInstructions = instructions.size(),
-                           beforeModifiers = modifiers.size(), beforeSockets = sockets.size();
+                const auto beforeInstructions = instructions_.size();
+                const auto beforeModifiers = modifiers_.size();
+                const auto beforeSockets = sockets_.size();
                 if (!entry(blob, members.dataOffset + j * kEntryStride)) {
-                    instructions.resize(beforeInstructions);
-                    modifiers.resize(beforeModifiers);
-                    sockets.resize(beforeSockets);
+                    instructions_.resize(beforeInstructions);
+                    modifiers_.resize(beforeModifiers);
+                    sockets_.resize(beforeSockets);
                     ++skipped;
                 }
             }
-            pool.entries.count = static_cast<std::uint32_t>(entries.size()) - pool.entries.first;
+            pool.entries.count = static_cast<std::uint32_t>(entries_.size()) - pool.entries.first;
         } else {
             pool = {};
             ++skipped;
         }
-        if (!append(pools, pool, domain::kPoolCapacity)) {
+        if (!append(pools_, pool, domain::kPoolCapacity)) {
             return false;
         }
     }
-    if (skipped != 0) {
-        core::log::writef(core::log::Channel::client,
-                          core::log::Level::warn,
-                          "ev=pkg stage=rewards skipped=%zu",
-                          skipped);
-    }
+    core::log::writef(core::log::Channel::client,
+                      skipped == 0 ? core::log::Level::info : core::log::Level::warn,
+                      "ev=pkg stage=rewards pools=%zu entries=%zu skipped=%zu",
+                      pools_.size(),
+                      entries_.size(),
+                      skipped);
     loaded_ = true;
     return true;
 }
@@ -466,9 +501,7 @@ bool RewardBuild::read_item(std::uint32_t hash,
     }
     item.definitionHash = hash;
     if (acquired != domain::kAbsent) {
-        domain::Instruction flag{
-            static_cast<std::uint32_t>(middleware::content::packages::tables::UnlockOpcode::flag),
-            acquired};
+        domain::Instruction flag{static_cast<std::uint32_t>(tables::UnlockOpcode::flag), acquired};
         if (!conditions.bind(flag) || !domain::valid_instruction(flag)) {
             return false;
         }
@@ -509,7 +542,7 @@ bool RewardBuild::begin_items(std::size_t count) noexcept {
         if (!loaded_ || count == 0 || count > domain::kItemCapacity) {
             return false;
         }
-        items.assign(count, {});
+        items_.assign(count, {});
         return true;
     } catch (...) {
         return false;
@@ -520,8 +553,8 @@ void RewardBuild::item(std::uint16_t index,
                        std::uint32_t hash,
                        std::span<const std::byte> blob) noexcept {
     domain::Item parsed{};
-    if (index < items.size() && read_item(hash, blob, parsed)) {
-        items[index] = parsed;
+    if (index < items_.size() && read_item(hash, blob, parsed)) {
+        items_[index] = parsed;
     }
 }
 
@@ -529,39 +562,40 @@ bool RewardBuild::publish() noexcept {
     if (!loaded_) {
         return false;
     }
-    std::size_t write = 0, socketWrite = 0;
-    for (auto& pool : pools) {
+    std::size_t write = 0;
+    std::size_t socketWrite = 0;
+    for (auto& pool : pools_) {
         const auto range = pool.entries;
         pool.entries.first = static_cast<std::uint32_t>(write);
         for (std::size_t i = range.first; i < range.first + range.count; ++i) {
-            const auto& row = entries[i];
-            bool valid = (row.itemIndex == domain::kAbsent || row.itemIndex < items.size())
-                         && (row.poolIndex == domain::kAbsent || row.poolIndex < pools.size());
+            const auto& row = entries_[i];
+            bool valid = (row.itemIndex == domain::kAbsent || row.itemIndex < items_.size())
+                         && (row.poolIndex == domain::kAbsent || row.poolIndex < pools_.size());
             for (const auto& socket :
-                 std::span(sockets).subspan(row.sockets.first, row.sockets.count)) {
-                valid &= socket.plugItem == domain::kAbsent || socket.plugItem < items.size();
+                 std::span(sockets_).subspan(row.sockets.first, row.sockets.count)) {
+                valid &= socket.plugItem == domain::kAbsent || socket.plugItem < items_.size();
             }
             if (valid) {
                 auto retained = row;
                 retained.sockets.first = static_cast<std::uint32_t>(socketWrite);
                 for (const auto& socket :
-                     std::span(sockets).subspan(row.sockets.first, row.sockets.count)) {
-                    sockets[socketWrite++] = socket;
+                     std::span(sockets_).subspan(row.sockets.first, row.sockets.count)) {
+                    sockets_[socketWrite++] = socket;
                 }
-                entries[write++] = retained;
+                entries_[write++] = retained;
             }
         }
         pool.entries.count = static_cast<std::uint32_t>(write) - pool.entries.first;
     }
-    entries.resize(write);
-    sockets.resize(socketWrite);
-    for (auto& item : items) {
-        if (item.poolIndex != domain::kAbsent && item.poolIndex >= pools.size()) {
+    entries_.resize(write);
+    sockets_.resize(socketWrite);
+    for (auto& item : items_) {
+        if (item.poolIndex != domain::kAbsent && item.poolIndex >= pools_.size()) {
             item = {};
         }
     }
     return state::build_data::publish_reward_definitions(
-        {pools, entries, items, instructions, modifiers, sockets});
+        {pools_, entries_, items_, instructions_, modifiers_, sockets_});
 }
 
 } // namespace sunrise::client::content::items::packages

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

@@ -2,8 +2,8 @@
 
 #include "../../../../middleware/content/packages/reader/reader.h"
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
+#include "../../../../state/account/account_state.h"
 #include "../../../../state/build_data/rewards/definition.h"
-#include "state/account/account_state.h"
 
 namespace sunrise::client::content::items::packages {
 
@@ -62,12 +62,12 @@ private:
                                  std::span<const std::byte> blob,
                                  state::build_data::rewards::Item& item) noexcept;
     bool loaded_{};
-    std::vector<state::build_data::rewards::Pool> pools;
-    std::vector<state::build_data::rewards::Entry> entries;
-    std::vector<state::build_data::rewards::Item> items;
-    std::vector<state::build_data::rewards::Instruction> instructions;
-    std::vector<state::build_data::rewards::Modifier> modifiers;
-    std::vector<state::build_data::rewards::SocketOverride> sockets;
+    std::vector<state::build_data::rewards::Pool> pools_;
+    std::vector<state::build_data::rewards::Entry> entries_;
+    std::vector<state::build_data::rewards::Item> items_;
+    std::vector<state::build_data::rewards::Instruction> instructions_;
+    std::vector<state::build_data::rewards::Modifier> modifiers_;
+    std::vector<state::build_data::rewards::SocketOverride> sockets_;
 };
 
 /** Progression rewards and pool entries use the same socket-override layout. */

+ 2 - 1
Sunrise/src/client/content/items/packages/package_season_pass_build.cpp

@@ -29,7 +29,8 @@ bool read_season_reward(Storage& storage,
             progressionTable, at + tables::kProgressionRewardQuantityOffset, reward.quantity)
         || !tables::read(
             progressionTable, at + tables::kProgressionRewardClaimSlotOffset, claimSlot)
-        || reward.quantity == 0 || reward.quantity > INT32_MAX
+        || reward.quantity == 0
+        || reward.quantity > static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())
         || rank > (std::numeric_limits<std::uint8_t>::max)()
         || itemIndex > (std::numeric_limits<std::uint16_t>::max)()
         || claimSlot > (std::numeric_limits<std::uint16_t>::max)()

+ 2 - 35
Sunrise/src/middleware/content/packages/tables/definition_index_table.h

@@ -4,6 +4,8 @@
 #include <cstdint>
 #include <span>
 
+#include "unlock_opcode.h"
+
 namespace sunrise::middleware::content::packages::tables {
 
 /** Definition tags sit in this closed range. */
@@ -28,9 +30,6 @@ inline constexpr std::uint16_t kAbsentPackageId = 0xFFFFU;
     return static_cast<std::uint16_t>((tag - kTagLowerBound) >> kTagPackageShift);
 }
 
-/** Hidden plugs, perks and quest roots use this bucket instead of held inventory. */
-inline constexpr std::uint8_t kNonInventoryBucketId = 37;
-
 /** Element class of the item index table inside the investment container. */
 inline constexpr std::uint32_t kItemIndexTableClass = 0x80807BE8U;
 /** Serialized item definition class, distinct from the item index table class. */
@@ -98,38 +97,6 @@ inline constexpr std::size_t kUnlockInstructionOperandOffset = 4;
 /** An expression field is a 64-bit count then a 64-bit self-relative offset. */
 inline constexpr std::size_t kUnlockExpressionFieldSize = 16;
 inline constexpr std::size_t kUnlockExpressionPointerOffset = 8;
-/** Native instruction values before unlock references are bound. */
-enum class UnlockOpcode : std::uint32_t {
-    flag = 1,
-    logicalNot = 2,
-    logicalOr = 3,
-    logicalAnd = 4,
-    logicalNor = 5,
-    notEqualAlternate = 6,
-    logicalNand = 7,
-    equal = 8,
-    notEqual = 9,
-    loadValue = 10,
-    constant = 11,
-    expression = 12,
-    greaterThan = 13,
-    greaterOrEqual = 14,
-    lessThan = 15,
-    lessOrEqual = 16,
-    add = 17,
-    subtract = 18,
-    multiply = 19,
-    divide = 20,
-    remainder = 21,
-    negate = 22,
-    hash = 23,
-    hashCombine = 24,
-    bitwiseAnd = 25,
-    bitwiseOr = 26,
-    bitwiseXor = 27,
-    bitwiseNot = 28,
-};
-
 /** The opcode that reads a value slot. */
 inline constexpr std::uint32_t kUnlockReadValueOpcode =
     static_cast<std::uint32_t>(UnlockOpcode::loadValue);

+ 2 - 1
Sunrise/src/middleware/content/packages/tables/quest_initialization_reader.cpp

@@ -2,6 +2,7 @@
 
 #include <limits>
 
+#include "../../../../state/build_data/inventory/buckets/definition.h"
 #include "definition_index_table.h"
 #include "internal.h"
 
@@ -196,7 +197,7 @@ Quest read_quest_initialization(std::span<const std::byte> definition,
         std::uint8_t parentBucket = 0;
         std::int64_t parentObjective = 0;
         if (parentIndex == itemIndex || !read(parent, kBucketIdOffset, parentBucket)
-            || parentBucket != kNonInventoryBucketId
+            || parentBucket != state::build_data::inventory::buckets::kNonInventoryBucketId
             || !read(parent, kItemObjectiveBlockOffset, parentObjective) || parentObjective != 0) {
             return {};
         }

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

@@ -0,0 +1,39 @@
+#pragma once
+
+#include <cstdint>
+
+namespace sunrise::middleware::content::packages::tables {
+
+/** Native instruction values before unlock references are bound. */
+enum class UnlockOpcode : std::uint32_t {
+    flag = 1,
+    logicalNot = 2,
+    logicalOr = 3,
+    logicalAnd = 4,
+    logicalNor = 5,
+    notEqualAlternate = 6,
+    logicalNand = 7,
+    equal = 8,
+    notEqual = 9,
+    loadValue = 10,
+    constant = 11,
+    expression = 12,
+    greaterThan = 13,
+    greaterOrEqual = 14,
+    lessThan = 15,
+    lessOrEqual = 16,
+    add = 17,
+    subtract = 18,
+    multiply = 19,
+    divide = 20,
+    remainder = 21,
+    negate = 22,
+    hash = 23,
+    hashCombine = 24,
+    bitwiseAnd = 25,
+    bitwiseOr = 26,
+    bitwiseXor = 27,
+    bitwiseNot = 28,
+};
+
+} // namespace sunrise::middleware::content::packages::tables

+ 2 - 0
Sunrise/src/state/build_data/inventory/buckets/definition.h

@@ -25,6 +25,8 @@ inline constexpr std::uint32_t kProfileSlotCapacity = 701;
 inline constexpr std::uint32_t kSmallProfileSlotCapacity = 6;
 /** An all-one bucket id means no runtime bucket is available. */
 inline constexpr std::uint8_t kUnavailableBucketId = (std::numeric_limits<std::uint8_t>::max)();
+/** Hidden plugs, perks and quest roots use this bucket instead of held inventory. */
+inline constexpr std::uint8_t kNonInventoryBucketId = 37;
 /** Character bucket containing unopened engrams. */
 inline constexpr std::uint8_t kEngramBucketId = 31;
 /** Leaving out the unavailable id leaves at most 255 unique bucket records. */

+ 20 - 12
Sunrise/src/state/build_data/rewards/reward_catalog.cpp

@@ -10,7 +10,7 @@
 #include "../../unlocks/definition.h"
 #include "../table.h"
 #include "core/threading/srw_lock.h"
-#include "middleware/content/packages/tables/definition_index_table.h"
+#include "middleware/content/packages/tables/unlock_opcode.h"
 
 namespace sunrise::state::build_data::rewards {
 namespace {
@@ -60,6 +60,23 @@ bool valid_depth(View data,
     return true;
 }
 
+bool valid_entry(View data, const Entry& entry) noexcept {
+    return (entry.itemIndex == kAbsent || entry.itemIndex < data.items.size())
+           && (entry.poolIndex == kAbsent || entry.poolIndex < data.pools.size())
+           && fits(entry.condition, data.instructions) && fits(entry.modifiers, data.modifiers)
+           && fits(entry.sockets, data.sockets) && std::isfinite(entry.weight) && entry.weight >= 0;
+}
+
+bool valid_item(View data, const Item& item) noexcept {
+    if (item.definitionHash == 0) {
+        return item.poolIndex == kAbsent && item.acquiredFlag == kAbsent
+               && item.selectionCount == 0;
+    }
+    return item.selectionCount <= item.selections.size()
+           && (item.acquiredFlag == kAbsent || item.acquiredFlag < unlocks::kAccountFlagCapacity)
+           && (item.poolIndex == kAbsent || item.poolIndex < data.pools.size());
+}
+
 } // namespace
 
 void clear() noexcept {
@@ -132,21 +149,12 @@ bool valid(View data) noexcept {
         }
     }
     for (const Entry& entry : data.entries) {
-        if ((entry.itemIndex != kAbsent && entry.itemIndex >= data.items.size())
-            || (entry.poolIndex != kAbsent && entry.poolIndex >= data.pools.size())
-            || !fits(entry.condition, data.instructions) || !fits(entry.modifiers, data.modifiers)
-            || !fits(entry.sockets, data.sockets) || !std::isfinite(entry.weight)
-            || entry.weight < 0) {
+        if (!valid_entry(data, entry)) {
             return false;
         }
     }
     for (const Item& item : data.items) {
-        if ((item.definitionHash == 0
-             && (item.poolIndex != kAbsent || item.acquiredFlag != kAbsent
-                 || item.selectionCount != 0))
-            || item.selectionCount > item.selections.size()
-            || (item.acquiredFlag != kAbsent && item.acquiredFlag >= unlocks::kAccountFlagCapacity)
-            || (item.poolIndex != kAbsent && item.poolIndex >= data.pools.size())) {
+        if (!valid_item(data, item)) {
             return false;
         }
     }

+ 4 - 0
Sunrise/src/state/runtime/runtime.h

@@ -13,6 +13,10 @@
 #include "../unlocks/definition.h"
 #include "state.h"
 
+namespace sunrise::state::unlocks {
+struct Table;
+}
+
 namespace sunrise::state::account::settings {
 
 struct SettingsDelta;

+ 24 - 12
Sunrise/src/state/runtime/state_account_reward_grant_runtime.cpp

@@ -16,7 +16,6 @@
 #include "../rewards/reward_resolver.h"
 #include "../unlocks/unlocks_records.h"
 #include "../unlocks/unlocks_runtime.h"
-#include "middleware/content/packages/tables/definition_index_table.h"
 #include "runtime.h"
 #include "state_account_transaction_helpers.h"
 #include "storage/internal.h"
@@ -29,6 +28,8 @@ namespace item_details = build_data::items::details;
 namespace inventory_buckets = build_data::inventory::buckets;
 namespace family4_loadout = middleware::datagen::family4::loadout;
 
+static_assert(build_data::rewards::kSocketsPerItem == item_details::kInitialPlugCapacity);
+
 namespace {
 
 [[nodiscard]] bool materialize_record_reward(const AccountState& current,
@@ -128,11 +129,24 @@ apply_reward_sockets(const item_details::Definition& detail,
         std::span(rows).first(resolved.count), kUnclaimedRecordIndex, mutation, refusal);
 }
 
+/** Wrappers and direct account perks bypass quest initialization. */
+bool uses_reward_definition(const build_data::items::Definition& item) noexcept {
+    build_data::rewards::Item reward{};
+    if (!build_data::rewards::find_item(item.definitionIndex, reward)) {
+        return false;
+    }
+    return reward.poolIndex != build_data::rewards::kAbsent
+           || (item.bucketId == inventory_buckets::kNonInventoryBucketId
+               && reward.acquiredFlag != build_data::rewards::kAbsent);
+}
+
+enum class PassResolution { claim, replay };
+
 [[nodiscard]] bool resolve_pass(const build_data::season_pass::Reward& reward,
                                 const AccountState& account,
                                 std::uint64_t seed,
                                 rewards::Result& result,
-                                bool replay = false,
+                                PassResolution resolution,
                                 const char** reason = nullptr) noexcept {
     const auto character = selected_character_index(account);
     unlocks::Table flags{};
@@ -147,7 +161,7 @@ apply_reward_sockets(const item_details::Definition& detail,
     if (reason != nullptr) {
         *reason = "reward_condition";
     }
-    if (replay && reward.claimFlagIndex < flags.accountFlags.size()) {
+    if (resolution == PassResolution::replay && reward.claimFlagIndex < flags.accountFlags.size()) {
         flags.accountFlags[reward.claimFlagIndex] = unlocks::kFlagClear;
     }
     bool enabled = false;
@@ -185,7 +199,8 @@ apply_reward_sockets(const item_details::Definition& detail,
     const auto* grant = &mutation.grant;
     rewards::Result expected{};
     if (!mutation.prepared || mutation.sourceDefinitionHash != reward.itemHash
-        || !resolve_pass(reward, account_snapshot(), mutation.seed, expected, true)
+        || !resolve_pass(
+            reward, account_snapshot(), mutation.seed, expected, PassResolution::replay)
         || expected.count != grant->rewardCount) {
         return false;
     }
@@ -252,7 +267,8 @@ bool prepare_season_pass_reward(std::uint16_t rewardIndex,
     std::memcpy(&mutation.seed, random.data(), random.size());
     rewards::Result resolved{};
     reason = "reward_condition";
-    if (!resolve_pass(reward, account_snapshot(), mutation.seed, resolved, false, &reason)) {
+    if (!resolve_pass(
+            reward, account_snapshot(), mutation.seed, resolved, PassResolution::claim, &reason)) {
         return false;
     }
     reason = "reward_placement";
@@ -268,14 +284,10 @@ bool prepare_season_pass_reward(std::uint16_t rewardIndex,
 
 ItemGrantRoute item_grant_route(std::uint16_t itemIndex) noexcept {
     build_data::items::Definition item{};
-    build_data::rewards::Item reward{};
     if (!build_data::find_item_definition_index(itemIndex, item)) {
         return ItemGrantRoute::unavailable;
     }
-    if (build_data::rewards::find_item(itemIndex, reward)
-        && (reward.poolIndex != build_data::rewards::kAbsent
-            || (item.bucketId == middleware::content::packages::tables::kNonInventoryBucketId
-                && reward.acquiredFlag != build_data::rewards::kAbsent))) {
+    if (uses_reward_definition(item)) {
         return ItemGrantRoute::reward;
     }
     if (item.questInitialization.scope != build_data::items::QuestInitialization::Scope::none) {
@@ -433,7 +445,7 @@ namespace {
         if (reward.kind == RecordRewardKind::accountUnlock) {
             build_data::rewards::Item source{};
             if (reward.quantity != 1 || reward.afterQuantity != 1 || reward.instanceSoid != 0
-                || item.bucketId != middleware::content::packages::tables::kNonInventoryBucketId
+                || item.bucketId != build_data::inventory::buckets::kNonInventoryBucketId
                 || !build_data::rewards::find_item(item.definitionIndex, source)
                 || source.acquiredFlag == build_data::rewards::kAbsent
                 || source.acquiredFlag != reward.acquiredFlag) {
@@ -564,7 +576,7 @@ bool prepare_record_reward_grant(std::span<const DirectRecordReward> rewards,
                 prepared.previousFlag = static_cast<std::uint8_t>(before);
             }
         }
-        if (item.bucketId == middleware::content::packages::tables::kNonInventoryBucketId) {
+        if (item.bucketId == build_data::inventory::buckets::kNonInventoryBucketId) {
             reason = "perk_acquisition";
             if (prepared.acquiredFlag == build_data::rewards::kAbsent || requested.quantity != 1
                 || !requested.sockets.empty()) {

+ 1 - 2
Sunrise/src/state/runtime/state_progression_runtime.cpp

@@ -18,7 +18,6 @@
 #include "../unlocks/definition.h"
 #include "../unlocks/unlocks_runtime.h"
 #include "core/logging/log.h"
-#include "middleware/content/packages/tables/definition_index_table.h"
 #include "runtime.h"
 #include "state.h"
 #include "state_account_transaction_helpers.h"
@@ -345,7 +344,7 @@ progression::season_pass::progress_flag(const build_data::season_pass::Reward& r
     rewards::Item acquisition{};
     if (build_data::find_item_definition_index(reward.itemIndex, item)
         && item.definitionHash == reward.itemHash
-        && item.bucketId == middleware::content::packages::tables::kNonInventoryBucketId
+        && item.bucketId == build_data::inventory::buckets::kNonInventoryBucketId
         && rewards::find_item(reward.itemIndex, acquisition)
         && acquisition.definitionHash == reward.itemHash
         && acquisition.poolIndex == rewards::kAbsent) {