Parcourir la source

Name quest initialization schema constants and state sentinels

Gage Fulwood il y a 3 jours
Parent
commit
7e8110830f

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

@@ -108,7 +108,7 @@ bool build_item_rows(const reader::Source& source,
             corrected_plug_category(item.definitionHash, item.plugCategoryHash);
         build_items::QuestInitialization quest{};
         const auto parentIndex = tables::items::quest_parent(storage.definition);
-        if (needDefinitions && itemClass == 0x80807BEAU && parentIndex < table.count) {
+        if (needDefinitions && itemClass == tables::kItemDefinitionClass && parentIndex < table.count) {
             std::span<const std::byte> parent = storage.definition;
             tables::IndexRow parentRow{};
             std::uint32_t parentClass = itemClass;
@@ -122,7 +122,7 @@ bool build_item_rows(const reader::Source& source,
             if (parentIndex != item.definitionIndex) {
                 parent = storage.questParentDefinition;
             }
-            if (parentReady && parentClass == 0x80807BEAU) {
+            if (parentReady && parentClass == tables::kItemDefinitionClass) {
                 quest =
                     tables::items::read_quest_initialization(storage.definition,
                                                              item.definitionIndex,

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

@@ -30,6 +30,8 @@ inline constexpr std::uint16_t kAbsentPackageId = 0xFFFFU;
 
 /** Element class of the item index table inside the investment container. */
 inline constexpr std::uint32_t kItemIndexTableClass = 0x80807BE8U;
+/** Serialized item definition class, distinct from the item index table class. */
+inline constexpr std::uint32_t kItemDefinitionClass = 0x80807BEAU;
 /** Investment root slot of the records and lore table. */
 inline constexpr std::size_t kRecordTableSlot = 72;
 /** One record row, wider than any field this pass reads. */

+ 96 - 28
Sunrise/src/middleware/content/packages/tables/quest_initialization_reader.cpp

@@ -10,6 +10,46 @@ namespace {
 
 using Quest = state::build_data::items::QuestInitialization;
 
+// Serialized layout offsets are relative to the definition, block, or row named below,
+// not process addresses. Class IDs identify the expected serialized block/element type.
+constexpr std::size_t kMinimumQuestDefinitionSize = 0xF0;
+constexpr std::size_t kBlockClassPrefixSize = sizeof(std::uint32_t);
+constexpr std::size_t kMinimumQuestBlockSize = 0x20;
+
+constexpr std::size_t kItemObjectiveBlockOffset = 0x30;
+constexpr std::uint32_t kItemObjectiveBlockClass = 0x808077EBU;
+constexpr std::size_t kObjectiveParentItemOffset = 0x1C;
+constexpr std::size_t kObjectiveReferenceStride = sizeof(std::uint16_t);
+
+constexpr std::size_t kItemQuestSetBlockOffset = 0x60;
+constexpr std::uint32_t kQuestSetBlockClass = 0x808077C8U;
+constexpr std::size_t kQuestSetValueSlotOffset = 0x10;
+constexpr std::size_t kQuestSetModeOffset = 0x1C;
+/** Only this authored mode is supported; no semantics are assumed for other modes. */
+constexpr std::uint8_t kSupportedQuestSetMode = 1;
+constexpr std::uint32_t kQuestSetMemberClass = 0x808077CAU;
+constexpr std::size_t kQuestSetMemberStride = 8;
+constexpr std::size_t kQuestSetMemberValueOffset = 0;
+constexpr std::size_t kQuestSetMemberItemOffset = 4;
+constexpr std::size_t kQuestSetMemberReservedOffset = 6;
+
+constexpr std::size_t kItemUnlockBlockOffset = 0x90;
+constexpr std::uint32_t kItemUnlockBlockClass = 0x808077ABU;
+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;
+/** Separate objective-free roots are supported only in this native bucket. */
+constexpr std::uint8_t kSeparateQuestRootBucketId = 37;
+
+/** The remaining maps are scanned to reject matches outside account/character state. */
+constexpr std::size_t kThirdValueMapDescriptor = 40;
+constexpr std::size_t kFourthValueMapDescriptor = 56;
+constexpr std::size_t kValueMapRowStride = 8;
+constexpr std::size_t kValueMapSlotOffset = 4;
+constexpr std::size_t kValueMapReservedOffset = 6;
+constexpr std::uint16_t kUnavailableValueMapRow = 0xFFFFU;
+
 /** These are serialized block pointers, not count/relative array descriptors. */
 [[nodiscard]] bool block(std::span<const std::byte> bytes,
                          std::size_t field,
@@ -22,13 +62,14 @@ using Quest = state::build_data::items::QuestInitialization;
         return false;
     }
     const auto target = relative + static_cast<std::int64_t>(field);
-    if (target < 4 || static_cast<std::uint64_t>(target) > bytes.size()
-        || bytes.size() - static_cast<std::size_t>(target) < 32) {
+    if (target < static_cast<std::int64_t>(kBlockClassPrefixSize)
+        || static_cast<std::uint64_t>(target) > bytes.size()
+        || bytes.size() - static_cast<std::size_t>(target) < kMinimumQuestBlockSize) {
         return false;
     }
     offset = static_cast<std::size_t>(target);
     std::uint32_t actualClass = 0;
-    return read(bytes, offset - 4, actualClass) && actualClass == expectedClass;
+    return read(bytes, offset - kBlockClassPrefixSize, actualClass) && actualClass == expectedClass;
 }
 
 /** Bounds an authored array using its element class and fixed stride. */
@@ -47,13 +88,19 @@ using Quest = state::build_data::items::QuestInitialization;
 std::uint16_t quest_parent(std::span<const std::byte> definition) noexcept {
     std::uint8_t bucket = 0;
     std::size_t objective = 0;
-    std::uint16_t parent = 0xFFFFU;
+    std::uint16_t parent = kUnavailableQuestParent;
     Array objectives{};
-    if (definition.size() < 240 || !read(definition, kBucketIdOffset, bucket) || bucket != 40
-        || !block(definition, 0x30, 0x808077EBU, objective)
-        || !array(definition, objective, 0x808087B1U, 2, objectives)
-        || !read(definition, objective + 0x1C, parent)) {
-        return 0xFFFFU;
+    if (definition.size() < kMinimumQuestDefinitionSize
+        || !read(definition, kBucketIdOffset, bucket)
+        || bucket != state::build_data::items::kPursuitBucketId
+        || !block(definition, kItemObjectiveBlockOffset, kItemObjectiveBlockClass, objective)
+        || !array(definition,
+                  objective,
+                  kObjectiveReferenceArrayClass,
+                  kObjectiveReferenceStride,
+                  objectives)
+        || !read(definition, objective + kObjectiveParentItemOffset, parent)) {
+        return kUnavailableQuestParent;
     }
     return parent;
 }
@@ -69,23 +116,32 @@ Quest read_quest_initialization(std::span<const std::byte> definition,
     std::uint16_t slot = 0;
     Array members{}, flags{};
     const auto parentIndex = quest_parent(definition);
-    if (itemIndex >= itemCount || parentIndex >= itemCount || parent.size() < 240
-        || !block(parent, 0x60, 0x808077C8U, set) || !read(parent, set + 0x1C, mode) || mode != 1
-        || !read(parent, set + 0x10, slot) || slot >= 32768
-        || !array(parent, set, 0x808077CAU, 8, members) || members.count > itemCount) {
+    if (itemIndex >= itemCount || parentIndex >= itemCount
+        || parent.size() < kMinimumQuestDefinitionSize
+        || !block(parent, kItemQuestSetBlockOffset, kQuestSetBlockClass, set)
+        || !read(parent, set + kQuestSetModeOffset, mode) || mode != kSupportedQuestSetMode
+        || !read(parent, set + kQuestSetValueSlotOffset, slot) || slot >= kUnlockSlotLimit
+        || !array(parent, set, kQuestSetMemberClass, kQuestSetMemberStride, members)
+        || members.count > itemCount) {
         return {};
     }
     std::int64_t unlockRelative = 0;
-    if (!read(definition, 0x90, unlockRelative)
+    if (!read(definition, kItemUnlockBlockOffset, unlockRelative)
         || (unlockRelative != 0
-            && (!block(definition, 0x90, 0x808077ABU, unlock)
+            && (!block(definition, kItemUnlockBlockOffset, kItemUnlockBlockClass, unlock)
                 || !find_optional_array_at(definition, unlock, flags)
-                || (flags.count != 0 && !array(definition, unlock, 0x80807D4BU, 2, flags))))) {
+                || (flags.count != 0
+                    && !array(definition,
+                              unlock,
+                              kItemPresenceFlagClass,
+                              kItemPresenceFlagStride,
+                              flags))))) {
         return {};
     }
     for (std::size_t i = 0; i < flags.count; ++i) {
         std::uint16_t flag = 0;
-        if (!read(definition, flags.dataOffset + i * 2, flag) || flag >= 32768) {
+        if (!read(definition, flags.dataOffset + i * kItemPresenceFlagStride, flag)
+            || flag >= kUnlockSlotLimit) {
             return {};
         }
     }
@@ -97,18 +153,21 @@ 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 != 37 || !read(parent, 0x30, parentObjective) || parentObjective != 0) {
+            || parentBucket != kSeparateQuestRootBucketId
+            || !read(parent, kItemObjectiveBlockOffset, parentObjective) || parentObjective != 0) {
             return {};
         }
     }
     Quest quest{};
     std::size_t matches = 0;
     for (std::size_t i = 0; i < members.count; ++i) {
-        const auto at = members.dataOffset + i * 8;
+        const auto at = members.dataOffset + i * kQuestSetMemberStride;
         std::int32_t value = 0;
         std::uint16_t member = 0, reserved = 0;
-        if (!read(parent, at, value) || !read(parent, at + 4, member)
-            || !read(parent, at + 6, reserved) || reserved != 0 || member >= itemCount) {
+        if (!read(parent, at + kQuestSetMemberValueOffset, value)
+            || !read(parent, at + kQuestSetMemberItemOffset, member)
+            || !read(parent, at + kQuestSetMemberReservedOffset, reserved) || reserved != 0
+            || member >= itemCount) {
             return {};
         }
         if (member == itemIndex) {
@@ -127,28 +186,37 @@ Quest read_quest_initialization(std::span<const std::byte> definition,
 
     // Resolve across all four maps: a context/roster-lane match or duplicate is unsupported.
     matches = 0;
-    for (const std::size_t descriptor : {8U, 24U, 40U, 56U}) {
+    for (const std::size_t descriptor : {kAccountValueMapDescriptor,
+                                         kCharacterValueMapDescriptor,
+                                         kThirdValueMapDescriptor,
+                                         kFourthValueMapDescriptor}) {
         Array rows{};
         if (!find_optional_array_at(valueMap, descriptor, rows) || rows.dataOffset > valueMap.size()
-            || rows.count > (valueMap.size() - rows.dataOffset) / 8) {
+            || rows.count > (valueMap.size() - rows.dataOffset) / kValueMapRowStride) {
             return {};
         }
         for (std::size_t i = 0; i < rows.count; ++i) {
             std::int16_t mappedSlot = -1;
             std::uint16_t reserved = 0;
-            if (!read(valueMap, rows.dataOffset + i * 8 + 4, mappedSlot)
-                || !read(valueMap, rows.dataOffset + i * 8 + 6, reserved)) {
+            if (!read(valueMap,
+                      rows.dataOffset + i * kValueMapRowStride + kValueMapSlotOffset,
+                      mappedSlot)
+                || !read(valueMap,
+                         rows.dataOffset + i * kValueMapRowStride + kValueMapReservedOffset,
+                         reserved)) {
                 return {};
             }
             if (mappedSlot < 0 || static_cast<std::uint16_t>(mappedSlot) != slot) {
                 continue;
             }
-            if (reserved != 0 || ++matches != 1 || i >= 0xFFFFU
-                || (descriptor != 8 && descriptor != 24)) {
+            if (reserved != 0 || ++matches != 1 || i >= kUnavailableValueMapRow
+                || (descriptor != kAccountValueMapDescriptor
+                    && descriptor != kCharacterValueMapDescriptor)) {
                 return {};
             }
             quest.row = static_cast<std::uint16_t>(i);
-            quest.scope = descriptor == 8 ? Quest::Scope::account : Quest::Scope::character;
+            quest.scope = descriptor == kAccountValueMapDescriptor ? Quest::Scope::account
+                                                                   : Quest::Scope::character;
         }
     }
     return matches == 1 && (!separateRoot || quest.scope == Quest::Scope::character)

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

@@ -8,7 +8,10 @@
 
 namespace sunrise::middleware::content::packages::tables::items {
 
-/** Returns the objective block's set-bearing item index, or 0xFFFF when unsupported. */
+/** No supported set-bearing item was resolved from the pursuit definition. */
+inline constexpr std::uint16_t kUnavailableQuestParent = 0xFFFFU;
+
+/** Returns the objective block's set-bearing item index, or kUnavailableQuestParent. */
 [[nodiscard]] std::uint16_t quest_parent(std::span<const std::byte> definition) noexcept;
 
 /**

+ 1 - 1
Sunrise/src/state/build_data/items/item_catalog.cpp

@@ -106,7 +106,7 @@ bool valid(std::span<const Definition> definitions) noexcept {
         if (definition.definitionIndex >= definitions.size() || occupied[definition.definitionIndex]
             || !valid(definition.questInitialization)
             || (definition.questInitialization.scope != QuestInitialization::Scope::none
-                && definition.bucketId != 40)) {
+                && definition.bucketId != kPursuitBucketId)) {
             return false;
         }
         occupied[definition.definitionIndex] = true;

+ 11 - 3
Sunrise/src/state/build_data/items/quest_initialization.h

@@ -6,6 +6,12 @@
 
 namespace sunrise::state::build_data::items {
 
+/** Native pursuit bucket shared by quest items and bounties. */
+inline constexpr std::uint8_t kPursuitBucketId = 40;
+/** Zero is unset state; -1 is excluded as an authored first-step identifier. */
+inline constexpr std::int32_t kUnsetQuestValue = 0;
+inline constexpr std::int32_t kInvalidQuestInitialValue = -1;
+
 /** Authored initial value and bank row for the first member of a supported quest set. */
 struct QuestInitialization {
     enum class Scope : std::uint8_t { none, account, character };
@@ -20,9 +26,9 @@ struct QuestInitialization {
 [[nodiscard]] constexpr bool valid(const QuestInitialization& quest) noexcept {
     using Scope = QuestInitialization::Scope;
     if (quest.scope == Scope::none) {
-        return quest.row == 0 && quest.value == 0;
+        return quest.row == 0 && quest.value == kUnsetQuestValue;
     }
-    return quest.value != 0 && quest.value != -1
+    return quest.value != kUnsetQuestValue && quest.value != kInvalidQuestInitialValue
            && ((quest.scope == Scope::account && quest.row < unlocks::kObjectiveValueCapacity)
                || (quest.scope == Scope::character
                    && quest.row < unlocks::kCharacterObjectValueCapacity));
@@ -31,7 +37,9 @@ struct QuestInitialization {
 /** Set values are identifiers, not a numerically ordered progress counter. */
 [[nodiscard]] constexpr std::int32_t initialized_value(const QuestInitialization& quest,
                                                        std::int32_t before) noexcept {
-    return quest.scope != QuestInitialization::Scope::none && before == 0 ? quest.value : before;
+    return quest.scope != QuestInitialization::Scope::none && before == kUnsetQuestValue
+               ? quest.value
+               : before;
 }
 
 } // namespace sunrise::state::build_data::items

+ 1 - 1
Sunrise/src/state/runtime/runtime.h

@@ -145,7 +145,7 @@ struct PendingItemAcquisition {
         return profileChanged
                || (questInitialization.scope
                        == build_data::items::QuestInitialization::Scope::account
-                   && previousQuestValue == 0);
+                   && previousQuestValue == build_data::items::kUnsetQuestValue);
     }
 };
 

+ 3 - 2
Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp

@@ -40,7 +40,7 @@ using Quest = build_data::items::QuestInitialization;
         return false;
     }
     if (mutation.questInitialization.scope == Quest::Scope::none) {
-        return mutation.previousQuestValue == 0;
+        return mutation.previousQuestValue == build_data::items::kUnsetQuestValue;
     }
     std::int32_t current = 0;
     return investment::store::read_unlock(
@@ -536,7 +536,8 @@ bool commit_item_acquisition(PendingItemAcquisition& mutation) noexcept {
         return false;
     }
     const auto& quest = prepared.questInitialization;
-    if (quest.scope != Quest::Scope::none && prepared.previousQuestValue == 0
+    if (quest.scope != Quest::Scope::none
+        && prepared.previousQuestValue == build_data::items::kUnsetQuestValue
         && !investment::store::write_unlock(quest_bank(quest), quest.row, quest.value)) {
         return false;
     }