فهرست منبع

Count a category and its parent record separately

A lore book owns two adjacent value slots. The category's bar counts every
child it owns, the parent record among them; the parent's own bar counts only
the chapters, one fewer, and reads the slot above the category's.

Records now resolve the category slot they name at row field 120 through the
account value map, which is what tells a parent from its chapters: only the
parent names its own category's slot. Nodes resolve the parent slot as the
node's slot plus one, looked up through the mapping table rather than by adding
one to the index, since row order matching slot order is a local accident.

Verified on a cold start: the category reads 5/20 and the parent 5/19.

Reverts the two readiness-gate attempts. all_domains_ready only governs whether
the cache is written, so adding nodes there was inert; adding them to the
investment refresh gate made the package pass retry forever because the nodes
never published on that path. Warm starts still run with an empty node table.
Millie 2 هفته پیش
والد
کامیت
94cadac2ae

+ 8 - 0
Sunrise/src/client/content/items/packages/package_node_build.cpp

@@ -139,6 +139,14 @@ bool build_nodes(const reader::Source& source,
             if (found != indexBySlot.end()) {
                 definition.valueIndex = found->second;
             }
+            // The parent record's own bar reads the next slot up. Resolve it through the mapping
+            // table rather than adding one to the index: rows happen to run in slot order around
+            // here, but nothing guarantees that.
+            const auto parent = indexBySlot.find(
+                static_cast<std::int16_t>(slot + tables::kNodeParentSlotStep));
+            if (parent != indexBySlot.end()) {
+                definition.parentValueIndex = parent->second;
+            }
         }
 
         // Records the node owns, four bytes each as a row and a gate.

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

@@ -27,6 +27,47 @@ void report(const char* stage, unsigned long long detail) noexcept {
     return slot > 0;
 }
 
+/** Reads a value slot out of one expression field of one row, or reports that it names none. */
+[[nodiscard]] bool expression_value_slot(std::span<const std::byte> table,
+                                         std::size_t rowAt,
+                                         std::size_t field,
+                                         std::int16_t& slot) noexcept {
+    std::int64_t count = 0;
+    std::int64_t relative = 0;
+    std::memcpy(&count, table.data() + rowAt + field, sizeof count);
+    std::memcpy(&relative, table.data() + rowAt + field + 8, sizeof relative);
+    if (count < 1 || count > tables::kNodeExpressionCapacity) {
+        return false;
+    }
+    const std::size_t pointerAt = rowAt + field + 8;
+    const std::int64_t target = static_cast<std::int64_t>(pointerAt) + relative
+                                + static_cast<std::int64_t>(tables::kHeaderSkip);
+    if (target < 0
+        || static_cast<std::size_t>(target)
+                   + static_cast<std::size_t>(count) * tables::kUnlockInstructionStride
+               > table.size()) {
+        return false;
+    }
+    const auto base = static_cast<std::size_t>(target);
+    for (std::int64_t index = 0; index < count; ++index) {
+        std::uint32_t opcode = 0;
+        std::uint32_t operand = 0;
+        const std::size_t at =
+            base + static_cast<std::size_t>(index) * tables::kUnlockInstructionStride;
+        std::memcpy(&opcode, table.data() + at, sizeof opcode);
+        std::memcpy(&operand, table.data() + at + 4, sizeof operand);
+        if (opcode > tables::kUnlockOpcodeCeiling) {
+            return false;
+        }
+        if (opcode == tables::kUnlockReadValueOpcode
+            && operand <= static_cast<std::uint32_t>(INT16_MAX)) {
+            slot = static_cast<std::int16_t>(operand);
+            return true;
+        }
+    }
+    return false;
+}
+
 } // namespace
 
 /**
@@ -83,6 +124,40 @@ bool build_records(const reader::Source& source,
         }
     }
 
+    // The account value mapping table, read while the blob is still free. A record names its
+    // category's value slot, and that slot has to become an index the same way a flag slot does.
+    std::uint32_t valueMapTag = 0;
+    tables::Array valueMapRows{};
+    std::vector<std::uint16_t> valueIndexBySlot{};
+    if (tables::slot_tag(root, tables::kUnlockValueMapTableSlot, valueMapTag) && valueMapTag != 0
+        && tables::package_of(valueMapTag) != tables::kAbsentPackageId
+        && reader::read_tag(source, scratch, valueMapTag, blob)
+        && tables::find_array_at(std::span<const std::byte>{blob},
+                                 tables::kAccountValueMapDescriptor,
+                                 valueMapRows)
+        && valueMapRows.count != 0
+        && valueMapRows.dataOffset
+                   + static_cast<std::size_t>(valueMapRows.count) * tables::kUnlockMapRowStride
+               <= blob.size()) {
+        valueIndexBySlot.assign(kSlotSpace, domain::kUnavailableValueIndex);
+        for (std::uint64_t row = 0; row < valueMapRows.count; ++row) {
+            std::int16_t slot = 0;
+            std::memcpy(&slot,
+                        blob.data() + valueMapRows.dataOffset
+                            + static_cast<std::size_t>(row) * tables::kUnlockMapRowStride
+                            + tables::kUnlockMapDestinationSlotOffset,
+                        sizeof slot);
+            if (!addressable_slot(slot) || static_cast<std::size_t>(slot) >= kSlotSpace
+                || row > domain::kUnavailableValueIndex) {
+                continue;
+            }
+            std::uint16_t& existing = valueIndexBySlot[static_cast<std::size_t>(slot)];
+            if (existing == domain::kUnavailableValueIndex) {
+                existing = static_cast<std::uint16_t>(row);
+            }
+        }
+    }
+
     std::uint32_t tableTag = 0;
     tables::Array rows{};
     if (!tables::slot_tag(root, tables::kRecordTableSlot, tableTag) || tableTag == 0
@@ -111,6 +186,16 @@ bool build_records(const reader::Source& source,
         definition.definitionIndex = static_cast<std::uint16_t>(row);
         // 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;
+        if (!valueIndexBySlot.empty()
+            && expression_value_slot(std::span<const std::byte>{blob},
+                                     at,
+                                     tables::kRecordCategoryExpressionField,
+                                     categorySlot)
+            && addressable_slot(categorySlot)
+            && static_cast<std::size_t>(categorySlot) < kSlotSpace) {
+            definition.categoryValueIndex = valueIndexBySlot[static_cast<std::size_t>(categorySlot)];
+        }
         if (addressable_slot(slot) && static_cast<std::size_t>(slot) < kSlotSpace) {
             definition.completionFlagIndex = indexBySlot[static_cast<std::size_t>(slot)];
         }

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

@@ -96,6 +96,10 @@ inline constexpr std::size_t kRecordRowStride = 216;
 inline constexpr std::size_t kRecordCompletionFlagOffset = 100;
 /** 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. */
+inline constexpr std::size_t kRecordCategoryExpressionField = 120;
+/** A category's parent record reads the slot immediately above the category's own. */
+inline constexpr std::int32_t kNodeParentSlotStep = 1;
 
 /** Investment root slot of the presentation node table. */
 inline constexpr std::size_t kPresentationNodeTableSlot = 63;

+ 7 - 0
Sunrise/src/state/build_data/nodes/definition.h

@@ -28,6 +28,13 @@ struct Definition {
     std::uint16_t definitionIndex{};
     /** Account value bank mapping row, or kUnavailableValueIndex when no slot is addressable. */
     std::uint16_t valueIndex{kUnavailableValueIndex};
+    /**
+     * Account value index of the parent record's own bar, one slot above the node's.
+     *
+     * The node's bar counts every child including the parent record; the parent's bar counts only
+     * the chapters. They are separate slots and need separate counts.
+     */
+    std::uint16_t parentValueIndex{kUnavailableValueIndex};
     /** Records this node owns, held at node row `+136`. */
     std::uint8_t childCount{};
     /** Native record rows of the owned records. */

+ 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 naming no category value slot carries this instead of an index. */
+inline constexpr std::uint16_t kUnavailableValueIndex = 0xFFFFU;
+
 /**
  * One record reduced to what a claim needs.
  *
@@ -34,6 +37,13 @@ struct Definition {
     std::uint16_t completionFlagIndex{kUnavailableFlagIndex};
     /** Points this record is worth, which the shipped table keeps at 500 or below. */
     std::uint16_t scoreValue{};
+    /**
+     * Account value index of the category this record names, or kUnavailableValueIndex.
+     *
+     * Only a category's parent record names its category's own slot, so this is what distinguishes
+     * the parent from the chapters beneath it. The parent is excluded from its own progress bar.
+     */
+    std::uint16_t categoryValueIndex{kUnavailableValueIndex};
 };
 
 } // namespace sunrise::state::build_data::records

+ 1 - 9
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp

@@ -104,14 +104,7 @@ Context& context() noexcept {
     return g_context;
 }
 
-/**
- * @return True when every extracted domain is complete in State.
- *
- * The nodes domain is not written to the cache, so counting it here means a warm start finds the
- * build data incomplete and extracts again. That is slower than a cache hit and it is correct:
- * without it the node table is empty on every launch but the first, no presentation node is counted,
- * and no progress bar can move whatever the claims say. Persisting nodes would buy the speed back.
- */
+/** @return True when every extracted domain is complete in State. */
 bool all_domains_ready() noexcept {
     constants::InvestmentConstants published{};
     return runtime::named::ready() && item_definitions_ready() && configured_item_details_ready()
@@ -119,7 +112,6 @@ bool all_domains_ready() noexcept {
            && material_requirement_sets_ready() && inventory_bucket_descriptors_ready()
            && socket_entry_lists_ready() && ability_buckets_ready()
            && progression_definitions_ready() && record_definitions_ready()
-           && node_definitions_ready()
            && scenario_layouts_ready() && spawn_sets_ready()
            && hash_names_ready() && constants::find(published);
 }

+ 19 - 4
Sunrise/src/state/record_claims/record_claims.cpp

@@ -265,7 +265,12 @@ std::size_t apply_node_progress(std::span<std::int32_t> objectiveValues) noexcep
     build_data::nodes::for_each_driving(
         &progress, [](void* context, const build_data::nodes::Definition& node) noexcept {
             auto* state = static_cast<NodeProgress*>(context);
+            // Two counts, because a category and its parent record keep separate bars. The
+            // category counts every child it owns, the parent record among them; the parent's own
+            // bar counts only the chapters, which is why its denominator is one lower. The parent
+            // is the child that names the category's own value slot.
             std::int32_t claimed = 0;
+            std::int32_t claimedChapters = 0;
             for (std::size_t child = 0; child < node.childCount; ++child) {
                 build_data::records::Definition record{};
                 if (!build_data::find_record_definition(node.children[child], record)
@@ -273,10 +278,18 @@ std::size_t apply_node_progress(std::span<std::int32_t> objectiveValues) noexcep
                            == build_data::records::kUnavailableFlagIndex) {
                     continue;
                 }
-                if (claimed_locked(record.completionFlagIndex)) {
-                    ++claimed;
+                if (!claimed_locked(record.completionFlagIndex)) {
+                    continue;
+                }
+                ++claimed;
+                if (record.categoryValueIndex != node.valueIndex) {
+                    ++claimedChapters;
                 }
             }
+            if (static_cast<std::size_t>(node.parentValueIndex) < state->values.size()) {
+                state->values[node.parentValueIndex] = claimedChapters;
+                ++state->written;
+            }
             if (static_cast<std::size_t>(node.valueIndex) < state->values.size()) {
                 state->values[node.valueIndex] = claimed;
                 ++state->written;
@@ -285,10 +298,12 @@ std::size_t apply_node_progress(std::span<std::int32_t> objectiveValues) noexcep
                 std::array<char, 160> line{};
                 const int written = std::snprintf(
                     line.data(), line.size(),
-                    "ev=nodeprog node=%u value_index=%u children=%u claimed=%d",
+                    "ev=nodeprog node=%u value_index=%u parent_index=%u children=%u claimed=%d "
+                    "chapters=%d",
                     static_cast<unsigned>(node.definitionIndex),
                     static_cast<unsigned>(node.valueIndex),
-                    static_cast<unsigned>(node.childCount), claimed);
+                    static_cast<unsigned>(node.parentValueIndex),
+                    static_cast<unsigned>(node.childCount), claimed, claimedChapters);
                 if (written > 0) {
                     core::log::write(core::log::Channel::state, core::log::Level::info,
                                      {line.data(), static_cast<std::size_t>(written)});