Просмотр исходного кода

Resolve supplemental reward rows and drop the unused entry discriminator

Zach 1 месяц назад
Родитель
Сommit
fb0e27c62e

+ 11 - 3
Sunrise/src/client/content/items/packages/package_reward_build.cpp

@@ -19,10 +19,13 @@ namespace tables = middleware::content::packages::tables;
 namespace domain = state::build_data::rewards;
 
 /** Investment-root slots identify reward pools and unlock-slot bindings. */
+constexpr std::size_t kSupplementalRewardSlot = 84;
 constexpr std::size_t kPoolSlot = 88;
 constexpr std::size_t kExpressionSlot = 109;
 constexpr std::size_t kFlagSlot = 112;
 constexpr std::size_t kValueSlot = 114;
+/** Only the omitted-bank sentinel permits a no-op; other invalid tags leave effects unresolved. */
+constexpr std::uint32_t kAbsentTableTag = 0xFFFFFFFFU;
 /** Native reward schema classes and fixed row sizes. */
 constexpr std::uint32_t kPoolClass = 0x80807553U;
 constexpr std::uint32_t kPoolRowClass = 0x8080748CU;
@@ -53,7 +56,7 @@ constexpr std::size_t kAcquiredFlagField = 0xDA;
 /** Byte offsets inside the serialized reward rows. */
 constexpr std::size_t kEntryQuantityOffset = 4;
 constexpr std::size_t kEntryPoolOffset = 8;
-constexpr std::size_t kEntryMappingOffset = 10;
+constexpr std::size_t kEntrySupplementalOffset = 10;
 constexpr std::size_t kEntryCategoryOffset = 20;
 constexpr std::size_t kEntryWeightOffset = 24;
 constexpr std::size_t kEntryConditionOffset = 32;
@@ -113,7 +116,7 @@ bool read_reward_entry(std::span<const std::byte> blob,
     return tables::read(blob, at, entry.itemIndex)
            && tables::read(blob, at + kEntryQuantityOffset, entry.quantity)
            && tables::read(blob, at + kEntryPoolOffset, entry.poolIndex)
-           && tables::read(blob, at + kEntryMappingOffset, entry.mappingIndex)
+           && tables::read(blob, at + kEntrySupplementalOffset, entry.supplementalIndex)
            && tables::read(blob, at + kEntryCategoryOffset, entry.categoryHash)
            && tables::read(blob, at + kEntryWeightOffset, entry.weight)
            && std::isfinite(entry.weight) && entry.weight >= 0;
@@ -157,6 +160,7 @@ bool RewardBuild::entry(std::span<const std::byte> blob, std::size_t at) noexcep
         || !conditions.read(blob, at + kEntryConditionOffset, instructions_, out.condition)) {
         return false;
     }
+    out.supplementalMissing = supplementalMissing_ && out.supplementalIndex != domain::kAbsent;
     tables::Array rows{};
     if (!tables::read_array(
             blob, at + kEntryModifiersOffset, kModifierClass, kModifierStride, rows)) {
@@ -436,6 +440,7 @@ bool RewardBuild::load(const reader::Source& source,
                        std::span<const std::byte> root,
                        const SlotMaps& maps) noexcept {
     loaded_ = false;
+    supplementalMissing_ = false;
     pools_.clear();
     entries_.clear();
     instructions_.clear();
@@ -443,13 +448,16 @@ bool RewardBuild::load(const reader::Source& source,
     sockets_.clear();
     std::vector<std::byte> blob;
     tables::Array rows{};
-    if (!conditions.load(source, scratch, root, maps)
+    std::uint32_t supplementalTag = 0;
+    if (!tables::slot_tag(root, kSupplementalRewardSlot, supplementalTag)
+        || !conditions.load(source, scratch, root, maps)
         || !root_table(source, scratch, root, kPoolSlot, blob, kPoolClass)
         || !tables::read_array(
             blob, tables::kTableArrayDescriptor, kPoolRowClass, kPoolStride, rows)
         || rows.count == 0 || rows.count > domain::kPoolCapacity) {
         return false;
     }
+    supplementalMissing_ = supplementalTag == kAbsentTableTag;
     std::size_t skipped = 0;
     for (std::size_t i = 0; i < rows.count; ++i) {
         const auto at = rows.dataOffset + i * kPoolStride;

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

@@ -62,6 +62,7 @@ private:
                                  std::span<const std::byte> blob,
                                  state::build_data::rewards::Item& item) noexcept;
     bool loaded_{};
+    bool supplementalMissing_{};
     std::vector<state::build_data::rewards::Pool> pools_;
     std::vector<state::build_data::rewards::Entry> entries_;
     std::vector<state::build_data::rewards::Item> items_;

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

@@ -20,7 +20,8 @@ bool encode(const rewards::Entry& value, RewardEntryRecord& record) noexcept {
     record = {};
     record.itemIndex = value.itemIndex;
     record.poolIndex = value.poolIndex;
-    record.mappingIndex = value.mappingIndex;
+    record.supplementalIndex = value.supplementalIndex;
+    record.supplementalMissing = static_cast<std::uint8_t>(value.supplementalMissing);
     record.quantity = value.quantity;
     record.categoryHash = value.categoryHash;
     record.weight = value.weight;
@@ -32,9 +33,14 @@ bool encode(const rewards::Entry& value, RewardEntryRecord& record) noexcept {
 
 bool decode(const RewardEntryRecord& record, rewards::Entry& value) noexcept {
     value = {};
+    if (record.supplementalMissing > 1
+        || (record.supplementalMissing != 0 && record.supplementalIndex == rewards::kAbsent)) {
+        return false;
+    }
     value.itemIndex = record.itemIndex;
     value.poolIndex = record.poolIndex;
-    value.mappingIndex = record.mappingIndex;
+    value.supplementalIndex = record.supplementalIndex;
+    value.supplementalMissing = record.supplementalMissing != 0;
     value.quantity = record.quantity;
     value.categoryHash = record.categoryHash;
     value.weight = record.weight;

+ 4 - 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 = 68;
+inline constexpr std::uint32_t kCacheFormatVersion = 69;
 /** 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. */
@@ -622,7 +622,8 @@ struct RewardPoolRecord {
 struct RewardEntryRecord {
     std::uint16_t itemIndex{};
     std::uint16_t poolIndex{};
-    std::uint16_t mappingIndex{};
+    std::uint16_t supplementalIndex{};
+    std::uint8_t supplementalMissing{};
     std::uint32_t quantity{};
     std::uint32_t categoryHash{};
     float weight{};
@@ -655,7 +656,7 @@ static_assert(sizeof(RewardRangeRecord) == 2 * sizeof(std::uint32_t));
 static_assert(sizeof(RewardPoolRecord) == sizeof(std::uint32_t) + sizeof(RewardRangeRecord));
 static_assert(sizeof(RewardEntryRecord)
               == 3 * sizeof(std::uint16_t) + 2 * sizeof(std::uint32_t) + sizeof(float)
-                     + 3 * sizeof(RewardRangeRecord));
+                     + sizeof(std::uint8_t) + 3 * sizeof(RewardRangeRecord));
 static_assert(sizeof(RewardItemRecord)
               == 2 * sizeof(std::uint32_t) + 2 * sizeof(std::uint16_t) + sizeof(std::uint8_t)
                      + rewards::kSelectionCapacity * sizeof(RewardSelectionRecord));

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

@@ -27,8 +27,6 @@ inline constexpr std::uint32_t kSmallProfileSlotCapacity = 6;
 inline constexpr std::uint8_t kUnavailableBucketId = (std::numeric_limits<std::uint8_t>::max)();
 /** Hidden plugs, perks and quest roots use this bucket instead of held inventory. */
 inline constexpr std::uint8_t kNonInventoryBucketId = 37;
-/** Character bucket containing unopened engrams. */
-inline constexpr std::uint8_t kEngramBucketId = 31;
 /** Leaving out the unavailable id leaves at most 255 unique bucket records. */
 inline constexpr std::size_t kDescriptorCapacity = kUnavailableBucketId;
 /** Signed -1 marks a bucket that has no equipment slot. */

+ 3 - 1
Sunrise/src/state/build_data/rewards/definition.h

@@ -69,7 +69,9 @@ struct Modifier {
 struct Entry {
     std::uint16_t itemIndex{kAbsent};
     std::uint16_t poolIndex{kAbsent};
-    std::uint16_t mappingIndex{kAbsent};
+    std::uint16_t supplementalIndex{kAbsent};
+    /** True only when the source bank is explicitly absent from the investment root. */
+    bool supplementalMissing{};
     std::uint32_t quantity{};
     std::uint32_t categoryHash{};
     float weight{};

+ 1 - 0
Sunrise/src/state/build_data/rewards/reward_catalog.cpp

@@ -63,6 +63,7 @@ bool valid_depth(View data,
 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())
+           && (!entry.supplementalMissing || entry.supplementalIndex != kAbsent)
            && fits(entry.condition, data.instructions) && fits(entry.modifiers, data.modifiers)
            && fits(entry.sockets, data.sockets) && std::isfinite(entry.weight) && entry.weight >= 0;
 }

+ 24 - 28
Sunrise/src/state/rewards/reward_resolver.cpp

@@ -4,7 +4,6 @@
 #include <cmath>
 
 #include "../build_data/rewards/reward_catalog.h"
-#include "../build_data/runtime.h"
 #include "middleware/content/packages/tables/unlock_opcode.h"
 
 namespace sunrise::state::rewards {
@@ -15,6 +14,8 @@ namespace definitions = build_data::rewards;
 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;
 
 bool refuse(const Context& context, const char* reason) noexcept {
     if (context.refusal != nullptr) {
@@ -157,9 +158,9 @@ struct Resolver {
     const Context& context;
     Result& result;
     std::uint64_t random;
-    Selection selection;
-    std::array<const definitions::Entry*, definitions::kGrantCapacity> selectedEntries{};
-    std::array<std::uint32_t, definitions::kGrantCapacity> selectedCategories{};
+    std::array<const definitions::Entry*, kDrawCapacity> selectedEntries{};
+    std::array<std::uint32_t, kDrawCapacity> selectedCategories{};
+    std::size_t selectedCount{};
 
     double fraction() noexcept {
         // SplitMix64 makes a prepared seed replayable without shared random state.
@@ -180,18 +181,11 @@ struct Resolver {
             return true;
         }
         // Draws exclude the selected pool row within its category, not every copy of its item.
-        for (std::size_t i = 0; i < result.count; ++i) {
+        for (std::size_t i = 0; i < selectedCount; ++i) {
             if (selectedEntries[i] == &entry && selectedCategories[i] == category) {
                 return true;
             }
         }
-        if (selection == Selection::equipment && entry.poolIndex == definitions::kAbsent) {
-            build_data::items::details::Definition item{};
-            if (!build_data::find_configured_item_detail(entry.itemIndex, item)
-                || !item.equipmentSlot.has_value()) {
-                return true;
-            }
-        }
         bool enabled = false;
         if (!condition(data, entry.condition, context, enabled)) {
             return false;
@@ -225,10 +219,6 @@ struct Resolver {
         if (value == 0) {
             return true;
         }
-        // A mapping can accompany an item or nested pool; all effects belong to the grant.
-        if (entry.mappingIndex != definitions::kAbsent) {
-            return refuse(context, "reward_mapping");
-        }
         if (entry.poolIndex != definitions::kAbsent) {
             double total = 0;
             if (!pool_weight(entry.poolIndex, category, depth + 1, total)) {
@@ -241,7 +231,7 @@ struct Resolver {
             if (entry.itemIndex >= data.items.size()) {
                 return false;
             }
-        } else {
+        } else if (entry.supplementalIndex == definitions::kAbsent) {
             return refuse(context, "reward_target");
         }
         output = value;
@@ -292,19 +282,29 @@ struct Resolver {
         if (chosen == nullptr) {
             return refuse(context, "empty_pool");
         }
+        if (chosen->supplementalIndex != definitions::kAbsent && !chosen->supplementalMissing) {
+            return refuse(context, "supplemental_reward");
+        }
+        // A missing supplemental definition does not cancel the selected item grant.
         if (chosen->poolIndex != definitions::kAbsent) {
             // A pool reference selects one leaf; quantity belongs to that leaf.
             double childTotal = 0;
             return pool_weight(chosen->poolIndex, category, depth + 1, childTotal) && childTotal > 0
                    && draw(chosen->poolIndex, category, depth + 1, childTotal);
         }
+        if (selectedCount == selectedEntries.size()) {
+            return refuse(context, "reward_selection_capacity");
+        }
+        selectedEntries[selectedCount] = chosen;
+        selectedCategories[selectedCount++] = category;
+        if (chosen->itemIndex == definitions::kAbsent) {
+            return true;
+        }
         if (chosen->quantity == 0 || chosen->quantity > INT32_MAX
             || result.count == result.grants.size()
             || !definitions::fits(chosen->sockets, data.sockets)) {
             return false;
         }
-        selectedEntries[result.count] = chosen;
-        selectedCategories[result.count] = category;
         auto& grant = result.grants[result.count++];
         grant.itemIndex = chosen->itemIndex;
         grant.quantity = static_cast<std::int32_t>(chosen->quantity);
@@ -322,7 +322,6 @@ bool resolve_item(definitions::View data,
                   const Context& context,
                   std::uint16_t itemIndex,
                   std::uint32_t quantity,
-                  Selection selection,
                   Result& result) noexcept {
     result = {};
     if (context.refusal != nullptr) {
@@ -347,10 +346,10 @@ bool resolve_item(definitions::View data,
             || item.selectionCount > item.selections.size()) {
             return false;
         }
-        Resolver resolver{data, context, staged, context.seed, selection};
+        Resolver resolver{data, context, staged, context.seed};
         for (std::size_t i = 0; i < item.selectionCount; ++i) {
             const auto& declared = item.selections[i];
-            if (declared.count > staged.grants.size()) {
+            if (declared.count > kDrawCapacity) {
                 return false;
             }
             for (std::size_t j = 0; j < declared.count; ++j) {
@@ -368,7 +367,7 @@ bool resolve_item(definitions::View data,
             }
         }
         if (staged.count == 0) {
-            return false;
+            return refuse(context, "empty_reward");
         }
     }
     result = staged;
@@ -394,23 +393,20 @@ bool eligible(std::span<const definitions::Instruction> instructions,
 bool resolve(const Context& context,
              std::uint16_t itemIndex,
              std::uint32_t quantity,
-             Selection selection,
              Result& result) noexcept {
     struct Request {
         const Context& context;
         std::uint16_t item;
         std::uint32_t quantity;
-        Selection selection;
         Result& result;
-    } request{context, itemIndex, quantity, selection, result};
+    } request{context, itemIndex, quantity, result};
     result = {};
     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.selection, value.result);
+        return resolve_item(data, value.context, value.item, value.quantity, value.result);
     });
 }
 

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

@@ -6,9 +6,6 @@
 
 namespace sunrise::state::rewards {
 
-/** Pass engrams select equipment; ordinary acquisitions expand every declared lane. */
-enum class Selection : std::uint8_t { all, equipment };
-
 struct Grant {
     std::uint16_t itemIndex{build_data::rewards::kAbsent};
     std::int32_t quantity{};
@@ -38,7 +35,6 @@ struct Context {
 [[nodiscard]] bool resolve(const Context& context,
                            std::uint16_t itemIndex,
                            std::uint32_t quantity,
-                           Selection selection,
                            Result& result) noexcept;
 
 } // namespace sunrise::state::rewards

+ 0 - 5
Sunrise/src/state/runtime/state_account_reward_grant_runtime.cpp

@@ -171,13 +171,9 @@ enum class PassResolution { claim, replay };
         || !enabled) {
         return false;
     }
-    build_data::items::Definition item{};
-    const bool engram = build_data::find_item_definition_index(reward.itemIndex, item)
-                        && item.bucketId == inventory_buckets::kEngramBucketId;
     if (!rewards::resolve({flags, account.characters[character].characterClass, seed, reason},
                           reward.itemIndex,
                           reward.quantity,
-                          engram ? rewards::Selection::equipment : rewards::Selection::all,
                           result)) {
         return false;
     }
@@ -340,7 +336,6 @@ bool prepare_item_reward(std::uint16_t itemIndex,
     if (!rewards::resolve({flags, account.characters[character].characterClass, seed, &reason},
                           itemIndex,
                           quantity,
-                          rewards::Selection::all,
                           resolved)) {
         return false;
     }