Kaynağa Gözat

Share one unlock-expression evaluator and tighten reward layering

Zach 1 ay önce
ebeveyn
işleme
6961c3c156
44 değiştirilmiş dosya ile 843 ekleme ve 704 silme
  1. 3 3
      Sunrise/Sunrise.vcxproj
  2. 1 0
      Sunrise/src/client/content/items/packages/internal.h
  3. 9 7
      Sunrise/src/client/content/items/packages/package_item_build.cpp
  4. 1 2
      Sunrise/src/client/content/items/packages/package_item_rows.cpp
  5. 64 73
      Sunrise/src/client/content/items/packages/package_reward_build.cpp
  6. 17 1
      Sunrise/src/client/content/items/packages/package_reward_build.h
  7. 45 34
      Sunrise/src/client/content/items/packages/package_season_pass_build.cpp
  8. 1 20
      Sunrise/src/middleware/content/packages/tables/activity_table.h
  9. 1 0
      Sunrise/src/middleware/content/packages/tables/definition_index_table.cpp
  10. 18 16
      Sunrise/src/middleware/content/packages/tables/definition_index_table.h
  11. 23 0
      Sunrise/src/middleware/content/packages/tables/field_reader.h
  12. 4 2
      Sunrise/src/middleware/content/packages/tables/quest_initialization_reader.cpp
  13. 1 4
      Sunrise/src/middleware/content/packages/tables/unlock_expression.h
  14. 0 39
      Sunrise/src/middleware/content/packages/tables/unlock_opcode.h
  15. 10 4
      Sunrise/src/server/bap/bap_world_reward_queue.cpp
  16. 5 0
      Sunrise/src/server/bap/encrypted/push/snapshot/family4_reward_updates.cpp
  17. 14 8
      Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp
  18. 1 0
      Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h
  19. 7 1
      Sunrise/src/server/web_service/web_service_record_actions.cpp
  20. 5 5
      Sunrise/src/state/build_data/cache/records/cache_domain_validation.cpp
  21. 19 3
      Sunrise/src/state/build_data/cache/records/cache_reward_records.cpp
  22. 7 3
      Sunrise/src/state/build_data/cache/records/format.h
  23. 5 2
      Sunrise/src/state/build_data/inventory/buckets/definition.h
  24. 25 23
      Sunrise/src/state/build_data/rewards/definition.h
  25. 30 0
      Sunrise/src/state/build_data/rewards/reward_build_data_runtime.cpp
  26. 18 45
      Sunrise/src/state/build_data/rewards/reward_catalog.cpp
  27. 0 2
      Sunrise/src/state/build_data/rewards/reward_catalog.h
  28. 30 2
      Sunrise/src/state/build_data/runtime.h
  29. 0 6
      Sunrise/src/state/build_data/runtime/build_data_catalog_runtime.cpp
  30. 19 20
      Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp
  31. 6 7
      Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.h
  32. 4 2
      Sunrise/src/state/build_data/season_pass/definition.h
  33. 5 0
      Sunrise/src/state/build_data/season_pass/season_pass_build_data_runtime.cpp
  34. 34 17
      Sunrise/src/state/build_data/season_pass/season_pass_catalog.cpp
  35. 0 119
      Sunrise/src/state/build_data/vendors/vendor_expression.cpp
  36. 0 58
      Sunrise/src/state/build_data/vendors/vendor_expression.h
  37. 2 1
      Sunrise/src/state/progression/season_pass_reward_catalog.h
  38. 73 131
      Sunrise/src/state/rewards/reward_resolver.cpp
  39. 3 0
      Sunrise/src/state/rewards/reward_resolver.h
  40. 16 4
      Sunrise/src/state/runtime/runtime.h
  41. 11 27
      Sunrise/src/state/runtime/state_account_reward_grant_runtime.cpp
  42. 11 13
      Sunrise/src/state/runtime/state_progression_runtime.cpp
  43. 203 0
      Sunrise/src/state/unlocks/unlocks_expression.cpp
  44. 92 0
      Sunrise/src/state/unlocks/unlocks_expression.h

+ 3 - 3
Sunrise/Sunrise.vcxproj

@@ -103,6 +103,7 @@
     <ClCompile Include="src\client\content\items\packages\package_reward_build.cpp" />
     <ClCompile Include="src\state\rewards\reward_resolver.cpp" />
     <ClCompile Include="src\state\build_data\rewards\reward_catalog.cpp" />
+    <ClCompile Include="src\state\build_data\rewards\reward_build_data_runtime.cpp" />
     <ClCompile Include="src\state\gameplay\external\entity_object_types.cpp" />
     <ClCompile Include="src\state\investment\investment_account_store.cpp" />
     <ClCompile Include="src\state\investment\investment_database.cpp" />
@@ -710,7 +711,7 @@
     <ClCompile Include="src\state\build_data\cache\records\cache_vendor_records.cpp" />
     <ClCompile Include="src\state\build_data\vendors\vendor_catalog.cpp" />
     <ClCompile Include="src\state\build_data\vendors\artifact_sale_view.cpp" />
-    <ClCompile Include="src\state\build_data\vendors\vendor_expression.cpp" />
+    <ClCompile Include="src\state\unlocks\unlocks_expression.cpp" />
     <ClCompile Include="src\client\content\vendors\package_vendor_build.cpp" />
     <ClCompile Include="src\state\build_data\cache\records\cache_hash_name_records.cpp" />
     <ClCompile Include="src\state\build_data\cache\records\cache_entity_metadata_records.cpp" />
@@ -1621,7 +1622,7 @@
     <ClInclude Include="src\state\build_data\spawn_sets\definition.h" />
     <ClInclude Include="src\state\build_data\vendors\definition.h" />
     <ClInclude Include="src\state\build_data\vendors\vendor_catalog.h" />
-    <ClInclude Include="src\state\build_data\vendors\vendor_expression.h" />
+    <ClInclude Include="src\state\unlocks\unlocks_expression.h" />
     <ClInclude Include="src\client\content\vendors\layout.h" />
     <ClInclude Include="src\client\content\vendors\vendor_build.h" />
     <ClInclude Include="src\state\build_data\scenarios\definition.h" />
@@ -2281,7 +2282,6 @@
     <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" />

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

@@ -85,6 +85,7 @@ struct Storage {
     reader::Scratch scratch{};
     /** Read once per root. Every domain resolves its unlock slots through these. */
     SlotMaps slotMaps{};
+    /** Reward pools, wrappers and condition tables read alongside the item walk. */
     RewardBuild rewardBuild{};
     /** Node rows held until the value slot and owned records are resolved. */
     std::array<state::build_data::nodes::Definition, state::build_data::nodes::kDefinitionCapacity>

+ 9 - 7
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -8,7 +8,6 @@
 #include "../../../../middleware/content/packages/reader/reader.h"
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
 #include "../../../../state/build_data/activities/activity_catalog.h"
-#include "../../../../state/build_data/rewards/reward_catalog.h"
 #include "../../../../state/build_data/runtime.h"
 #include "../../activity/activity_catalog_build.h"
 #include "../../activity/entity_position_profile_build.h"
@@ -36,7 +35,8 @@ namespace {
            && state::build_data::ability_buckets_ready()
            && state::build_data::socket_entry_buckets_ready()
            && state::build_data::progression_definitions_ready()
-           && state::build_data::season_pass_ready() && state::build_data::rewards::ready()
+           && state::build_data::season_pass_ready()
+           && state::build_data::reward_definitions_ready()
            && state::build_data::repeatable_bounties_ready()
            && state::build_data::record_definitions_ready()
            && state::build_data::node_definitions_ready()
@@ -128,8 +128,8 @@ bool build() noexcept {
             if (!state::build_data::item_definitions_ready()
                 || !state::build_data::record_definitions_ready()
                 || !state::build_data::node_definitions_ready()
-                || !state::build_data::season_pass_ready() || !state::build_data::rewards::ready()
-                || !exotic_catalysts_settled()) {
+                || !state::build_data::season_pass_ready()
+                || !state::build_data::reward_definitions_ready() || !exotic_catalysts_settled()) {
                 reason = "unlock_maps";
                 if (!read_unlock_slot_maps(
                         source, storage, std::span<const std::byte>{storage.root})) {
@@ -187,11 +187,13 @@ bool build() noexcept {
                         std::span(storage.progressionSteps).first(storage.progressionStepCount));
                 }
             }
-            const bool rewardTables =
-                (!state::build_data::rewards::ready() || !state::build_data::season_pass_ready())
+            // Pass rows bind their conditions through the reward condition tables.
+            const bool conditionTablesLoaded =
+                (!state::build_data::reward_definitions_ready()
+                 || !state::build_data::season_pass_ready())
                 && storage.rewardBuild.load(
                     source, storage.scratch, storage.root, storage.slotMaps);
-            if (rewardTables && !state::build_data::season_pass_ready()
+            if (conditionTablesLoaded && !state::build_data::season_pass_ready()
                 && build_season_pass(source, storage, std::span<const std::byte>{storage.root})) {
                 (void)state::build_data::publish_season_pass(
                     std::span(storage.seasonPassRewards).first(storage.seasonPassRewardCount));

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

@@ -4,7 +4,6 @@
 #include "../../../../middleware/content/packages/tables/quest_initialization_reader.h"
 #include "../../../../state/build_data/items/catalysts/exotic_catalyst_builder.h"
 #include "../../../../state/build_data/items/details/item_detail_catalog.h"
-#include "../../../../state/build_data/rewards/reward_catalog.h"
 #include "../../../../state/build_data/runtime.h"
 #include "internal.h"
 #include "package_socket_plug_build.h"
@@ -62,7 +61,7 @@ bool build_item_rows(const reader::Source& source,
                      const tables::Array& table,
                      std::size_t& rowCount,
                      const char*& reason) noexcept {
-    const bool needRewards = !state::build_data::rewards::ready();
+    const bool needRewards = !state::build_data::reward_definitions_ready();
     const bool rewardStorageReady =
         !needRewards || storage.rewardBuild.begin_items(static_cast<std::size_t>(table.count));
     const bool needDefinitions = !state::build_data::item_definitions_ready();

+ 64 - 73
Sunrise/src/client/content/items/packages/package_reward_build.cpp

@@ -4,11 +4,11 @@
 #include <cmath>
 #include <vector>
 
+#include "../../../../core/logging/log.h"
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
-#include "../../../../middleware/content/packages/tables/internal.h"
-#include "../../../../state/build_data/rewards/reward_catalog.h"
+#include "../../../../middleware/content/packages/tables/field_reader.h"
 #include "../../../../state/build_data/runtime.h"
-#include "core/logging/log.h"
+#include "../../../../state/unlocks/unlocks_expression.h"
 #include "internal.h"
 
 namespace sunrise::client::content::items::packages {
@@ -17,6 +17,7 @@ namespace {
 namespace reader = middleware::content::packages::reader;
 namespace tables = middleware::content::packages::tables;
 namespace domain = state::build_data::rewards;
+namespace unlocks = state::unlocks;
 
 /** Investment-root slots identify reward pools and unlock-slot bindings. */
 constexpr std::size_t kSupplementalRewardSlot = 84;
@@ -76,7 +77,7 @@ 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.
+/** 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;
@@ -85,6 +86,7 @@ constexpr std::uint32_t kClassRow = 0x808074FAU;
 constexpr std::size_t kUseConditionPointer = 24;
 constexpr std::uint32_t kUseConditionClass = 0x80802980U;
 
+/** Appends within a bank's capacity; an allocation failure is refused rather than thrown. */
 template <typename T> bool append(std::vector<T>& bank, T value, std::size_t capacity) noexcept {
     if (bank.size() >= capacity) {
         return false;
@@ -97,6 +99,7 @@ template <typename T> bool append(std::vector<T>& bank, T value, std::size_t cap
     }
 }
 
+/** Reads the table an investment-root slot names, checking its class when one is expected. */
 bool root_table(const reader::Source& source,
                 reader::Scratch& scratch,
                 std::span<const std::byte> root,
@@ -110,6 +113,7 @@ bool root_table(const reader::Source& source,
            && (expectedClass == 0 || cls == expectedClass);
 }
 
+/** Reads one entry's fixed fields; a negative or non-finite weight is refused. */
 bool read_reward_entry(std::span<const std::byte> blob,
                        std::size_t at,
                        domain::Entry& entry) noexcept {
@@ -124,16 +128,11 @@ bool read_reward_entry(std::span<const std::byte> blob,
 
 /** 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::size_t at = 0;
     std::uint32_t cls = 0;
     tables::Array expression{};
-    if (!tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kUseConditionClass
+    if (!tables::relative(blob, kUseConditionPointer, at) || at < kDefinitionClassPrefixSize
+        || !tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kUseConditionClass
         || !tables::read_array(
             blob, at, kExpressionClass, tables::kUnlockInstructionStride, expression)
         || expression.count != 1) {
@@ -142,7 +141,7 @@ bool read_class_flag(std::span<const std::byte> blob, std::uint16_t& output) noe
     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)
+        || opcode != tables::kUnlockReadFlagOpcode
         || !tables::read(
             blob, expression.dataOffset + tables::kUnlockInstructionOperandOffset, flag)
         || flag >= domain::kAbsent) {
@@ -267,55 +266,50 @@ bool RewardConditions::load(const reader::Source& source,
                                  expressionRows_);
 }
 
-bool RewardConditions::bind(domain::Instruction& instruction) const noexcept {
-    const auto opcode = static_cast<tables::UnlockOpcode>(instruction.opcode);
-    const bool flag = opcode == tables::UnlockOpcode::flag;
-    if (!flag && opcode != tables::UnlockOpcode::loadValue) {
-        return true;
+bool RewardConditions::bind(std::uint32_t native,
+                            std::uint32_t operand,
+                            domain::Instruction& instruction) const noexcept {
+    const bool flag = native == tables::kUnlockReadFlagOpcode;
+    if (!flag && native != tables::kUnlockReadValueOpcode) {
+        instruction = {{}, unlocks::Bank::none, operand};
+        return unlocks::decode_opcode(native, instruction.opcode);
     }
     const auto& rows = flag ? flagRows_ : valueRows_;
     const std::span<const std::byte> blob = flag ? flags_ : values_;
-    if (instruction.operand >= rows.count) {
-        return false;
-    }
-    const std::size_t at = rows.dataOffset + instruction.operand * kBindingStride;
     std::uint32_t hash = 0;
-    if (!tables::read(blob, at, hash)) {
+    if (operand >= rows.count
+        || !tables::read(blob, rows.dataOffset + operand * kBindingStride, hash)) {
         return false;
     }
-    using Read = domain::BankRead;
+    instruction.opcode = flag ? unlocks::Opcode::flag : unlocks::Opcode::loadValue;
     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>(Read::characterClass),
-                               static_cast<std::uint32_t>(characterClass)};
+                && operand == classFlags_[characterClass]) {
+                instruction.bank = unlocks::Bank::characterClass;
+                instruction.operand = static_cast<std::uint32_t>(characterClass);
                 return true;
             }
         }
     }
-    const auto& account = flag ? maps_->accountFlag : maps_->accountValue;
-    const auto& character = flag ? maps_->characterFlag : maps_->characterValue;
-    const auto accountIndex = bank_index(account, static_cast<std::int32_t>(instruction.operand));
+    const auto slot = static_cast<std::int32_t>(operand);
+    const auto accountIndex = bank_index(flag ? maps_->accountFlag : maps_->accountValue, slot);
+    const auto profileIndex = flag ? bank_index(maps_->profileFlag, slot) : kUnmappedSlot;
     const auto characterIndex =
-        bank_index(character, static_cast<std::int32_t>(instruction.operand));
+        bank_index(flag ? maps_->characterFlag : maps_->characterValue, slot);
     if (accountIndex != kUnmappedSlot) {
-        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>(Read::profileFlag),
-            bank_index(maps_->profileFlag, static_cast<std::int32_t>(instruction.operand))};
+        instruction.bank = unlocks::Bank::account;
+        instruction.operand = accountIndex;
+    } else if (profileIndex != kUnmappedSlot) {
+        instruction.bank = unlocks::Bank::profile;
+        instruction.operand = profileIndex;
     } else if (characterIndex != kUnmappedSlot) {
-        instruction = {
-            static_cast<std::uint32_t>(flag ? Read::characterFlag : Read::characterValue),
-            characterIndex};
+        instruction.bank = unlocks::Bank::character;
+        instruction.operand = characterIndex;
     } else {
-        instruction = {static_cast<std::uint32_t>(flag ? Read::externalFlag : Read::externalValue),
-                       hash};
+        instruction.bank = unlocks::Bank::external;
+        instruction.operand = hash;
     }
     return true;
 }
@@ -331,30 +325,29 @@ bool RewardConditions::append_expression(std::span<const std::byte> blob,
         return false;
     }
     for (std::size_t i = 0; i < rows.count; ++i) {
-        domain::Instruction instruction{};
-        if (!tables::read(
-                blob, rows.dataOffset + i * tables::kUnlockInstructionStride, instruction.opcode)
-            || !tables::read(blob,
-                             rows.dataOffset + i * tables::kUnlockInstructionStride
-                                 + tables::kUnlockInstructionOperandOffset,
-                             instruction.operand)) {
+        const std::size_t row = rows.dataOffset + i * tables::kUnlockInstructionStride;
+        std::uint32_t native = 0;
+        std::uint32_t operand = 0;
+        if (!tables::read(blob, row, native)
+            || !tables::read(blob, row + tables::kUnlockInstructionOperandOffset, operand)) {
             return false;
         }
-        if (static_cast<tables::UnlockOpcode>(instruction.opcode)
-            == tables::UnlockOpcode::expression) {
+        if (native == tables::kUnlockExpressionOpcode) {
             const auto before = bank.size();
-            if (instruction.operand >= expressionRows_.count
+            if (operand >= expressionRows_.count
                 || !append_expression(expressions_,
-                                      expressionRows_.dataOffset
-                                          + instruction.operand * kExpressionRowStride
+                                      expressionRows_.dataOffset + operand * kExpressionRowStride
                                           + kExpressionBodyOffset,
                                       bank,
                                       depth + 1)
                 || bank.size() == before) {
                 return false;
             }
-        } else if (!bind(instruction) || !domain::valid_instruction(instruction)
-                   || !append(bank, instruction, domain::kInstructionCapacity)) {
+            continue;
+        }
+        domain::Instruction instruction{};
+        if (!bind(native, operand, instruction) || !unlocks::valid(instruction)
+            || !append(bank, instruction, domain::kInstructionCapacity)) {
             return false;
         }
     }
@@ -396,8 +389,7 @@ bool RewardConditions::read_list(std::span<const std::byte> blob,
         // Every expression attached to a progression reward must hold.
         if (i != 0
             && !append(instructions,
-                       domain::Instruction{
-                           static_cast<std::uint32_t>(tables::UnlockOpcode::logicalAnd), 0},
+                       domain::Instruction{unlocks::Opcode::logicalAnd, unlocks::Bank::none, 0},
                        output.size())) {
             return false;
         }
@@ -501,30 +493,29 @@ bool RewardBuild::read_item(std::uint32_t hash,
                             std::span<const std::byte> blob,
                             domain::Item& item) noexcept {
     std::uint16_t acquired = domain::kAbsent;
-    std::int64_t relative = 0;
+    std::int64_t wrapper = 0;
     std::uint32_t cls = 0;
-    if (!tables::read(blob, kWrapperField, relative)
+    if (!tables::read(blob, kWrapperField, wrapper)
         || !tables::read(blob, kAcquiredFlagField, acquired)) {
         return false;
     }
     item.definitionHash = hash;
     if (acquired != domain::kAbsent) {
-        domain::Instruction flag{static_cast<std::uint32_t>(tables::UnlockOpcode::flag), acquired};
-        if (!conditions.bind(flag) || !domain::valid_instruction(flag)) {
+        domain::Instruction flag{};
+        if (!conditions.bind(tables::kUnlockReadFlagOpcode, acquired, flag)
+            || !unlocks::valid(flag)) {
             return false;
         }
-        if (flag.opcode == static_cast<std::uint32_t>(domain::BankRead::accountFlag)) {
+        if (flag.bank == unlocks::Bank::account) {
             item.acquiredFlag = static_cast<std::uint16_t>(flag.operand);
         }
     }
-    if (relative != 0) {
-        if (relative < 0 || static_cast<std::uint64_t>(relative) > blob.size()
-            || kWrapperField > blob.size() - static_cast<std::size_t>(relative)) {
-            return false;
-        }
-        const auto at = kWrapperField + static_cast<std::size_t>(relative);
+    // A zero delta means the item is not a wrapper.
+    if (wrapper != 0) {
+        std::size_t at = 0;
         tables::Array selections{};
-        if (!tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kWrapperClass
+        if (!tables::relative(blob, kWrapperField, at) || at < kDefinitionClassPrefixSize
+            || !tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kWrapperClass
             || !tables::read(blob, at, item.poolIndex)
             || !tables::read(blob, at + kWrapperFlagsOffset, item.flags)
             || !tables::read_array(
@@ -581,7 +572,7 @@ bool RewardBuild::publish() noexcept {
                          && (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();
+                valid &= domain::valid_socket(socket, items_.size());
             }
             if (valid) {
                 auto retained = row;

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

@@ -1,5 +1,11 @@
 #pragma once
 
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+#include <vector>
+
 #include "../../../../middleware/content/packages/reader/reader.h"
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
 #include "../../../../state/account/account_state.h"
@@ -12,15 +18,21 @@ struct SlotMaps;
 /** Resolves expression references and unlock slots once for a package pass. */
 class RewardConditions {
 public:
+    /** Reads the root's flag, value, expression and class tables; false when one is missing. */
     [[nodiscard]] bool load(const middleware::content::packages::reader::Source& source,
                             middleware::content::packages::reader::Scratch& scratch,
                             std::span<const std::byte> root,
                             const SlotMaps& maps) noexcept;
-    [[nodiscard]] bool bind(state::build_data::rewards::Instruction& instruction) const noexcept;
+    /** Binds one native instruction; flag and value slots resolve to their saved bank. */
+    [[nodiscard]] bool bind(std::uint32_t native,
+                            std::uint32_t operand,
+                            state::build_data::rewards::Instruction& instruction) const noexcept;
+    /** Appends one expanded expression to the bank; the bank is unchanged on failure. */
     [[nodiscard]] bool read(std::span<const std::byte> blob,
                             std::size_t at,
                             std::vector<state::build_data::rewards::Instruction>& bank,
                             state::build_data::rewards::Range& range) const noexcept;
+    /** Expands a list of expressions into one program where every expression must hold. */
     [[nodiscard]] bool read_list(std::span<const std::byte> blob,
                                  std::size_t at,
                                  std::span<state::build_data::rewards::Instruction> output,
@@ -48,12 +60,16 @@ private:
 class RewardBuild {
 public:
     RewardConditions conditions;
+    /** Reads every reward pool; entries that fail to read are skipped and counted. */
     [[nodiscard]] bool load(const middleware::content::packages::reader::Source& source,
                             middleware::content::packages::reader::Scratch& scratch,
                             std::span<const std::byte> root,
                             const SlotMaps& maps) noexcept;
+    /** Sizes the dense item rows before the item walk. */
     [[nodiscard]] bool begin_items(std::size_t count) noexcept;
+    /** Records one item's wrapper and acquisition flag; an unreadable item stays unavailable. */
     void item(std::uint16_t index, std::uint32_t hash, std::span<const std::byte> blob) noexcept;
+    /** Drops rows naming items or pools that were not read, then publishes the banks. */
     [[nodiscard]] bool publish() noexcept;
 
 private:

+ 45 - 34
Sunrise/src/client/content/items/packages/package_season_pass_build.cpp

@@ -1,10 +1,10 @@
 #include <algorithm>
 #include <limits>
 
-#include "../../../../middleware/content/packages/tables/internal.h"
+#include "../../../../core/logging/log.h"
+#include "../../../../middleware/content/packages/tables/field_reader.h"
 #include "../../../../state/build_data/season_pass/season_pass_catalog.h"
 #include "../../../../state/progression/season_pass_reward_catalog.h"
-#include "core/logging/log.h"
 #include "internal.h"
 #include "package_reward_build.h"
 
@@ -13,6 +13,10 @@ namespace {
 
 namespace domain = state::build_data::season_pass;
 
+/**
+ * Reads one pass row: item, rank, quantity and claim flag, then its sockets and conditions.
+ * @return False when any field is unreadable or out of range.
+ */
 bool read_season_reward(Storage& storage,
                         std::span<const std::byte> progressionTable,
                         const tables::Array& itemRows,
@@ -21,17 +25,19 @@ bool read_season_reward(Storage& storage,
     std::uint32_t rank = 0;
     std::uint32_t itemIndex = 0;
     std::uint32_t claimSlot = 0;
-    tables::IndexRow entry{};
     if (!tables::read(progressionTable, at + tables::kProgressionRewardRankOffset, rank)
         || !tables::read(
             progressionTable, at + tables::kProgressionRewardItemIndexOffset, itemIndex)
         || !tables::read(
             progressionTable, at + tables::kProgressionRewardQuantityOffset, reward.quantity)
         || !tables::read(
-            progressionTable, at + tables::kProgressionRewardClaimSlotOffset, claimSlot)
-        || 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)()
+            progressionTable, at + tables::kProgressionRewardClaimSlotOffset, claimSlot)) {
+        return false;
+    }
+
+    // Native fields are 32-bit, the stored row keeps the narrower native index widths.
+    tables::IndexRow entry{};
+    if (rank > (std::numeric_limits<std::uint8_t>::max)()
         || itemIndex > (std::numeric_limits<std::uint16_t>::max)()
         || claimSlot > (std::numeric_limits<std::uint16_t>::max)()
         || !tables::index_row(storage.itemIndexTable, itemRows, itemIndex, entry)) {
@@ -40,45 +46,50 @@ bool read_season_reward(Storage& storage,
     reward.itemHash = entry.definitionHash;
     reward.itemIndex = static_cast<std::uint16_t>(itemIndex);
     reward.requiredRank = static_cast<std::uint8_t>(rank);
+
+    // A reward with no claim flag carries slot 0, any other slot must map to the account bank.
+    if (claimSlot != 0) {
+        reward.claimFlagIndex =
+            bank_index(storage.slotMaps.accountFlag, static_cast<std::int32_t>(claimSlot));
+        if (reward.claimFlagIndex == domain::kUnavailableFlagIndex) {
+            return false;
+        }
+    }
+
     std::size_t socketCount = 0;
+    std::size_t conditionCount = 0;
     if (!read_reward_sockets(progressionTable,
                              at + tables::kProgressionRewardSocketsOffset,
                              reward.sockets,
-                             socketCount)) {
+                             socketCount)
+        || !storage.rewardBuild.conditions.read_list(
+            progressionTable,
+            at + tables::kProgressionRewardConditionsOffset,
+            reward.condition,
+            conditionCount)) {
         return false;
     }
     reward.socketCount = static_cast<std::uint8_t>(socketCount);
-    std::size_t conditionCount = 0;
-    if (!storage.rewardBuild.conditions.read_list(progressionTable,
-                                                  at + tables::kProgressionRewardConditionsOffset,
-                                                  reward.condition,
-                                                  conditionCount)) {
-        return false;
-    }
     reward.conditionCount = static_cast<std::uint8_t>(conditionCount);
-    // A reward with no claim flag carries slot 0.
-    if (claimSlot != 0) {
-        reward.claimFlagIndex =
-            bank_index(storage.slotMaps.accountFlag, static_cast<std::int32_t>(claimSlot));
-    }
-
-    if (!domain::valid(std::span(&reward, 1))
-        || (claimSlot != 0 && reward.claimFlagIndex == domain::kUnavailableFlagIndex)
-        || !std::all_of(reward.sockets.begin(),
-                        reward.sockets.begin() + reward.socketCount,
-                        [&](const auto& socket) {
-                            return socket.socketType != state::build_data::rewards::kAbsent
-                                   && (socket.plugItem == state::build_data::rewards::kAbsent
-                                       || socket.plugItem < itemRows.count);
-                        })) {
-        return false;
-    }
-    return true;
+    return domain::valid(std::span(&reward, 1))
+           && std::all_of(reward.sockets.begin(),
+                          reward.sockets.begin() + reward.socketCount,
+                          [&itemRows](const state::build_data::rewards::SocketOverride& socket) {
+                              return state::build_data::rewards::valid_socket(socket,
+                                                                              itemRows.count);
+                          });
 }
 
 } // namespace
 
-/** Preserves native reward order for opcode-2400 claim indices. */
+/**
+ * Reads the season pass reward list in native order, which the opcode-2400 claim indexes.
+ * A row that fails to read is kept as an unavailable row so later claim indices stay aligned.
+ * @param source Installed package source.
+ * @param storage Pass storage receiving the reward rows.
+ * @param root Investment root bytes.
+ * @return True when the reward list read and produced at least one row.
+ */
 bool build_season_pass(const reader::Source& source,
                        Storage& storage,
                        std::span<const std::byte> root) noexcept {

+ 1 - 20
Sunrise/src/middleware/content/packages/tables/activity_table.h

@@ -4,6 +4,7 @@
 
 #include "../../../../state/build_data/activities/activity_catalog.h"
 #include "definition_index_table.h"
+#include "field_reader.h"
 
 namespace sunrise::middleware::content::packages::tables::activities {
 /** Public activity index, NOT the package-definition index in root slot 29. */
@@ -32,26 +33,6 @@ template <class T>
     std::memcpy(&value, bytes.data() + offset, sizeof(T));
     return true;
 }
-/**
- * Resolves a self-relative field.
- * @param field Offset of the 8-byte signed delta.
- * @param target Receives the resolved offset.
- * @return False when the delta is zero or the target falls outside the blob.
- */
-[[nodiscard]] inline bool
-relative(std::span<const std::byte> bytes, std::size_t field, std::size_t& target) noexcept {
-    std::int64_t delta{};
-    if (!read(bytes, field, delta) || delta == 0
-        || field > static_cast<std::size_t>((std::numeric_limits<std::int64_t>::max)())) {
-        return false;
-    }
-    const auto base = static_cast<std::int64_t>(field);
-    if (delta < -base || delta > (std::numeric_limits<std::int64_t>::max)() - base) {
-        return false;
-    }
-    target = static_cast<std::size_t>(base + delta);
-    return target < bytes.size();
-}
 /**
  * Parses the dense public activity table.
  * @param bytes The table blob.

+ 1 - 0
Sunrise/src/middleware/content/packages/tables/definition_index_table.cpp

@@ -125,6 +125,7 @@ bool find_optional_array_at(std::span<const std::byte> blob,
     return resolve_descriptor(blob, descriptorOffset, true, output);
 }
 
+/** Reads an optional typed array and checks every fixed-stride row lies in its blob. */
 bool read_array(std::span<const std::byte> blob,
                 std::size_t at,
                 std::uint32_t elementClass,

+ 18 - 16
Sunrise/src/middleware/content/packages/tables/definition_index_table.h

@@ -4,8 +4,6 @@
 #include <cstdint>
 #include <span>
 
-#include "unlock_opcode.h"
-
 namespace sunrise::middleware::content::packages::tables {
 
 /** Definition tags sit in this closed range. */
@@ -97,26 +95,27 @@ 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;
+/**
+ * Record and node field detection accepts a field only when every opcode is at most 20. This is a
+ * detection bound, not the opcode range: installed reward conditions also use opcode 22.
+ */
+inline constexpr std::uint32_t kUnlockOpcodeCeiling = 20;
 /** The opcode that reads a value slot. */
-inline constexpr std::uint32_t kUnlockReadValueOpcode =
-    static_cast<std::uint32_t>(UnlockOpcode::loadValue);
+inline constexpr std::uint32_t kUnlockReadValueOpcode = 10;
 /** The opcode that tests a flag. */
-inline constexpr std::uint32_t kUnlockReadFlagOpcode =
-    static_cast<std::uint32_t>(UnlockOpcode::flag);
+inline constexpr std::uint32_t kUnlockReadFlagOpcode = 1;
 /** The opcode that pushes a literal. */
-inline constexpr std::uint32_t kUnlockLiteralOpcode =
-    static_cast<std::uint32_t>(UnlockOpcode::constant);
+inline constexpr std::uint32_t kUnlockLiteralOpcode = 11;
+/** The opcode that expands a shared expression row. Its operand is the row index. */
+inline constexpr std::uint32_t kUnlockExpressionOpcode = 12;
 /** The opcode that tests greater than or equal. */
-inline constexpr std::uint32_t kUnlockGreaterEqualOpcode =
-    static_cast<std::uint32_t>(UnlockOpcode::greaterOrEqual);
+inline constexpr std::uint32_t kUnlockGreaterEqualOpcode = 14;
 /** The opcode that inverts the value on top of the stack. Its operand is unused. */
-inline constexpr std::uint32_t kUnlockNotOpcode =
-    static_cast<std::uint32_t>(UnlockOpcode::logicalNot);
+inline constexpr std::uint32_t kUnlockNotOpcode = 2;
 /** The opcode that folds the top two values with logical and. */
-inline constexpr std::uint32_t kUnlockAndOpcode =
-    static_cast<std::uint32_t>(UnlockOpcode::logicalAnd);
+inline constexpr std::uint32_t kUnlockAndOpcode = 4;
 /** The opcode that tests the top two values for equality. */
-inline constexpr std::uint32_t kUnlockEqualOpcode = static_cast<std::uint32_t>(UnlockOpcode::equal);
+inline constexpr std::uint32_t kUnlockEqualOpcode = 8;
 /** High half every installed array marker and element class carries. */
 inline constexpr std::uint32_t kDefinitionClassHigh = 0x8080U;
 /** Shift that leaves `kDefinitionClassHigh` from a marker or element class. */
@@ -135,7 +134,10 @@ inline constexpr std::size_t kNodeExpressionFieldAlternate = 48;
 /** Records a node owns, four bytes each as a row then a gate. */
 inline constexpr std::size_t kNodeChildRecordField = 136;
 inline constexpr std::size_t kNodeChildRecordStride = 4;
-/** Array descriptor of the account object's profile unlock flag mapping table. */
+/**
+ * Array descriptor of the profile flag mapping table, between the account (+8) and character (+40)
+ * maps. Profile flags are account-wide but saved in their own bank, not the account flag bank.
+ */
 inline constexpr std::size_t kProfileFlagMapDescriptor = 24;
 /** Array descriptor of the character object's flag mapping table, sized to that bank. */
 inline constexpr std::size_t kCharacterFlagMapDescriptor = 40;

+ 23 - 0
Sunrise/src/middleware/content/packages/tables/field_reader.h

@@ -1,7 +1,9 @@
 #pragma once
 
 #include <cstddef>
+#include <cstdint>
 #include <cstring>
+#include <limits>
 #include <span>
 
 namespace sunrise::middleware::content::packages::tables {
@@ -23,4 +25,25 @@ read(std::span<const std::byte> blob, std::size_t offset, Value& value) noexcept
     return true;
 }
 
+/**
+ * Resolves a self-relative field.
+ * @param field Offset of the 8-byte signed delta.
+ * @param target Receives the resolved offset.
+ * @return False when the delta is zero or the target falls outside the blob.
+ */
+[[nodiscard]] inline bool
+relative(std::span<const std::byte> bytes, std::size_t field, std::size_t& target) noexcept {
+    std::int64_t delta{};
+    if (!read(bytes, field, delta) || delta == 0
+        || field > static_cast<std::size_t>((std::numeric_limits<std::int64_t>::max)())) {
+        return false;
+    }
+    const auto base = static_cast<std::int64_t>(field);
+    if (delta < -base || delta > (std::numeric_limits<std::int64_t>::max)() - base) {
+        return false;
+    }
+    target = static_cast<std::size_t>(base + delta);
+    return target < bytes.size();
+}
+
 } // namespace sunrise::middleware::content::packages::tables

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

@@ -2,7 +2,6 @@
 
 #include <limits>
 
-#include "../../../../state/build_data/inventory/buckets/definition.h"
 #include "definition_index_table.h"
 #include "internal.h"
 
@@ -58,6 +57,9 @@ constexpr std::uint32_t kItemPresenceFlagClass = 0x80807D4BU;
 constexpr std::size_t kItemPresenceFlagStride = sizeof(std::uint16_t);
 /** Authored value/flag slots must fit the nonnegative range of a signed 16-bit mapping. */
 constexpr std::uint16_t kUnlockSlotLimit = 0x8000U;
+/** Policy: pursuits without presence flags need a bucket-37 root with no objective block. */
+constexpr std::uint8_t kSeparateQuestRootBucketId = 37;
+
 /** Map +40 has no supported save bank; a matching slot makes initialization unsafe. */
 constexpr std::size_t kThirdValueMapDescriptor = 40;
 /** Map +56 is also checked for duplicate slots but has no supported save bank. */
@@ -197,7 +199,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 != state::build_data::inventory::buckets::kNonInventoryBucketId
+            || parentBucket != kSeparateQuestRootBucketId
             || !read(parent, kItemObjectiveBlockOffset, parentObjective) || parentObjective != 0) {
             return {};
         }

+ 1 - 4
Sunrise/src/middleware/content/packages/tables/unlock_expression.h

@@ -9,9 +9,6 @@
 
 namespace sunrise::middleware::content::packages::tables {
 
-/** Upstream's record/node field heuristic; this is not the reward evaluator's opcode limit. */
-inline constexpr std::uint32_t kExpressionDetectionOpcodeCeiling = 20;
-
 /**
  * Reads the first operand of a given opcode out of one expression field.
  * The same field holds a value read on one row and a flag test on another, so the opcode is asked
@@ -54,7 +51,7 @@ inline constexpr std::uint32_t kExpressionDetectionOpcodeCeiling = 20;
         const std::size_t at = base + static_cast<std::size_t>(index) * kUnlockInstructionStride;
         std::memcpy(&instruction, table.data() + at, sizeof instruction);
         std::memcpy(&operand, table.data() + at + kUnlockInstructionOperandOffset, sizeof operand);
-        if (instruction > kExpressionDetectionOpcodeCeiling) {
+        if (instruction > kUnlockOpcodeCeiling) {
             return false;
         }
         if (instruction == opcode && operand <= static_cast<std::uint32_t>(INT16_MAX)) {

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

@@ -1,39 +0,0 @@
-#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

+ 10 - 4
Sunrise/src/server/bap/bap_world_reward_queue.cpp

@@ -1,10 +1,11 @@
 #include <memory>
-#include <mutex>
 #include <new>
+#include <span>
 
+#include "../../core/logging/log.h"
+#include "../../middleware/crypto/random_bytes.h"
 #include "../../state/build_data/runtime.h"
 #include "../../state/investment/store_internal.h"
-#include "core/logging/log.h"
 #include "internal.h"
 
 namespace sunrise::server::bap {
@@ -27,10 +28,14 @@ bool commit_world_reward(const WorldRewardRequest& request) noexcept {
         } else {
             const std::unique_ptr<state::PendingRecordRewardGrant> grant(
                 new (std::nothrow) state::PendingRecordRewardGrant);
-            const char* reason = "reward_allocation";
-            committed = grant && request.quantity > 0
+            std::uint64_t seed = 0;
+            const bool seeded =
+                middleware::crypto::random::fill(std::as_writable_bytes(std::span(&seed, 1)));
+            const char* reason = seeded ? "reward_allocation" : "random_source";
+            committed = seeded && grant && request.quantity > 0
                         && state::prepare_item_reward(request.itemDefinitionIndex,
                                                       static_cast<std::uint32_t>(request.quantity),
+                                                      seed,
                                                       *grant,
                                                       &reason);
             if (committed) {
@@ -68,6 +73,7 @@ bool enqueue_world_reward(std::uint16_t definitionIndex,
 
 } // namespace
 
+/** Logs one refused reward stage with its native index and a specific reason. */
 void report_reward_refusal(const char* stage, std::uint16_t index, const char* reason) noexcept {
     core::log::writef(core::log::Channel::server,
                       core::log::Level::warn,

+ 5 - 0
Sunrise/src/server/bap/encrypted/push/snapshot/family4_reward_updates.cpp

@@ -21,6 +21,11 @@ namespace sunrise::server::bap::encrypted::push::snapshot {
 
 namespace family4_datagen = middleware::datagen::family4;
 
+// Every row one reward grant publishes is named in the character or the profile change list.
+static_assert(state::kRecordRewardGrantCapacity
+              == family4_datagen::character::layout::kInventoryChangeRecordCapacity
+                     + family4_datagen::account::layout::kProfileInventoryChangeRecordCapacity);
+
 /** Builds all record rewards and the pending claim into one Family-4 revision. */
 bool prepare_record_reward_grant(
     Scratch& scratch,

+ 14 - 8
Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp

@@ -4,17 +4,17 @@
 #include <limits>
 #include <memory>
 #include <new>
+#include <span>
 
 #include "../../../../core/logging/log.h"
+#include "../../../../middleware/crypto/random_bytes.h"
 #include "../../../../middleware/secure_channel/runtime.h"
 #include "../../../../state/account/account_state.h"
 #include "../../../../state/activity/destination/definition.h"
 #include "../../../../state/activity/runtime.h"
-#include "../../../../state/build_data/runtime.h"
 #include "../../../../state/runtime/runtime.h"
 #include "../internal.h"
 #include "../push/activity/activity_keepalive_push.h"
-#include "queuez_reward_staging.h"
 #include "queuez_state_validation.h"
 #include "state/investment/store_internal.h"
 
@@ -56,23 +56,29 @@ selected_character(const state::AccountState& account) noexcept {
     return nullptr;
 }
 
-bool consume_world_record_reward(const WorldRewardRequest& request,
-                                 Session& session,
-                                 Scratch& scratch,
-                                 std::span<std::byte> response,
-                                 std::size_t& written,
-                                 bool& touchesScratch) noexcept {
+/** Publishes and commits one world reward resolved through its installed reward definition. */
+[[nodiscard]] bool consume_world_record_reward(const WorldRewardRequest& request,
+                                               Session& session,
+                                               Scratch& scratch,
+                                               std::span<std::byte> response,
+                                               std::size_t& written,
+                                               bool& touchesScratch) noexcept {
     const std::unique_ptr<state::PendingRecordRewardGrant> pending(
         new (std::nothrow) state::PendingRecordRewardGrant);
     const auto fail = [&](const char* reason) noexcept {
         report_reward_refusal("world_publish", request.itemDefinitionIndex, reason);
         return false;
     };
+    std::uint64_t seed = 0;
+    if (!middleware::crypto::random::fill(std::as_writable_bytes(std::span(&seed, 1)))) {
+        return fail("random_source");
+    }
     const char* reason = "transaction";
     state::investment::store::Transaction transaction;
     if (!pending || !transaction.ready() || request.quantity <= 0
         || !state::prepare_item_reward(request.itemDefinitionIndex,
                                        static_cast<std::uint32_t>(request.quantity),
+                                       seed,
                                        *pending,
                                        &reason)) {
         return fail(reason);

+ 1 - 0
Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h

@@ -188,6 +188,7 @@ namespace sunrise::server::bap::encrypted::queuez {
                                              std::span<const std::uint64_t> appendedResidents,
                                              RecordRewardGrant& grant) noexcept;
 
+/** Stages a prepared record-reward grant, appending the residents its new instances need. */
 [[nodiscard]] bool stage_record_reward_grant(const SessionState& before,
                                              const state::PendingRecordRewardGrant& pending,
                                              RecordRewardGrant& grant) noexcept;

+ 7 - 1
Sunrise/src/server/web_service/web_service_record_actions.cpp

@@ -6,6 +6,7 @@
 #include <string_view>
 
 #include "../../core/logging/log.h"
+#include "../../middleware/crypto/random_bytes.h"
 #include "../../middleware/web_service/messages/opcode1801.h"
 #include "../../middleware/web_service/messages/opcode1821.h"
 #include "../../middleware/web_service/messages/opcode2400.h"
@@ -156,8 +157,13 @@ void claim_season_pass_reward(const middleware::web_service::Message& message,
     if (grant == nullptr) {
         return fail("storage");
     }
+    std::uint64_t seed = 0;
+    if (!middleware::crypto::random::fill(std::as_writable_bytes(std::span(&seed, 1)))) {
+        clear_mutation(outcome);
+        return fail("random_source");
+    }
     const char* reason = nullptr;
-    if (!state::prepare_season_pass_reward(request.rewardIndex, *grant, &reason)) {
+    if (!state::prepare_season_pass_reward(request.rewardIndex, seed, *grant, &reason)) {
         clear_mutation(outcome);
         return fail(reason != nullptr ? reason : "reward_preparation");
     }

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

@@ -287,9 +287,12 @@ bool valid_domains(const BuildIdentity& build, Domains domains) noexcept {
         || !hash_names::valid(domains.hashNames)) {
         return false;
     }
+    // Reward rows are dense by item index, so each must name its item or be unavailable.
+    if (domains.rewardItems.size() != domains.items.size()) {
+        return false;
+    }
     for (std::size_t index = 0; index < domains.items.size(); ++index) {
         if (domains.items[index].definitionIndex != index
-            || domains.rewardItems.size() != domains.items.size()
             || (domains.rewardItems[index].definitionHash != 0
                 && domains.rewardItems[index].definitionHash
                        != domains.items[index].definitionHash)) {
@@ -305,10 +308,7 @@ bool valid_domains(const BuildIdentity& build, Domains domains) noexcept {
             return false;
         }
         for (std::size_t i = 0; i < reward.socketCount; ++i) {
-            const auto& socket = reward.sockets[i];
-            if (socket.socketType == rewards::kAbsent
-                || (socket.plugItem != rewards::kAbsent
-                    && socket.plugItem >= domains.items.size())) {
+            if (!rewards::valid_socket(reward.sockets[i], domains.items.size())) {
                 return false;
             }
         }

+ 19 - 3
Sunrise/src/state/build_data/cache/records/cache_reward_records.cpp

@@ -2,6 +2,7 @@
 
 namespace sunrise::state::build_data::cache::records {
 
+/** Encodes one pool identity and its member range. */
 bool encode(const rewards::Pool& value, RewardPoolRecord& record) noexcept {
     record = {};
     record.definitionHash = value.definitionHash;
@@ -9,6 +10,7 @@ bool encode(const rewards::Pool& value, RewardPoolRecord& record) noexcept {
     return true;
 }
 
+/** Decodes one pool; the complete-domain validator checks its member range. */
 bool decode(const RewardPoolRecord& record, rewards::Pool& value) noexcept {
     value = {};
     value.definitionHash = record.definitionHash;
@@ -16,6 +18,7 @@ bool decode(const RewardPoolRecord& record, rewards::Pool& value) noexcept {
     return true;
 }
 
+/** Encodes one pool entry with its dependent bank ranges. */
 bool encode(const rewards::Entry& value, RewardEntryRecord& record) noexcept {
     record = {};
     record.itemIndex = value.itemIndex;
@@ -31,6 +34,7 @@ bool encode(const rewards::Entry& value, RewardEntryRecord& record) noexcept {
     return true;
 }
 
+/** Decodes one pool entry, refusing an absent-bank marker without a supplemental reference. */
 bool decode(const RewardEntryRecord& record, rewards::Entry& value) noexcept {
     value = {};
     if (record.supplementalMissing > 1
@@ -50,6 +54,7 @@ bool decode(const RewardEntryRecord& record, rewards::Entry& value) noexcept {
     return true;
 }
 
+/** Encodes one item's wrapper and acquisition flag. */
 bool encode(const rewards::Item& value, RewardItemRecord& record) noexcept {
     record = {};
     record.definitionHash = value.definitionHash;
@@ -63,6 +68,7 @@ bool encode(const rewards::Item& value, RewardItemRecord& record) noexcept {
     return true;
 }
 
+/** Decodes one item's wrapper; the complete-domain validator checks its references. */
 bool decode(const RewardItemRecord& record, rewards::Item& value) noexcept {
     value = {};
     value.definitionHash = record.definitionHash;
@@ -76,20 +82,27 @@ bool decode(const RewardItemRecord& record, rewards::Item& value) noexcept {
     return true;
 }
 
+/** Encodes one bound condition instruction. */
 bool encode(const rewards::Instruction& value, RewardInstructionRecord& record) noexcept {
     record = {};
-    record.opcode = value.opcode;
+    record.opcode = static_cast<std::uint8_t>(value.opcode);
+    record.bank = static_cast<std::uint8_t>(value.bank);
     record.operand = value.operand;
     return true;
 }
 
+/** Decodes one bound condition instruction, refusing an unknown opcode or bank. */
 bool decode(const RewardInstructionRecord& record, rewards::Instruction& value) noexcept {
     value = {};
-    value.opcode = record.opcode;
+    if (record.reserved != 0 || !unlocks::decode_opcode(record.opcode, value.opcode)) {
+        return false;
+    }
+    value.bank = static_cast<unlocks::Bank>(record.bank);
     value.operand = record.operand;
-    return true;
+    return unlocks::valid(value);
 }
 
+/** Encodes one conditional weight modifier. */
 bool encode(const rewards::Modifier& value, RewardModifierRecord& record) noexcept {
     record = {};
     record.condition = {value.condition.first, value.condition.count};
@@ -98,6 +111,7 @@ bool encode(const rewards::Modifier& value, RewardModifierRecord& record) noexce
     return true;
 }
 
+/** Decodes one weight modifier; the complete-domain validator checks its condition range. */
 bool decode(const RewardModifierRecord& record, rewards::Modifier& value) noexcept {
     value = {};
     value.condition = {record.condition.first, record.condition.count};
@@ -106,6 +120,7 @@ bool decode(const RewardModifierRecord& record, rewards::Modifier& value) noexce
     return true;
 }
 
+/** Encodes one socket override. */
 bool encode(const rewards::SocketOverride& value, RewardSocketOverrideRecord& record) noexcept {
     record = {};
     record.socketType = value.socketType;
@@ -116,6 +131,7 @@ bool encode(const rewards::SocketOverride& value, RewardSocketOverrideRecord& re
     return true;
 }
 
+/** Decodes one socket override; the complete-domain validator checks its plug reference. */
 bool decode(const RewardSocketOverrideRecord& record, rewards::SocketOverride& value) noexcept {
     value = {};
     value.socketType = record.socketType;

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

@@ -32,7 +32,7 @@ namespace sunrise::state::build_data::cache::records {
 /** These 8 ASCII bytes mark a Sunrise build-data file. */
 inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', 'E', 'B'};
 /** Bump when stored layouts or extracted values change; other versions are rebuilt. */
-inline constexpr std::uint32_t kCacheFormatVersion = 69;
+inline constexpr std::uint32_t kCacheFormatVersion = 70;
 /** 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. */
@@ -320,7 +320,10 @@ struct RewardSocketOverrideRecord {
 
 /** One normalized reward-condition instruction. */
 struct RewardInstructionRecord {
-    std::uint32_t opcode{};
+    std::uint8_t opcode{};
+    std::uint8_t bank{};
+    /** Must be zero, so the packed instruction always matches. */
+    std::uint16_t reserved{};
     std::uint32_t operand{};
 };
 
@@ -664,7 +667,8 @@ static_assert(sizeof(RewardModifierRecord)
               == sizeof(RewardRangeRecord) + sizeof(std::uint16_t) + sizeof(float));
 static_assert(sizeof(RewardSocketOverrideRecord)
               == 4 * sizeof(std::uint16_t) + sizeof(std::uint32_t));
-static_assert(sizeof(RewardInstructionRecord) == 2 * sizeof(std::uint32_t));
+static_assert(sizeof(RewardInstructionRecord)
+              == 2 * sizeof(std::uint8_t) + sizeof(std::uint16_t) + sizeof(std::uint32_t));
 
 static_assert(sizeof(Prefix) == kCacheMagic.size() + sizeof(std::uint32_t));
 static_assert(sizeof(InvestmentConstants)

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

@@ -25,8 +25,11 @@ 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;
+/**
+ * Season Pass perks share this 5-slot character bucket with quest roots. A reward item here that
+ * carries an acquisition flag is granted by setting that flag rather than as an inventory row.
+ */
+inline constexpr std::uint8_t kPerkBucketId = 37;
 /** Leaving out the unavailable id leaves at most 255 unique bucket records. */
 inline constexpr std::size_t kDescriptorCapacity = kUnavailableBucketId;
 /** Signed -1 marks a bucket that has no equipment slot. */

+ 25 - 23
Sunrise/src/state/build_data/rewards/definition.h

@@ -5,22 +5,31 @@
 #include <cstdint>
 #include <span>
 
+#include "../../unlocks/unlocks_expression.h"
+#include "../items/details/definition.h"
+#include "../items/item_catalog.h"
+
 namespace sunrise::state::build_data::rewards {
 
 /** Native references use all bits set for an absent row. */
 inline constexpr std::uint16_t kAbsent = 0xFFFF;
-/** Bounds cover signed native indices and the flat banks they reference. */
+/** The shipped build declares 979 pools. The domain leaves room above that. */
 inline constexpr std::size_t kPoolCapacity = 4096;
+/** Item rows are dense over the installed item-definition table. */
+inline constexpr std::size_t kItemCapacity = items::kDefinitionCapacity;
+/**
+ * The shipped build declares 10,549 entries, 14,185 condition instructions, 3,714 modifiers, and
+ * 1,812 socket overrides. Each bank leaves room above that.
+ */
 inline constexpr std::size_t kEntryCapacity = 32768;
-inline constexpr std::size_t kItemCapacity = 32768;
-inline constexpr std::size_t kInstructionCapacity = 131072;
+inline constexpr std::size_t kInstructionCapacity = 32768;
 inline constexpr std::size_t kModifierCapacity = 32768;
 inline constexpr std::size_t kSocketOverrideCapacity = 32768;
-/** Native reward overrides address the 12 ordinary sockets on an item. */
-inline constexpr std::size_t kSocketsPerItem = 12;
-/** A wrapper can select independently from four reward categories. */
+/** Reward overrides address the ordinary socket lanes an item's initial plugs fill. */
+inline constexpr std::size_t kSocketsPerItem = items::details::kInitialPlugCapacity;
+/** Category selections one wrapper declares. The most any shipped wrapper declares is four. */
 inline constexpr std::size_t kSelectionCapacity = 4;
-/** A resolved grant fits one bounded inventory transaction. */
+/** Rows one grant may publish: one push's 16-record character and profile change lists. */
 inline constexpr std::size_t kGrantCapacity = 32;
 /** Nested pools and referenced conditions share a bounded traversal depth. */
 inline constexpr std::size_t kTraversalDepth = 32;
@@ -30,22 +39,8 @@ struct Range {
     std::uint32_t count{};
 };
 
-struct Instruction {
-    std::uint32_t opcode{};
-    std::uint32_t operand{};
-};
-
-/** Resolved bank reads occupy a separate range from native expression operators. */
-enum class BankRead : std::uint32_t {
-    accountFlag = 256,
-    characterFlag,
-    accountValue,
-    characterValue,
-    externalFlag,
-    externalValue,
-    characterClass,
-    profileFlag,
-};
+/** Reward and pass conditions are unlock expressions bound to their saved banks. */
+using Instruction = unlocks::Instruction;
 
 /** A fixed override names its plug directly rather than selecting from a set. */
 inline constexpr std::uint32_t kFixedPlugSelection = 0xFFFFFFFFU;
@@ -113,4 +108,11 @@ template <typename T> [[nodiscard]] constexpr bool fits(Range range, std::span<T
     return range.first <= bank.size() && range.count <= bank.size() - range.first;
 }
 
+/** An override names a socket type and, when it fixes a plug, an item that was read. */
+[[nodiscard]] constexpr bool valid_socket(const SocketOverride& socket,
+                                          std::size_t itemCount) noexcept {
+    return socket.socketType != kAbsent
+           && (socket.plugItem == kAbsent || socket.plugItem < itemCount);
+}
+
 } // namespace sunrise::state::build_data::rewards

+ 30 - 0
Sunrise/src/state/build_data/rewards/reward_build_data_runtime.cpp

@@ -0,0 +1,30 @@
+#include "../runtime.h"
+#include "../runtime/persistence/publication_transaction.h"
+#include "reward_catalog.h"
+
+namespace sunrise::state::build_data {
+
+/** @return True when the reward pools and item wrappers are published. */
+bool reward_definitions_ready() noexcept {
+    return rewards::ready();
+}
+
+/** Publishes the complete reward graph in one step. */
+bool publish_reward_definitions(rewards::View definitions) noexcept {
+    runtime::persistence::Transaction transaction;
+    return transaction.active()
+           && transaction.finish(rewards::replace(definitions), rewards::clear);
+}
+
+/** Reads one item's reward row. */
+bool find_reward_item(std::uint16_t itemIndex, rewards::Item& item) noexcept {
+    return rewards::find_item(itemIndex, item);
+}
+
+/** Lends the complete reward graph to one callback under the catalog's read lock. */
+bool read_reward_definitions(void* context,
+                             bool (*consume)(void*, rewards::View) noexcept) noexcept {
+    return rewards::read(context, consume);
+}
+
+} // namespace sunrise::state::build_data

+ 18 - 45
Sunrise/src/state/build_data/rewards/reward_catalog.cpp

@@ -6,11 +6,9 @@
 #include <mutex>
 #include <shared_mutex>
 
-#include "../../account/account_state.h"
 #include "../../unlocks/definition.h"
 #include "../table.h"
 #include "core/threading/srw_lock.h"
-#include "middleware/content/packages/tables/unlock_opcode.h"
 
 namespace sunrise::state::build_data::rewards {
 namespace {
@@ -23,6 +21,7 @@ Table<Instruction, kInstructionCapacity> g_instructions;
 Table<Modifier, kModifierCapacity> g_modifiers;
 Table<SocketOverride, kSocketOverrideCapacity> g_sockets;
 
+/** Borrows every published bank; the caller holds the catalog lock. */
 View view() noexcept {
     return {g_pools.rows(),
             g_entries.rows(),
@@ -32,6 +31,7 @@ View view() noexcept {
             g_sockets.rows()};
 }
 
+/** Rejects a pool cycle or a nested path deeper than kTraversalDepth, memoizing subtree heights. */
 bool valid_depth(View data,
                  std::size_t index,
                  std::array<std::uint8_t, kPoolCapacity>& heights,
@@ -60,6 +60,7 @@ bool valid_depth(View data,
     return true;
 }
 
+/** Checks one entry's references, dependent ranges and weight. */
 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())
@@ -68,6 +69,7 @@ bool valid_entry(View data, const Entry& entry) noexcept {
            && fits(entry.sockets, data.sockets) && std::isfinite(entry.weight) && entry.weight >= 0;
 }
 
+/** An unavailable item row carries nothing; an available one names valid banks. */
 bool valid_item(View data, const Item& item) noexcept {
     if (item.definitionHash == 0) {
         return item.poolIndex == kAbsent && item.acquiredFlag == kAbsent
@@ -80,6 +82,7 @@ bool valid_item(View data, const Item& item) noexcept {
 
 } // namespace
 
+/** Discards the reward graph and every borrowed bank. */
 void clear() noexcept {
     const std::lock_guard guard(g_lock);
     g_pools.clear();
@@ -90,51 +93,13 @@ void clear() noexcept {
     g_sockets.clear();
 }
 
+/** @return True once pool and item banks have been published. */
 bool ready() noexcept {
     const std::shared_lock guard(g_lock);
     return g_pools.count() != 0 && g_items.count() != 0;
 }
 
-bool valid_instruction(const Instruction& instruction) noexcept {
-    const auto slot = instruction.operand;
-    switch (static_cast<BankRead>(instruction.opcode)) {
-    case BankRead::accountFlag:
-        return slot < unlocks::kAccountFlagCapacity;
-    case BankRead::profileFlag:
-        return slot < unlocks::kProfileFlagCapacity;
-    case BankRead::characterFlag:
-        return slot < unlocks::kCharacterObjectFlagCapacity;
-    case BankRead::accountValue:
-        return slot < unlocks::kObjectiveValueCapacity;
-    case BankRead::characterValue:
-        return slot < unlocks::kCharacterObjectValueCapacity;
-    case BankRead::characterClass:
-        return slot < kCharacterClassCount;
-    case BankRead::externalFlag:
-    case BankRead::externalValue:
-        return true;
-    default:
-        break;
-    }
-    using Op = middleware::content::packages::tables::UnlockOpcode;
-    switch (static_cast<Op>(instruction.opcode)) {
-    case Op::constant:
-    case Op::logicalNot:
-    case Op::logicalOr:
-    case Op::logicalAnd:
-    case Op::equal:
-    case Op::greaterThan:
-    case Op::greaterOrEqual:
-    case Op::lessThan:
-    case Op::lessOrEqual:
-    case Op::add:
-    case Op::negate:
-        return true;
-    default:
-        return false;
-    }
-}
-
+/** Checks bank ranges, item references and bounded acyclic pool traversal. */
 bool valid(View data) noexcept {
     if (data.pools.empty() || data.pools.size() > kPoolCapacity || data.entries.empty()
         || data.entries.size() > kEntryCapacity || data.items.empty()
@@ -159,7 +124,7 @@ bool valid(View data) noexcept {
             return false;
         }
     }
-    if (!std::all_of(data.instructions.begin(), data.instructions.end(), valid_instruction)) {
+    if (!std::all_of(data.instructions.begin(), data.instructions.end(), unlocks::valid)) {
         return false;
     }
     for (const Modifier& modifier : data.modifiers) {
@@ -168,8 +133,7 @@ bool valid(View data) noexcept {
         }
     }
     for (const SocketOverride& socket : data.sockets) {
-        if (socket.socketType == kAbsent
-            || (socket.plugItem != kAbsent && socket.plugItem >= data.items.size())) {
+        if (!valid_socket(socket, data.items.size())) {
             return false;
         }
     }
@@ -182,6 +146,7 @@ bool valid(View data) noexcept {
     return true;
 }
 
+/** Publishes validated banks together under the catalog lock. */
 bool replace(View data) noexcept {
     if (!valid(data)) {
         return false;
@@ -192,11 +157,13 @@ bool replace(View data) noexcept {
            && g_modifiers.replace(data.modifiers) && g_sockets.replace(data.sockets);
 }
 
+/** Lends every bank to one callback under the read lock. */
 bool read(void* context, bool (*consume)(void*, View) noexcept) noexcept {
     const std::shared_lock guard(g_lock);
     return consume != nullptr && g_pools.count() != 0 && consume(context, view());
 }
 
+/** Copies one extracted item row; unavailable rows are refused. */
 bool find_item(std::uint16_t itemIndex, Item& item) noexcept {
     item = {};
     const std::shared_lock guard(g_lock);
@@ -207,31 +174,37 @@ bool find_item(std::uint16_t itemIndex, Item& item) noexcept {
     return true;
 }
 
+/** Copies every pool; count is zero when the output is too small. */
 bool snapshot(std::span<Pool> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_pools.snapshot(output, count);
 }
 
+/** Copies every entry; count is zero when the output is too small. */
 bool snapshot(std::span<Entry> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_entries.snapshot(output, count);
 }
 
+/** Copies every item row; count is zero when the output is too small. */
 bool snapshot(std::span<Item> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_items.snapshot(output, count);
 }
 
+/** Copies every condition instruction; count is zero when the output is too small. */
 bool snapshot(std::span<Instruction> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_instructions.snapshot(output, count);
 }
 
+/** Copies every modifier; count is zero when the output is too small. */
 bool snapshot(std::span<Modifier> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_modifiers.snapshot(output, count);
 }
 
+/** Copies every socket override; count is zero when the output is too small. */
 bool snapshot(std::span<SocketOverride> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_sockets.snapshot(output, count);

+ 0 - 2
Sunrise/src/state/build_data/rewards/reward_catalog.h

@@ -8,8 +8,6 @@ namespace sunrise::state::build_data::rewards {
 void clear() noexcept;
 /** True once pool and item banks have been published. */
 [[nodiscard]] bool ready() noexcept;
-/** Checks a bound instruction against its saved bank and the native opcode range. */
-[[nodiscard]] bool valid_instruction(const Instruction& instruction) noexcept;
 /** Checks bank ranges, item references and bounded acyclic pool traversal. */
 [[nodiscard]] bool valid(View data) noexcept;
 /** Publishes validated banks together under the catalog lock. */

+ 30 - 2
Sunrise/src/state/build_data/runtime.h

@@ -413,6 +413,36 @@ publish_progression_definitions(std::span<const progressions::Definition> defini
 [[nodiscard]] bool find_season_pass_reward(std::uint16_t rewardIndex,
                                            season_pass::Reward& reward) noexcept;
 
+/** @return Season pass reward rows in State, including unavailable rows. */
+[[nodiscard]] std::size_t season_pass_reward_count() noexcept;
+
+/** @return True when the reward pools and item wrappers are in State. */
+[[nodiscard]] bool reward_definitions_ready() noexcept;
+
+/**
+ * Publishes the complete reward graph in one step.
+ * @param definitions Pools, entries, item rows, conditions, modifiers and socket overrides.
+ * @return True when the graph passes the checks and any needed cache write succeeds.
+ */
+[[nodiscard]] bool publish_reward_definitions(rewards::View definitions) noexcept;
+
+/**
+ * Reads one item's reward row.
+ * @param itemIndex Native item-definition index.
+ * @param item Receives the row.
+ * @return True when the catalog holds an extracted row for that item.
+ */
+[[nodiscard]] bool find_reward_item(std::uint16_t itemIndex, rewards::Item& item) noexcept;
+
+/**
+ * Lends the complete reward graph to one callback under the catalog's read lock.
+ * @param context Caller state passed through unchanged.
+ * @param consume Receives the graph, which stays valid only during the call.
+ * @return The callback's result, or false when no graph is published.
+ */
+[[nodiscard]] bool read_reward_definitions(void* context,
+                                           bool (*consume)(void*, rewards::View) noexcept) noexcept;
+
 /** @return True when the repeatable bounty table is in State. */
 [[nodiscard]] bool repeatable_bounties_ready() noexcept;
 
@@ -750,6 +780,4 @@ publish_vendor_catalog(std::span<const vendors::IndexEntry> index,
  */
 [[nodiscard]] bool persist() noexcept;
 
-/** Publishes a complete, validated reward graph. */
-[[nodiscard]] bool publish_reward_definitions(rewards::View definitions) noexcept;
 } // namespace sunrise::state::build_data

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

@@ -387,10 +387,4 @@ void clear_catalogs() noexcept {
 
 } // namespace runtime
 
-bool publish_reward_definitions(rewards::View definitions) noexcept {
-    runtime::persistence::Transaction transaction;
-    return transaction.active()
-           && transaction.finish(rewards::replace(definitions), rewards::clear);
-}
-
 } // namespace sunrise::state::build_data

+ 19 - 20
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp

@@ -131,7 +131,7 @@ to_record(const constants::InvestmentConstants& value) noexcept {
            && material_requirement_sets_ready() && inventory_bucket_descriptors_ready()
            && socket_entry_lists_ready() && ability_buckets_ready()
            && progression_definitions_ready() && season_pass_ready() && repeatable_bounties_ready()
-           && rewards::ready() && record_definitions_ready() && node_definitions_ready()
+           && reward_definitions_ready() && record_definitions_ready() && node_definitions_ready()
            && sobject_definitions_ready() && scenario_layouts_ready() && spawn_sets_ready()
            && hash_names_ready() && vendor_catalog_ready()
            && gameplay::entity_position_profiles::available()
@@ -273,14 +273,14 @@ cache::records::MutableDomains scratch_domains(Context& state) noexcept {
         progressionSteps,
         seasonPassRewards,
         bountyRows,
-        ensure_scratch<rewards::Pool, rewards::kPoolCapacity>(state.rewardPoolsScratch),
-        ensure_scratch<rewards::Entry, rewards::kEntryCapacity>(state.rewardEntriesScratch),
-        ensure_scratch<rewards::Item, rewards::kItemCapacity>(state.rewardItemsScratch),
+        ensure_scratch<rewards::Pool, rewards::kPoolCapacity>(state.rewardPoolScratch),
+        ensure_scratch<rewards::Entry, rewards::kEntryCapacity>(state.rewardEntryScratch),
+        ensure_scratch<rewards::Item, rewards::kItemCapacity>(state.rewardItemScratch),
         ensure_scratch<rewards::Instruction, rewards::kInstructionCapacity>(
-            state.rewardInstructionsScratch),
-        ensure_scratch<rewards::Modifier, rewards::kModifierCapacity>(state.rewardModifiersScratch),
+            state.rewardInstructionScratch),
+        ensure_scratch<rewards::Modifier, rewards::kModifierCapacity>(state.rewardModifierScratch),
         ensure_scratch<rewards::SocketOverride, rewards::kSocketOverrideCapacity>(
-            state.rewardSocketsScratch),
+            state.rewardSocketScratch),
     };
 }
 
@@ -311,13 +311,12 @@ void release_scratch_locked(Context& state) noexcept {
     release_bank(state.progressionStepScratch);
     release_bank(state.seasonPassRewardScratch);
     release_bank(state.bountyScratch);
-    release_bank(state.rewardPoolsScratch);
-    release_bank(state.rewardEntriesScratch);
-    release_bank(state.rewardItemsScratch);
-    release_bank(state.rewardInstructionsScratch);
-    release_bank(state.rewardModifiersScratch);
-    release_bank(state.rewardSocketsScratch);
-
+    release_bank(state.rewardPoolScratch);
+    release_bank(state.rewardEntryScratch);
+    release_bank(state.rewardItemScratch);
+    release_bank(state.rewardInstructionScratch);
+    release_bank(state.rewardModifierScratch);
+    release_bank(state.rewardSocketScratch);
     release_bank(state.recordScratch);
     release_bank(state.recordObjectiveScratch);
     release_bank(state.recordIntervalScratch);
@@ -420,14 +419,14 @@ cache::records::Domains occupied_domains(Context& state,
         std::span<const season_pass::Reward>{state.seasonPassRewardScratch.data(),
                                              counts.seasonPassRewards},
         std::span<const bounties::Definition>{state.bountyScratch.data(), counts.bounties},
-        std::span<const rewards::Pool>{state.rewardPoolsScratch.data(), counts.rewardPools},
-        std::span<const rewards::Entry>{state.rewardEntriesScratch.data(), counts.rewardEntries},
-        std::span<const rewards::Item>{state.rewardItemsScratch.data(), counts.rewardItems},
-        std::span<const rewards::Instruction>{state.rewardInstructionsScratch.data(),
+        std::span<const rewards::Pool>{state.rewardPoolScratch.data(), counts.rewardPools},
+        std::span<const rewards::Entry>{state.rewardEntryScratch.data(), counts.rewardEntries},
+        std::span<const rewards::Item>{state.rewardItemScratch.data(), counts.rewardItems},
+        std::span<const rewards::Instruction>{state.rewardInstructionScratch.data(),
                                               counts.rewardInstructions},
-        std::span<const rewards::Modifier>{state.rewardModifiersScratch.data(),
+        std::span<const rewards::Modifier>{state.rewardModifierScratch.data(),
                                            counts.rewardModifiers},
-        std::span<const rewards::SocketOverride>{state.rewardSocketsScratch.data(),
+        std::span<const rewards::SocketOverride>{state.rewardSocketScratch.data(),
                                                  counts.rewardSockets},
     };
 }

+ 6 - 7
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.h

@@ -80,13 +80,12 @@ struct Context {
     std::vector<progressions::Step> progressionStepScratch{};
     std::vector<season_pass::Reward> seasonPassRewardScratch{};
     std::vector<bounties::Definition> bountyScratch{};
-    std::vector<rewards::Pool> rewardPoolsScratch{};
-    std::vector<rewards::Entry> rewardEntriesScratch{};
-    std::vector<rewards::Item> rewardItemsScratch{};
-    std::vector<rewards::Instruction> rewardInstructionsScratch{};
-    std::vector<rewards::Modifier> rewardModifiersScratch{};
-    std::vector<rewards::SocketOverride> rewardSocketsScratch{};
-
+    std::vector<rewards::Pool> rewardPoolScratch{};
+    std::vector<rewards::Entry> rewardEntryScratch{};
+    std::vector<rewards::Item> rewardItemScratch{};
+    std::vector<rewards::Instruction> rewardInstructionScratch{};
+    std::vector<rewards::Modifier> rewardModifierScratch{};
+    std::vector<rewards::SocketOverride> rewardSocketScratch{};
     std::vector<records::Definition> recordScratch{};
     std::vector<records::Objective> recordObjectiveScratch{};
     std::vector<records::Interval> recordIntervalScratch{};

+ 4 - 2
Sunrise/src/state/build_data/season_pass/definition.h

@@ -14,8 +14,8 @@ inline constexpr std::size_t kRewardCapacity = 256;
 /** A reward whose claim flag no mapping table addresses carries this instead of an index. */
 inline constexpr std::uint16_t kUnavailableFlagIndex = 0xFFFFU;
 
-/** Bound on the expanded eligibility expression of one pass row. */
-inline constexpr std::size_t kConditionCapacity = 64;
+/** Instructions in one row's expanded eligibility condition. The longest shipped row needs 5. */
+inline constexpr std::size_t kConditionCapacity = 16;
 
 /** One reward row of the pass, in the native order the opcode-2400 claim names by index. */
 struct Reward {
@@ -29,8 +29,10 @@ struct Reward {
     std::uint16_t claimFlagIndex{kUnavailableFlagIndex};
     /** Rank the account needs before the row may be claimed. */
     std::uint8_t requiredRank{};
+    /** Fixed plugs this row places on the granted item, in native order. */
     std::array<rewards::SocketOverride, rewards::kSocketsPerItem> sockets{};
     std::uint8_t socketCount{};
+    /** Bound eligibility condition; every native expression on the row must hold. */
     std::array<rewards::Instruction, kConditionCapacity> condition{};
     std::uint8_t conditionCount{};
 };

+ 5 - 0
Sunrise/src/state/build_data/season_pass/season_pass_build_data_runtime.cpp

@@ -21,4 +21,9 @@ bool find_season_pass_reward(std::uint16_t rewardIndex, season_pass::Reward& rew
     return season_pass::find(rewardIndex, reward);
 }
 
+/** @return Season pass reward rows in State. */
+std::size_t season_pass_reward_count() noexcept {
+    return season_pass::count();
+}
+
 } // namespace sunrise::state::build_data

+ 34 - 17
Sunrise/src/state/build_data/season_pass/season_pass_catalog.cpp

@@ -1,11 +1,12 @@
 #include "season_pass_catalog.h"
 
 #include <algorithm>
+#include <limits>
 #include <mutex>
 #include <shared_mutex>
 
 #include "../../unlocks/definition.h"
-#include "../rewards/reward_catalog.h"
+#include "../../unlocks/unlocks_expression.h"
 #include "../table.h"
 #include "core/threading/srw_lock.h"
 
@@ -15,30 +16,43 @@ core::threading::SrwLock g_lock;
 Table<Reward, kRewardCapacity> g_rewards;
 } // namespace
 
+/** Clears the reward rows under the catalog lock. */
 void clear() noexcept {
     const std::lock_guard guard(g_lock);
     g_rewards.clear();
 }
 
+/** Checks one complete pass catalog; unavailable rows keep their native positions. */
 bool valid(std::span<const Reward> rewards) noexcept {
-    return !rewards.empty() && rewards.size() <= kRewardCapacity
-           && std::all_of(rewards.begin(), rewards.end(), [](const Reward& reward) {
-                  if (reward.itemHash == 0) {
-                      return reward.quantity == 0 && reward.conditionCount == 0
-                             && reward.socketCount == 0
-                             && reward.claimFlagIndex == kUnavailableFlagIndex;
-                  }
-                  return reward.quantity != 0 && reward.quantity <= INT32_MAX
-                         && (reward.claimFlagIndex == kUnavailableFlagIndex
-                             || reward.claimFlagIndex < unlocks::kAccountFlagCapacity)
-                         && reward.socketCount <= reward.sockets.size()
-                         && reward.conditionCount <= reward.condition.size()
-                         && std::all_of(reward.condition.begin(),
-                                        reward.condition.begin() + reward.conditionCount,
-                                        rewards::valid_instruction);
-              });
+    if (rewards.empty() || rewards.size() > kRewardCapacity) {
+        return false;
+    }
+    constexpr auto kMaximumQuantity =
+        static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)());
+    for (const Reward& reward : rewards) {
+        // An unavailable row keeps its native claim index and grants nothing.
+        if (reward.itemHash == 0) {
+            if (reward.quantity != 0 || reward.conditionCount != 0 || reward.socketCount != 0
+                || reward.claimFlagIndex != kUnavailableFlagIndex) {
+                return false;
+            }
+            continue;
+        }
+        if (reward.quantity == 0 || reward.quantity > kMaximumQuantity
+            || (reward.claimFlagIndex != kUnavailableFlagIndex
+                && reward.claimFlagIndex >= unlocks::kAccountFlagCapacity)
+            || reward.socketCount > reward.sockets.size()
+            || reward.conditionCount > reward.condition.size()
+            || !std::all_of(reward.condition.begin(),
+                            reward.condition.begin() + reward.conditionCount,
+                            unlocks::valid)) {
+            return false;
+        }
+    }
+    return true;
 }
 
+/** Replaces the complete pass catalog in one step. */
 bool replace(std::span<const Reward> rewards) noexcept {
     if (!valid(rewards)) {
         return false;
@@ -47,6 +61,7 @@ bool replace(std::span<const Reward> rewards) noexcept {
     return g_rewards.replace(rewards);
 }
 
+/** Reads one available reward row by the index an opcode-2400 claim names. */
 bool find(std::uint16_t rewardIndex, Reward& reward) noexcept {
     reward = {};
     const std::shared_lock guard(g_lock);
@@ -57,11 +72,13 @@ bool find(std::uint16_t rewardIndex, Reward& reward) noexcept {
     return true;
 }
 
+/** Copies every reward row in native reward order. */
 bool snapshot(std::span<Reward> output, std::size_t& count) noexcept {
     const std::shared_lock guard(g_lock);
     return g_rewards.snapshot(output, count);
 }
 
+/** @return The reward row count, including unavailable rows, read under the lock. */
 std::size_t count() noexcept {
     const std::shared_lock guard(g_lock);
     return g_rewards.count();

+ 0 - 119
Sunrise/src/state/build_data/vendors/vendor_expression.cpp

@@ -1,119 +0,0 @@
-#include "vendor_expression.h"
-
-#include <array>
-
-namespace sunrise::state::build_data::vendors {
-namespace {
-
-/** What one stack slot holds. The two kinds never substitute for each other. */
-enum class Kind : std::uint8_t {
-    boolean,
-    number,
-};
-
-/** One typed stack slot. */
-struct Slot {
-    Kind kind{};
-    bool boolean{};
-    std::int32_t number{};
-};
-
-/** Fixed evaluation stack, which refuses rather than growing. */
-class Stack final {
-public:
-    /** @param slot Value to push. @return True when there was room. */
-    [[nodiscard]] bool push(const Slot& slot) noexcept {
-        if (depth_ == slots_.size()) {
-            return false;
-        }
-        slots_[depth_++] = slot;
-        return true;
-    }
-
-    /** @param kind Required kind. @param slot Receives the value. @return True when it matched. */
-    [[nodiscard]] bool pop(Kind kind, Slot& slot) noexcept {
-        if (depth_ == 0 || slots_[depth_ - 1].kind != kind) {
-            return false;
-        }
-        slot = slots_[--depth_];
-        return true;
-    }
-
-    /** @return Slots currently held. */
-    [[nodiscard]] std::size_t depth() const noexcept {
-        return depth_;
-    }
-
-private:
-    std::array<Slot, kExpressionStackCapacity> slots_{};
-    std::size_t depth_{};
-};
-
-/** @param value Boolean result. @return The stack slot holding it. */
-[[nodiscard]] Slot boolean_slot(bool value) noexcept {
-    return {Kind::boolean, value, 0};
-}
-
-/** @param value Numeric result. @return The stack slot holding it. */
-[[nodiscard]] Slot number_slot(std::int32_t value) noexcept {
-    return {Kind::number, false, value};
-}
-
-/**
- * Runs one instruction against the stack.
- * @param instruction Instruction to run.
- * @param inputs Flag and value readers.
- * @param stack Evaluation stack.
- * @return True when the opcode is known and its operands were there in the right kind.
- */
-[[nodiscard]] bool
-step(const Instruction& instruction, const Inputs& inputs, Stack& stack) noexcept {
-    Slot left{};
-    Slot right{};
-    switch (instruction.opcode) {
-    case Opcode::flag: {
-        std::uint8_t logical = 0;
-        return inputs.flag(inputs.context, instruction.operand, logical)
-               && stack.push(boolean_slot(logical == kFlagActive));
-    }
-    case Opcode::logicalNot:
-        return stack.pop(Kind::boolean, left) && stack.push(boolean_slot(!left.boolean));
-    case Opcode::loadValue: {
-        std::int32_t value = 0;
-        return inputs.value(inputs.context, instruction.operand, value)
-               && stack.push(number_slot(value));
-    }
-    case Opcode::constant:
-        return stack.push(number_slot(instruction.operand));
-    case Opcode::lessThan:
-        // The right operand was pushed last, so it comes off first.
-        return stack.pop(Kind::number, right) && stack.pop(Kind::number, left)
-               && stack.push(boolean_slot(left.number < right.number));
-    default:
-        return false;
-    }
-}
-
-} // namespace
-
-/** Evaluates one expression program. */
-bool evaluate(std::span<const Instruction> program, const Inputs& inputs, bool& result) noexcept {
-    result = false;
-    if (program.empty() || inputs.flag == nullptr || inputs.value == nullptr) {
-        return false;
-    }
-    Stack stack;
-    for (const Instruction& instruction : program) {
-        if (!step(instruction, inputs, stack)) {
-            return false;
-        }
-    }
-    Slot final{};
-    if (stack.depth() != 1 || !stack.pop(Kind::boolean, final)) {
-        return false;
-    }
-    result = final.boolean;
-    return true;
-}
-
-} // namespace sunrise::state::build_data::vendors

+ 0 - 58
Sunrise/src/state/build_data/vendors/vendor_expression.h

@@ -1,58 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <span>
-
-namespace sunrise::state::build_data::vendors {
-
-/** Stack slots one expression may use. Known gates need two, so this leaves headroom. */
-inline constexpr std::size_t kExpressionStackCapacity = 16;
-
-/** Opcodes the client's expression evaluator implements. */
-enum class Opcode : std::uint8_t {
-    /** Pushes true when the flag's logical byte is `kFlagActive`. Operand is the flag slot. */
-    flag = 1,
-    /** Pops one boolean and pushes its negation. */
-    logicalNot = 2,
-    /** Pushes one progression value. Operand is the value slot. */
-    loadValue = 10,
-    /** Pushes the operand as a constant. */
-    constant = 11,
-    /** Pops right then left and pushes left less than right. */
-    lessThan = 15,
-};
-
-/** A flag is active only for this logical byte. Logical 0 and 1 are both inactive. */
-inline constexpr std::uint8_t kFlagActive = 2;
-
-/** One expression instruction. Opcodes with no operand leave it unread. */
-struct Instruction {
-    Opcode opcode{};
-    std::uint16_t operand{};
-};
-
-/** Reads one flag's logical byte. Returns false when the slot has no value. */
-using FlagReader = bool (*)(void* context, std::uint16_t slot, std::uint8_t& logical) noexcept;
-/** Reads one progression value. Returns false when the slot has no value. */
-using ValueReader = bool (*)(void* context, std::uint16_t slot, std::int32_t& value) noexcept;
-
-/** The two readers one evaluation needs, and the caller state they share. */
-struct Inputs {
-    FlagReader flag{};
-    ValueReader value{};
-    void* context{};
-};
-
-/**
- * Evaluates one expression program.
- * Every failure is refused, never defaulted: bad opcode, bad stack, bad kind, unreadable slot.
- * @param program Instructions in evaluation order.
- * @param inputs Flag and value readers.
- * @param result Receives the expression result, and is cleared on any refusal.
- * @return True only when the whole program evaluated to one boolean.
- */
-[[nodiscard]] bool
-evaluate(std::span<const Instruction> program, const Inputs& inputs, bool& result) noexcept;
-
-} // namespace sunrise::state::build_data::vendors

+ 2 - 1
Sunrise/src/state/progression/season_pass_reward_catalog.h

@@ -11,7 +11,8 @@ inline constexpr std::uint16_t kProgressionDefinitionIndex = 40;
 /** Repeating HUD bar paired with the Arrivals progression. */
 inline constexpr std::uint16_t kHudProgressionDefinitionIndex = 41;
 
-/** Acquisition flag of a direct perk reward, or rewards::kAbsent for an item or package. */
+/** Acquisition flag of a direct perk reward, or build_data::rewards::kAbsent for an item or
+ * package. */
 [[nodiscard]] std::uint16_t progress_flag(const build_data::season_pass::Reward& reward) noexcept;
 
 } // namespace sunrise::state::progression::season_pass

+ 73 - 131
Sunrise/src/state/rewards/reward_resolver.cpp

@@ -2,9 +2,10 @@
 
 #include <algorithm>
 #include <cmath>
+#include <limits>
 
-#include "../build_data/rewards/reward_catalog.h"
-#include "middleware/content/packages/tables/unlock_opcode.h"
+#include "../build_data/runtime.h"
+#include "../unlocks/unlocks_expression.h"
 
 namespace sunrise::state::rewards {
 namespace {
@@ -12,10 +13,11 @@ namespace {
 namespace definitions = build_data::rewards;
 /** The empty category tag matches every wrapper selection. */
 constexpr std::uint32_t kEmptyTag = 0x811C9DC5U;
-/** Native postfix expressions use at most 256 signed 32-bit stack values. */
-constexpr std::size_t kExpressionCapacity = 256;
 /** Native wrappers retain up to 64 selections, including rows without an item grant. */
 constexpr std::size_t kDrawCapacity = 64;
+/** A grant quantity is published as a signed 32-bit stack size. */
+constexpr auto kMaximumQuantity =
+    static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)());
 
 bool refuse(const Context& context, const char* reason) noexcept {
     if (context.refusal != nullptr) {
@@ -24,133 +26,77 @@ bool refuse(const Context& context, const char* reason) noexcept {
     return false;
 }
 
-bool apply_condition_operator(const definitions::Instruction& instruction,
-                              std::span<std::int32_t> stack,
-                              std::size_t& size,
-                              const Context& context) noexcept {
-    if (!definitions::valid_instruction(instruction)) {
-        return refuse(context, "condition_opcode");
-    }
-    using Op = middleware::content::packages::tables::UnlockOpcode;
-    const auto opcode = static_cast<Op>(instruction.opcode);
-    const bool unary = opcode == Op::logicalNot || opcode == Op::negate;
-    if (size < (unary ? 1U : 2U)) {
-        return refuse(context, "condition_shape");
+/** Reads one validated flag from the caller's unlock banks or selected character. */
+bool read_flag(const void* raw, const unlocks::Instruction& instruction, bool& set) noexcept {
+    const auto& context = *static_cast<const Context*>(raw);
+    const auto index = instruction.operand;
+    switch (instruction.bank) {
+    case unlocks::Bank::account:
+        set = context.unlocks.accountFlags[index] == unlocks::kFlagSet;
+        return true;
+    case unlocks::Bank::profile:
+        set = context.unlocks.profileFlags[index] == unlocks::kFlagSet;
+        return true;
+    case unlocks::Bank::character:
+        set = context.unlocks.characterObjectFlags[index] == unlocks::kFlagSet;
+        return true;
+    case unlocks::Bank::characterClass:
+        set = index == static_cast<std::uint32_t>(context.characterClass);
+        return true;
+    default:
+        return false;
     }
-    const auto right = unary ? 0 : stack[--size];
-    auto& left = stack[size - 1];
-    switch (opcode) {
-    case Op::logicalNot:
-        left = left == 0;
-        break;
-    case Op::negate:
-        left = static_cast<std::int32_t>(0U - static_cast<std::uint32_t>(left));
-        break;
-    case Op::logicalOr:
-        left = left != 0 || right != 0;
-        break;
-    case Op::logicalAnd:
-        left = left != 0 && right != 0;
-        break;
-    case Op::equal:
-        left = left == right;
-        break;
-    case Op::greaterThan:
-        left = left > right;
-        break;
-    case Op::greaterOrEqual:
-        left = left >= right;
-        break;
-    case Op::lessOrEqual:
-        left = left <= right;
-        break;
-    case Op::add:
-        left = static_cast<std::int32_t>(static_cast<std::uint32_t>(left)
-                                         + static_cast<std::uint32_t>(right));
-        break;
-    case Op::lessThan:
-        left = left < right;
-        break;
+}
+
+/** Reads one validated value from the caller's unlock banks. */
+bool read_value(const void* raw,
+                const unlocks::Instruction& instruction,
+                std::int32_t& value) noexcept {
+    const auto& context = *static_cast<const Context*>(raw);
+    switch (instruction.bank) {
+    case unlocks::Bank::account:
+        value = context.unlocks.objectiveValues[instruction.operand];
+        return true;
+    case unlocks::Bank::character:
+        value = context.unlocks.characterObjectValues[instruction.operand];
+        return true;
     default:
-        return refuse(context, "condition_opcode");
+        return false;
     }
-    return true;
 }
 
-bool condition(definitions::View data,
-               definitions::Range expression,
+/** An empty program is unconditional; external identities are not in the saved banks. */
+bool condition(std::span<const definitions::Instruction> program,
                const Context& context,
                bool& result) noexcept {
-    result = expression.count == 0;
-    if (!definitions::fits(expression, data.instructions)) {
-        return refuse(context, "condition_shape");
+    result = program.empty();
+    if (program.empty()) {
+        return true;
     }
-    std::array<std::int32_t, kExpressionCapacity> stack{};
-    std::size_t size = 0;
-    for (const auto& instruction : data.instructions.subspan(expression.first, expression.count)) {
-        const auto operand = instruction.operand;
-        std::int32_t value = 0;
-        using Read = definitions::BankRead;
-        using Op = middleware::content::packages::tables::UnlockOpcode;
-        switch (instruction.opcode) {
-        case static_cast<std::uint32_t>(Read::accountFlag):
-            if (operand >= context.unlocks.accountFlags.size()) {
-                return refuse(context, "condition_shape");
-            }
-            value = context.unlocks.accountFlags[operand] == unlocks::kFlagSet;
-            break;
-        case static_cast<std::uint32_t>(Read::profileFlag):
-            if (operand >= context.unlocks.profileFlags.size()) {
-                return refuse(context, "condition_shape");
-            }
-            value = context.unlocks.profileFlags[operand] == unlocks::kFlagSet;
-            break;
-        case static_cast<std::uint32_t>(Read::characterFlag):
-            if (operand >= context.unlocks.characterObjectFlags.size()) {
-                return refuse(context, "condition_shape");
-            }
-            value = context.unlocks.characterObjectFlags[operand] == unlocks::kFlagSet;
-            break;
-        case static_cast<std::uint32_t>(Read::accountValue):
-            if (operand >= context.unlocks.objectiveValues.size()) {
-                return refuse(context, "condition_shape");
-            }
-            value = context.unlocks.objectiveValues[operand];
-            break;
-        case static_cast<std::uint32_t>(Read::characterValue):
-            if (operand >= context.unlocks.characterObjectValues.size()) {
-                return refuse(context, "condition_shape");
-            }
-            value = context.unlocks.characterObjectValues[operand];
-            break;
-        case static_cast<std::uint32_t>(Read::characterClass):
-            value = operand == static_cast<std::uint32_t>(context.characterClass);
-            break;
-        case static_cast<std::uint32_t>(Op::constant):
-            value = static_cast<std::int32_t>(operand);
-            break;
-        case static_cast<std::uint32_t>(Read::externalFlag):
-            return refuse(context, "external_flag");
-        case static_cast<std::uint32_t>(Read::externalValue):
-            return refuse(context, "external_value");
-        default:
-            if (!apply_condition_operator(instruction, stack, size, context)) {
-                return false;
-            }
-            continue;
-        }
-        if (size == stack.size()) {
+    for (const auto& instruction : program) {
+        if (!unlocks::valid(instruction)) {
             return refuse(context, "condition_shape");
         }
-        stack[size++] = value;
-    }
-    if (expression.count != 0) {
-        if (size != 1) {
-            return refuse(context, "condition_shape");
+        if (instruction.bank == unlocks::Bank::external) {
+            return refuse(context,
+                          instruction.opcode == unlocks::Opcode::flag ? "external_flag"
+                                                                      : "external_value");
         }
-        result = stack[0] != 0;
     }
-    return true;
+    const unlocks::Inputs inputs{read_flag, read_value, &context};
+    return unlocks::evaluate(program, inputs, result) || refuse(context, "condition_shape");
+}
+
+bool condition(definitions::View data,
+               definitions::Range expression,
+               const Context& context,
+               bool& result) noexcept {
+    if (!definitions::fits(expression, data.instructions)) {
+        result = false;
+        return refuse(context, "condition_shape");
+    }
+    return condition(
+        data.instructions.subspan(expression.first, expression.count), context, result);
 }
 
 struct Resolver {
@@ -300,7 +246,7 @@ struct Resolver {
         if (chosen->itemIndex == definitions::kAbsent) {
             return true;
         }
-        if (chosen->quantity == 0 || chosen->quantity > INT32_MAX
+        if (chosen->quantity == 0 || chosen->quantity > kMaximumQuantity
             || result.count == result.grants.size()
             || !definitions::fits(chosen->sockets, data.sockets)) {
             return false;
@@ -327,7 +273,7 @@ bool resolve_item(definitions::View data,
     if (context.refusal != nullptr) {
         *context.refusal = "reward_shape";
     }
-    if (itemIndex >= data.items.size() || quantity == 0 || quantity > INT32_MAX) {
+    if (itemIndex >= data.items.size() || quantity == 0 || quantity > kMaximumQuantity) {
         return false;
     }
     const auto& item = data.items[itemIndex];
@@ -382,12 +328,7 @@ bool resolve_item(definitions::View data,
 bool eligible(std::span<const definitions::Instruction> instructions,
               const Context& context,
               bool& result) noexcept {
-    definitions::View view{};
-    view.instructions = instructions;
-    if (instructions.size() > UINT32_MAX) {
-        return false;
-    }
-    return condition(view, {0, static_cast<std::uint32_t>(instructions.size())}, context, result);
+    return condition(instructions, context, result);
 }
 
 bool resolve(const Context& context,
@@ -404,10 +345,11 @@ bool resolve(const Context& context,
     if (context.refusal != nullptr) {
         *context.refusal = "reward_item";
     }
-    return definitions::read(&request, [](void* raw, definitions::View data) noexcept {
-        auto& value = *static_cast<Request*>(raw);
-        return resolve_item(data, value.context, value.item, value.quantity, value.result);
-    });
+    return build_data::read_reward_definitions(
+        &request, [](void* raw, definitions::View data) noexcept {
+            auto& value = *static_cast<Request*>(raw);
+            return resolve_item(data, value.context, value.item, value.quantity, value.result);
+        });
 }
 
 } // namespace sunrise::state::rewards

+ 3 - 0
Sunrise/src/state/rewards/reward_resolver.h

@@ -6,6 +6,7 @@
 
 namespace sunrise::state::rewards {
 
+/** One item a resolved reward grants, with the socket overrides its row carries. */
 struct Grant {
     std::uint16_t itemIndex{build_data::rewards::kAbsent};
     std::int32_t quantity{};
@@ -13,6 +14,7 @@ struct Grant {
     std::size_t socketCount{};
 };
 
+/** Every grant one resolution produces, in draw order. */
 struct Result {
     std::array<Grant, build_data::rewards::kGrantCapacity> grants{};
     std::size_t count{};
@@ -27,6 +29,7 @@ struct Context {
     const char** refusal{};
 };
 
+/** Evaluates one bound condition; an empty condition is always eligible. */
 [[nodiscard]] bool eligible(std::span<const build_data::rewards::Instruction> instructions,
                             const Context& context,
                             bool& result) noexcept;

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

@@ -25,8 +25,9 @@ struct SettingsDelta;
 
 namespace sunrise::state {
 
-/** Acquisition planner selected from installed wrapper, quest and bucket definitions. */
+/** Which prepare path grants one installed item. */
 enum class ItemGrantRoute : std::uint8_t { unavailable, quest, profile, reward };
+/** Chooses the prepare path from the item's wrapper, quest setup and bucket. */
 [[nodiscard]] ItemGrantRoute item_grant_route(std::uint16_t itemIndex) noexcept;
 /**
  * Assigns runtime SOIDs only to installed profile mod/shader rows which are socket action sources.
@@ -264,7 +265,7 @@ struct PendingRecordRewardGrant {
     bool prepared{};
 };
 
-/** One uncommitted Season reward and the exact native row or bundle it will claim. */
+/** One uncommitted Season reward, the native row it will claim, and the seed of its draw. */
 struct PendingSeasonPassReward {
     PendingRecordRewardGrant grant{};
     std::uint32_t sourceDefinitionHash{};
@@ -535,15 +536,26 @@ prepare_item_acquisition(std::uint16_t collectibleIndex,
 
 /** Atomically commits one prepared reward grant and its durable Season claim. */
 [[nodiscard]] bool commit_season_pass_reward(PendingSeasonPassReward& mutation) noexcept;
-/** Resolves an installed item or wrapper; refusal receives a static reason on failure. */
+/**
+ * Resolves an installed item or wrapper for the selected character.
+ * @param seed Caller-drawn entropy for any wrapper draws.
+ * @param refusal Receives a static reason on failure.
+ */
 [[nodiscard]] bool prepare_item_reward(std::uint16_t itemIndex,
                                        std::uint32_t quantity,
+                                       std::uint64_t seed,
                                        PendingRecordRewardGrant& mutation,
                                        const char** refusal = nullptr) noexcept;
-/** Prepares an eligible native pass row without writing its claim or acquisition flags. */
+/**
+ * Prepares an eligible native pass row without writing its claim or acquisition flags.
+ * @param seed Caller-drawn entropy; the commit replays the same draw from it.
+ * @param refusal Receives a static reason on failure.
+ */
 [[nodiscard]] bool prepare_season_pass_reward(std::uint16_t rewardIndex,
+                                              std::uint64_t seed,
                                               PendingSeasonPassReward& mutation,
                                               const char** refusal = nullptr) noexcept;
+/** Applies a prepared grant's acquisition flags to the saved banks; false if one moved. */
 [[nodiscard]] bool preview_reward_unlocks(const PendingRecordRewardGrant& mutation,
                                           unlocks::Table& after) noexcept;
 

+ 11 - 27
Sunrise/src/state/runtime/state_account_reward_grant_runtime.cpp

@@ -5,17 +5,13 @@
 #include <array>
 #include <cstddef>
 #include <cstdint>
-#include <cstring>
 #include <limits>
 
-#include "../../middleware/crypto/random_bytes.h"
 #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
-#include "../build_data/rewards/reward_catalog.h"
 #include "../build_data/runtime.h"
 #include "../investment/store_internal.h"
 #include "../rewards/reward_resolver.h"
 #include "../unlocks/unlocks_records.h"
-#include "../unlocks/unlocks_runtime.h"
 #include "runtime.h"
 #include "state_account_transaction_helpers.h"
 #include "storage/internal.h"
@@ -28,8 +24,6 @@ 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,
@@ -132,11 +126,11 @@ apply_reward_sockets(const item_details::Definition& detail,
 /** 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)) {
+    if (!build_data::find_reward_item(item.definitionIndex, reward)) {
         return false;
     }
     return reward.poolIndex != build_data::rewards::kAbsent
-           || (item.bucketId == inventory_buckets::kNonInventoryBucketId
+           || (item.bucketId == inventory_buckets::kPerkBucketId
                && reward.acquiredFlag != build_data::rewards::kAbsent);
 }
 
@@ -153,7 +147,7 @@ enum class PassResolution { claim, replay };
     build_data::rewards::Item source{};
     if (character >= account.characterCount
         || !investment::store::read_unlocks(flags, static_cast<int>(character))
-        || !build_data::rewards::find_item(reward.itemIndex, source)
+        || !build_data::find_reward_item(reward.itemIndex, source)
         || source.definitionHash != reward.itemHash || reward.socketCount > reward.sockets.size()
         || reward.conditionCount > reward.condition.size()) {
         return false;
@@ -206,7 +200,7 @@ enum class PassResolution { claim, replay };
         build_data::items::Definition definition{};
         build_data::rewards::Item source{};
         if (!build_data::find_item_definition_index(planned.itemIndex, definition)
-            || !build_data::rewards::find_item(planned.itemIndex, source)
+            || !build_data::find_reward_item(planned.itemIndex, source)
             || prepared.definitionHash != definition.definitionHash
             || prepared.quantity != planned.quantity
             || prepared.acquiredFlag != source.acquiredFlag) {
@@ -236,6 +230,7 @@ enum class PassResolution { claim, replay };
 } // namespace
 
 bool prepare_season_pass_reward(std::uint16_t rewardIndex,
+                                std::uint64_t seed,
                                 PendingSeasonPassReward& mutation,
                                 const char** refusal) noexcept {
     const char* unused = nullptr;
@@ -255,12 +250,7 @@ bool prepare_season_pass_reward(std::uint16_t rewardIndex,
     if (season_pass_reward_claimed(rewardIndex)) {
         return false;
     }
-    reason = "random_source";
-    std::array<std::byte, sizeof mutation.seed> random{};
-    if (!middleware::crypto::random::fill(random)) {
-        return false;
-    }
-    std::memcpy(&mutation.seed, random.data(), random.size());
+    mutation.seed = seed;
     rewards::Result resolved{};
     reason = "reward_condition";
     if (!resolve_pass(
@@ -306,6 +296,7 @@ ItemGrantRoute item_grant_route(std::uint16_t itemIndex) noexcept {
 
 bool prepare_item_reward(std::uint16_t itemIndex,
                          std::uint32_t quantity,
+                         std::uint64_t seed,
                          PendingRecordRewardGrant& mutation,
                          const char** refusal) noexcept {
     const char* unused = nullptr;
@@ -314,16 +305,9 @@ bool prepare_item_reward(std::uint16_t itemIndex,
     mutation = {};
     const std::lock_guard lock(investment::store::g_mutex);
     build_data::items::Definition item{};
-    std::array<std::byte, sizeof(std::uint64_t)> random{};
     if (!build_data::find_item_definition_index(itemIndex, item)) {
         return false;
     }
-    reason = "random_source";
-    if (!middleware::crypto::random::fill(random)) {
-        return false;
-    }
-    std::uint64_t seed = 0;
-    std::memcpy(&seed, random.data(), random.size());
     reason = "selected_character";
     const AccountState account = account_snapshot();
     const auto character = selected_character_index(account);
@@ -440,8 +424,8 @@ namespace {
         if (reward.kind == RecordRewardKind::accountUnlock) {
             build_data::rewards::Item source{};
             if (reward.quantity != 1 || reward.afterQuantity != 1 || reward.instanceSoid != 0
-                || item.bucketId != build_data::inventory::buckets::kNonInventoryBucketId
-                || !build_data::rewards::find_item(item.definitionIndex, source)
+                || item.bucketId != build_data::inventory::buckets::kPerkBucketId
+                || !build_data::find_reward_item(item.definitionIndex, source)
                 || source.acquiredFlag == build_data::rewards::kAbsent
                 || source.acquiredFlag != reward.acquiredFlag) {
                 return false;
@@ -556,7 +540,7 @@ bool prepare_record_reward_grant(std::span<const DirectRecordReward> rewards,
         if (requested.acquireUnlock) {
             reason = "acquisition_flag";
             build_data::rewards::Item source{};
-            if (!build_data::rewards::find_item(item.definitionIndex, source)
+            if (!build_data::find_reward_item(item.definitionIndex, source)
                 || source.definitionHash != item.definitionHash) {
                 return false;
             }
@@ -571,7 +555,7 @@ bool prepare_record_reward_grant(std::span<const DirectRecordReward> rewards,
                 prepared.previousFlag = static_cast<std::uint8_t>(before);
             }
         }
-        if (item.bucketId == build_data::inventory::buckets::kNonInventoryBucketId) {
+        if (item.bucketId == build_data::inventory::buckets::kPerkBucketId) {
             reason = "perk_acquisition";
             if (prepared.acquiredFlag == build_data::rewards::kAbsent || requested.quantity != 1
                 || !requested.sockets.empty()) {

+ 11 - 13
Sunrise/src/state/runtime/state_progression_runtime.cpp

@@ -8,16 +8,14 @@
 #include <limits>
 #include <span>
 
-#include "../build_data/rewards/reward_catalog.h"
+#include "../../core/logging/log.h"
 #include "../build_data/runtime.h"
-#include "../build_data/season_pass/season_pass_catalog.h"
 #include "../investment/investment.h"
 #include "../investment/store_internal.h"
 #include "../progression/season_pass_reward_catalog.h"
 #include "../rewards/reward_resolver.h"
 #include "../unlocks/definition.h"
 #include "../unlocks/unlocks_runtime.h"
-#include "core/logging/log.h"
 #include "runtime.h"
 #include "state.h"
 #include "state_account_transaction_helpers.h"
@@ -249,8 +247,9 @@ bool publish_artifact_character_banks(CharacterArtifactWrite& write) noexcept {
     return publish_artifact_character_banks(write);
 }
 
+/** Logs one earned perk whose flags were not granted, and why. */
 void report_perk_refusal(std::size_t index, const char* reason) noexcept {
-    core::log::writef(core::log::Channel::server,
+    core::log::writef(core::log::Channel::state,
                       core::log::Level::warn,
                       "ev=season_xp stage=automatic_perk index=%zu result=skip reason=%s",
                       index,
@@ -259,7 +258,7 @@ void report_perk_refusal(std::size_t index, const char* reason) noexcept {
 
 /** Earned perks set both flags in the transaction that publishes their qualifying rank. */
 bool grant_progress_flags() noexcept {
-    if (!build_data::season_pass_ready() || !build_data::rewards::ready()
+    if (!build_data::season_pass_ready() || !build_data::reward_definitions_ready()
         || !build_data::item_definitions_ready()) {
         return true;
     }
@@ -276,7 +275,7 @@ bool grant_progress_flags() noexcept {
         return false;
     }
     const auto rank = seasonal_rank();
-    const auto count = build_data::season_pass::count();
+    const auto count = build_data::season_pass_reward_count();
     for (std::size_t index = 0; index < count; ++index) {
         build_data::season_pass::Reward reward{};
         if (!build_data::find_season_pass_reward(static_cast<std::uint16_t>(index), reward)) {
@@ -336,21 +335,20 @@ bool publish_experience_lanes(std::int32_t experience) noexcept {
 
 } // namespace
 
-// Packages share the perk bucket; only rewards without a pool grant the flag directly.
+/** Packages share the perk bucket; only rewards without a pool grant the flag directly. */
 std::uint16_t
 progression::season_pass::progress_flag(const build_data::season_pass::Reward& reward) noexcept {
-    namespace rewards = build_data::rewards;
     build_data::items::Definition item{};
-    rewards::Item acquisition{};
+    build_data::rewards::Item acquisition{};
     if (build_data::find_item_definition_index(reward.itemIndex, item)
         && item.definitionHash == reward.itemHash
-        && item.bucketId == build_data::inventory::buckets::kNonInventoryBucketId
-        && rewards::find_item(reward.itemIndex, acquisition)
+        && item.bucketId == build_data::inventory::buckets::kPerkBucketId
+        && build_data::find_reward_item(reward.itemIndex, acquisition)
         && acquisition.definitionHash == reward.itemHash
-        && acquisition.poolIndex == rewards::kAbsent) {
+        && acquisition.poolIndex == build_data::rewards::kAbsent) {
         return acquisition.acquiredFlag;
     }
-    return rewards::kAbsent;
+    return build_data::rewards::kAbsent;
 }
 
 /** @return Seasonal XP published in the account progression bank. */

+ 203 - 0
Sunrise/src/state/unlocks/unlocks_expression.cpp

@@ -0,0 +1,203 @@
+#include "unlocks_expression.h"
+
+#include <array>
+#include <limits>
+
+#include "../account/account_state.h"
+#include "definition.h"
+
+namespace sunrise::state::unlocks {
+namespace {
+
+/** Fixed evaluation stack, which refuses rather than growing. */
+class Stack final {
+public:
+    /** @param value Value to push. @return True when there was room. */
+    [[nodiscard]] bool push(std::int32_t value) noexcept {
+        if (depth_ == values_.size()) {
+            return false;
+        }
+        values_[depth_++] = value;
+        return true;
+    }
+
+    /** @param value Receives the top value. @return True when there was one. */
+    [[nodiscard]] bool pop(std::int32_t& value) noexcept {
+        if (depth_ == 0) {
+            return false;
+        }
+        value = values_[--depth_];
+        return true;
+    }
+
+    /** @return Values currently held. */
+    [[nodiscard]] std::size_t depth() const noexcept {
+        return depth_;
+    }
+
+private:
+    std::array<std::int32_t, kExpressionStackCapacity> values_{};
+    std::size_t depth_{};
+};
+
+/** Signed arithmetic wraps rather than overflowing. */
+[[nodiscard]] std::int32_t wrapping_add(std::int32_t left, std::int32_t right) noexcept {
+    return static_cast<std::int32_t>(static_cast<std::uint32_t>(left)
+                                     + static_cast<std::uint32_t>(right));
+}
+
+/**
+ * Applies one binary operator.
+ * @return False when the opcode is not a binary operator.
+ */
+[[nodiscard]] bool
+binary_result(Opcode opcode, std::int32_t left, std::int32_t right, std::int32_t& value) noexcept {
+    switch (opcode) {
+    case Opcode::logicalOr:
+        value = left != 0 || right != 0;
+        return true;
+    case Opcode::logicalAnd:
+        value = left != 0 && right != 0;
+        return true;
+    case Opcode::equal:
+        value = left == right;
+        return true;
+    case Opcode::greaterThan:
+        value = left > right;
+        return true;
+    case Opcode::greaterOrEqual:
+        value = left >= right;
+        return true;
+    case Opcode::lessThan:
+        value = left < right;
+        return true;
+    case Opcode::lessOrEqual:
+        value = left <= right;
+        return true;
+    case Opcode::add:
+        value = wrapping_add(left, right);
+        return true;
+    default:
+        return false;
+    }
+}
+
+/**
+ * Runs one instruction against the stack.
+ * @param instruction Instruction to run.
+ * @param inputs Flag and value readers.
+ * @param stack Evaluation stack.
+ * @return True when the opcode is known and its operands were there.
+ */
+[[nodiscard]] bool
+step(const Instruction& instruction, const Inputs& inputs, Stack& stack) noexcept {
+    std::int32_t left = 0;
+    std::int32_t right = 0;
+    std::int32_t value = 0;
+    switch (instruction.opcode) {
+    case Opcode::flag: {
+        bool set = false;
+        return inputs.flag(inputs.context, instruction, set) && stack.push(set ? 1 : 0);
+    }
+    case Opcode::loadValue:
+        return inputs.value(inputs.context, instruction, value) && stack.push(value);
+    case Opcode::constant:
+        return stack.push(static_cast<std::int32_t>(instruction.operand));
+    case Opcode::logicalNot:
+        return stack.pop(left) && stack.push(left == 0 ? 1 : 0);
+    case Opcode::negate:
+        return stack.pop(left) && stack.push(wrapping_add(~left, 1));
+    default:
+        // The right operand was pushed last, so it comes off first.
+        return stack.pop(right) && stack.pop(left)
+               && binary_result(instruction.opcode, left, right, value) && stack.push(value);
+    }
+}
+
+} // namespace
+
+/** Converts one native opcode word to an evaluated opcode. */
+bool decode_opcode(std::uint32_t native, Opcode& opcode) noexcept {
+    if (native > (std::numeric_limits<std::uint8_t>::max)()) {
+        return false;
+    }
+    switch (static_cast<Opcode>(native)) {
+    case Opcode::flag:
+    case Opcode::logicalNot:
+    case Opcode::logicalOr:
+    case Opcode::logicalAnd:
+    case Opcode::equal:
+    case Opcode::loadValue:
+    case Opcode::constant:
+    case Opcode::greaterThan:
+    case Opcode::greaterOrEqual:
+    case Opcode::lessThan:
+    case Opcode::lessOrEqual:
+    case Opcode::add:
+    case Opcode::negate:
+        opcode = static_cast<Opcode>(native);
+        return true;
+    default:
+        return false;
+    }
+}
+
+/** Checks one instruction's bank and its operand against that bank's capacity. */
+bool valid(const Instruction& instruction) noexcept {
+    Opcode decoded{};
+    if (!decode_opcode(static_cast<std::uint32_t>(instruction.opcode), decoded)) {
+        return false;
+    }
+    const std::uint32_t index = instruction.operand;
+    if (instruction.opcode == Opcode::flag) {
+        switch (instruction.bank) {
+        case Bank::account:
+            return index < kAccountFlagCapacity;
+        case Bank::profile:
+            return index < kProfileFlagCapacity;
+        case Bank::character:
+            return index < kCharacterObjectFlagCapacity;
+        case Bank::characterClass:
+            return index < kCharacterClassCount;
+        case Bank::external:
+            return true;
+        default:
+            return false;
+        }
+    }
+    if (instruction.opcode == Opcode::loadValue) {
+        switch (instruction.bank) {
+        case Bank::account:
+            return index < kObjectiveValueCapacity;
+        case Bank::character:
+            return index < kCharacterObjectValueCapacity;
+        case Bank::external:
+            return true;
+        default:
+            return false;
+        }
+    }
+    return instruction.bank == Bank::none;
+}
+
+/** Evaluates one expression program. */
+bool evaluate(std::span<const Instruction> program, const Inputs& inputs, bool& result) noexcept {
+    result = false;
+    if (program.empty() || inputs.flag == nullptr || inputs.value == nullptr) {
+        return false;
+    }
+    Stack stack;
+    for (const Instruction& instruction : program) {
+        if (!step(instruction, inputs, stack)) {
+            return false;
+        }
+    }
+    std::int32_t final = 0;
+    if (stack.depth() != 1 || !stack.pop(final)) {
+        return false;
+    }
+    result = final != 0;
+    return true;
+}
+
+} // namespace sunrise::state::unlocks

+ 92 - 0
Sunrise/src/state/unlocks/unlocks_expression.h

@@ -0,0 +1,92 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+namespace sunrise::state::unlocks {
+
+/** Stack slots one expression may use. The deepest installed reward condition needs nine. */
+inline constexpr std::size_t kExpressionStackCapacity = 16;
+
+/** Opcodes the client's expression evaluator implements that the server evaluates. */
+enum class Opcode : std::uint8_t {
+    /** Pushes 1 when the bound flag is set, otherwise 0. */
+    flag = 1,
+    /** Pops one value and pushes 1 when it was zero. */
+    logicalNot = 2,
+    /** Binary operators pop right then left and push the result. */
+    logicalOr = 3,
+    logicalAnd = 4,
+    equal = 8,
+    /** Pushes one bound value. */
+    loadValue = 10,
+    /** Pushes the operand as a signed constant. */
+    constant = 11,
+    greaterThan = 13,
+    greaterOrEqual = 14,
+    lessThan = 15,
+    lessOrEqual = 16,
+    add = 17,
+    /** Pops one value and pushes its two's-complement negation. */
+    negate = 22,
+};
+
+/** Saved bank a flag or value instruction reads once its authored unlock slot is bound. */
+enum class Bank : std::uint8_t {
+    /** Operators and constants read no bank. */
+    none,
+    account,
+    profile,
+    character,
+    /** A flag that is set when the operand names the selected character's class. */
+    characterClass,
+    /** Resolved outside the saved banks. The operand is its identity hash. */
+    external,
+};
+
+/** One bound expression instruction. */
+struct Instruction {
+    Opcode opcode{};
+    Bank bank{Bank::none};
+    /** Bank index, class index, external identity hash, or signed constant. */
+    std::uint32_t operand{};
+};
+
+/** Reads one bound flag. Returns false when the flag cannot be read. */
+using FlagReader = bool (*)(const void* context,
+                            const Instruction& instruction,
+                            bool& set) noexcept;
+/** Reads one bound value. Returns false when the value cannot be read. */
+using ValueReader = bool (*)(const void* context,
+                             const Instruction& instruction,
+                             std::int32_t& value) noexcept;
+
+/** The two readers one evaluation needs, and the caller state they share. */
+struct Inputs {
+    FlagReader flag{};
+    ValueReader value{};
+    const void* context{};
+};
+
+/**
+ * Converts one native opcode word to an evaluated opcode.
+ * @return False for an opcode the server does not evaluate.
+ */
+[[nodiscard]] bool decode_opcode(std::uint32_t native, Opcode& opcode) noexcept;
+
+/** Checks one instruction's bank and its operand against that bank's capacity. */
+[[nodiscard]] bool valid(const Instruction& instruction) noexcept;
+
+/**
+ * Evaluates one expression program.
+ * Every failure is refused, never defaulted: bad opcode, bad stack, unreadable flag or value.
+ * @param program Instructions in evaluation order.
+ * @param inputs Flag and value readers.
+ * @param result Receives whether the final value is nonzero, and is cleared on any refusal.
+ * @return True only when the whole program evaluated to one value.
+ */
+[[nodiscard]] bool
+evaluate(std::span<const Instruction> program, const Inputs& inputs, bool& result) noexcept;
+
+} // namespace sunrise::state::unlocks