Procházet zdrojové kódy

Join a collectible to the chapter record it completes

A collectible row and a record row both name a lore row at +0x2C, the same
offset in each table, which is why searching one table for the other's hash
never found anything: neither stores a hash, both store a row index.

Both domains now carry that row, and a record can be found by it. Confessions'
nine chapters are collectibles 1630 and 1632 to 1639 against lore rows 1086 to
1094, one each, verified against the published manifest.

A record naming no lore row is a book's parent triumph rather than a chapter.
That is a reliable parent test and does not depend on a child's position in the
list, which is what earlier attempts relied on.
Millie před 2 týdny
rodič
revize
b881861c00

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

@@ -127,6 +127,11 @@ bool build_collectibles(const reader::Source& source,
         output.collectibleHash = collectibleHash;
         output.collectibleIndex = static_cast<std::uint16_t>(row);
         output.itemDefinitionIndex = itemDefinitionIndex;
+        // The lore row this collectible unlocks. The record displaying the same row is the chapter
+        // it completes, so the two tables join on the row rather than on any hash. Read after the
+        // reset above, or it would be cleared.
+        std::memcpy(&output.loreRow, table.data() + at + tables::kLoreRowOffset,
+                    sizeof output.loreRow);
         if (requirementSetIndex == domain::kUnavailableMaterialRequirementSetIndex) {
             continue;
         }

+ 3 - 0
Sunrise/src/client/content/items/packages/package_record_build.cpp

@@ -145,6 +145,9 @@ bool build_records(const reader::Source& source,
         domain::Definition& definition = output[static_cast<std::size_t>(row)];
         definition = {};
         definition.definitionIndex = static_cast<std::uint16_t>(row);
+        // The lore row this record displays, or 0xFFFF for a book's parent triumph.
+        std::memcpy(&definition.loreRow, blob.data() + at + tables::kLoreRowOffset,
+                    sizeof definition.loreRow);
         // The shipped table tops out at 500, so anything wider is not a score and is dropped.
         definition.scoreValue = score <= 0xFFFFU ? static_cast<std::uint16_t>(score) : 0U;
         std::int16_t categorySlot = 0;

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

@@ -39,6 +39,19 @@ inline constexpr std::size_t kRecordTableSlot = 72;
 inline constexpr std::size_t kRecordRowStride = 216;
 /** Unlock slot of the record's completion flag, or a non-positive value when it has none. */
 inline constexpr std::size_t kRecordCompletionFlagOffset = 100;
+/**
+ * Lore row a record names, or 0xFFFF when it names none.
+ *
+ * A collectible row carries the same field at the same offset, which is what joins the two: a
+ * collectible and the record it unlocks name one lore row. A record with no lore is a book's parent
+ * triumph rather than one of its chapters.
+ */
+inline constexpr std::size_t kLoreRowOffset = 0x2C;
+/** Investment root slot of the lore table: 1425 rows of sixteen bytes. */
+inline constexpr std::size_t kLoreTableSlot = 52;
+/** One lore row, and the definition hash inside it. */
+inline constexpr std::size_t kLoreRowStride = 16;
+inline constexpr std::size_t kLoreHashOffset = 8;
 /** Points the record is worth. Zero for lore and for the interval records that score per step. */
 inline constexpr std::size_t kRecordScoreOffset = 92;
 /** A record names its category's value slot here. The record's own bar reads the next slot up. */

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

@@ -13,6 +13,9 @@ inline constexpr std::size_t kDefinitionCapacity = 1U << 15U;
 inline constexpr std::uint16_t kUnavailableItemDefinitionIndex = 0xFFFFU;
 /** A collectible with no acquisition charge carries this native requirement-set sentinel. */
 inline constexpr std::uint16_t kUnavailableMaterialRequirementSetIndex = 0xFFFFU;
+
+/** A collectible that unlocks no lore carries this instead of a row. */
+inline constexpr std::uint16_t kUnavailableLoreRow = 0xFFFFU;
 /** Installed requirement sets contain at most six material rows. */
 inline constexpr std::size_t kMaterialRequirementCapacity = 6;
 
@@ -31,6 +34,13 @@ struct Definition {
     std::uint16_t collectibleIndex{};
     std::uint16_t itemDefinitionIndex{kUnavailableItemDefinitionIndex};
     std::uint16_t materialRequirementSetIndex{kUnavailableMaterialRequirementSetIndex};
+    /**
+     * Lore row this collectible unlocks, or kUnavailableLoreRow when it unlocks none.
+     *
+     * The record that displays the same row is the chapter this collectible completes, so the pair
+     * joins on the row rather than on any hash.
+     */
+    std::uint16_t loreRow{kUnavailableLoreRow};
     std::uint8_t materialRequirementCount{};
     std::array<MaterialRequirement, kMaterialRequirementCapacity> materialRequirements{};
 };

+ 10 - 0
Sunrise/src/state/build_data/records/definition.h

@@ -20,6 +20,9 @@ inline constexpr std::uint16_t kTriumphScoreValueIndex = 2115U;
 /** A record whose completion flag no mapping table addresses carries this instead of an index. */
 inline constexpr std::uint16_t kUnavailableFlagIndex = 0xFFFFU;
 
+/** A record that displays no lore carries this instead of a row. */
+inline constexpr std::uint16_t kUnavailableLoreRow = 0xFFFFU;
+
 /** A record naming no category value slot carries this instead of an index. */
 inline constexpr std::uint16_t kUnavailableValueIndex = 0xFFFFU;
 
@@ -35,6 +38,13 @@ struct Definition {
     std::uint16_t definitionIndex{};
     /** Account flag bank mapping row, or kUnavailableFlagIndex when the slot is unaddressable. */
     std::uint16_t completionFlagIndex{kUnavailableFlagIndex};
+    /**
+     * Lore row this record displays, or kUnavailableLoreRow when it displays none.
+     *
+     * A chapter of a lore book names one. A book's parent triumph names none, which is a reliable
+     * way to tell a parent from a chapter without relying on its position in the child list.
+     */
+    std::uint16_t loreRow{kUnavailableLoreRow};
     /** Points this record is worth, which the shipped table keeps at 500 or below. */
     std::uint16_t scoreValue{};
     /**

+ 16 - 0
Sunrise/src/state/build_data/records/record_catalog.cpp

@@ -55,6 +55,22 @@ bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
     return g_definitions.snapshot(output, count);
 }
 
+/** Finds the record that displays one lore row. */
+bool find_by_lore_row(std::uint16_t loreRow, Definition& definition) noexcept {
+    if (loreRow == kUnavailableLoreRow) {
+        return false;
+    }
+    const Lock::Shared guard(g_lock);
+    for (const Definition& row : g_definitions.rows()) {
+        if (row.loreRow != loreRow) {
+            continue;
+        }
+        definition = row;
+        return true;
+    }
+    return false;
+}
+
 /** @return Number of generated record definitions, read under the lock. */
 std::size_t count() noexcept {
     const Lock::Shared guard(g_lock);

+ 11 - 0
Sunrise/src/state/build_data/records/record_catalog.h

@@ -43,4 +43,15 @@ void clear() noexcept;
 /** @return Number of generated record definitions, read under the lock. */
 [[nodiscard]] std::size_t count() noexcept;
 
+/**
+ * Finds the record that displays one lore row.
+ *
+ * A collectible and the chapter record it completes name the same lore row, so this is the join
+ * between them. A book's parent triumph names no row and is never returned.
+ * @param loreRow Lore table row, from a collectible or from elsewhere.
+ * @param definition Receives the record.
+ * @return True when exactly one record displays that row.
+ */
+[[nodiscard]] bool find_by_lore_row(std::uint16_t loreRow, Definition& definition) noexcept;
+
 } // namespace sunrise::state::build_data::records