Browse Source

Answer the review on scene event keys and directive elements

Reach the gate table through the graph's own field instead of scanning the blob, and check
the graph's class on the way.
Drop a scene's keys when one gate is malformed, so a misread graph yields no keys.
Log a scene with no graph at debug; it is not a fault.
Key a directive by its progress label only when it has no description, so shipped scripts
keep their title keys.
Publish a scene's event keys from the sorted section range instead of a full scan.
Move the key collector into the file's anonymous namespace and give build() its comment back.
Delete the per-slot event key accessor nothing calls.
Bump the pack version to 42; 41 is taken on master.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
stan 2 days ago
parent
commit
ab796ea4bb

+ 59 - 40
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.cpp

@@ -261,34 +261,34 @@ void log_scene_graph(const squad::DescriptorFact& descriptor,
 }
 
 /**
- * Finds the one typed array of gate elements in a scene graph.
+ * Reads the gate table field of a scene graph.
  * @param blob Graph bytes.
- * @param first Receives the offset of the first element.
- * @param count Receives the element count.
- * @return False when the graph holds no such array, or more than one.
+ * @param rows Receives the offset of the first table row.
+ * @param count Receives the row count.
+ * @return False when the field does not hold a table of gate rows that fits the graph.
  */
-[[nodiscard]] bool find_gate_array(std::span<const std::byte> blob,
-                                   std::size_t& first,
-                                   std::uint64_t& count) noexcept {
-    bool found = false;
-    for (std::size_t offset = 0; offset + 16 <= blob.size(); offset += 4) {
-        std::uint32_t marker = 0;
-        std::uint32_t elementClass = 0;
-        std::uint64_t declared = 0;
-        if (!read_value(blob, offset, marker) || marker != format::kPackageArrayMarker
-            || !read_value(blob, offset + 4, declared)
-            || !read_value(blob, offset + 12, elementClass)
-            || elementClass != format::kAuthoredSceneGateArrayClass) {
-            continue;
-        }
-        if (found) {
-            return false;
-        }
-        found = true;
-        first = offset + 20;
-        count = declared;
+[[nodiscard]] bool
+read_gate_table(std::span<const std::byte> blob, std::size_t& rows, std::uint64_t& count) noexcept {
+    std::int64_t relative = 0;
+    std::uint32_t marker = 0;
+    std::uint64_t repeated = 0;
+    std::uint32_t rowClass = 0;
+    const std::size_t pointer = format::kAuthoredSceneGateTableOffset + 8U;
+    if (!read_value(blob, format::kAuthoredSceneGateTableOffset, count)
+        || !read_value(blob, pointer, relative) || relative <= 0
+        || static_cast<std::uint64_t>(relative) > blob.size() - pointer) {
+        return false;
+    }
+    // The pointer is relative to its own field and lands on the count of the array header.
+    const std::size_t header = pointer + static_cast<std::size_t>(relative);
+    if (header < 4U || !read_value(blob, header - 4U, marker)
+        || marker != format::kPackageArrayMarker || !read_value(blob, header, repeated)
+        || repeated != count || !read_value(blob, header + 8U, rowClass)
+        || rowClass != format::kAuthoredSceneGateRowClass) {
+        return false;
     }
-    return found;
+    rows = header + 16U;
+    return rows <= blob.size() && count <= (blob.size() - rows) / format::kAuthoredSceneGateRowSize;
 }
 
 /** Reads one tag once and retains the physical class beside its bytes. */
@@ -384,14 +384,10 @@ void log_scene_graph(const squad::DescriptorFact& descriptor,
     return true;
 }
 
-} // namespace
-
-/** Builds both authored-scene sections from complete topology and package facts. */
 /**
  * Follows a scene resource to its event graph and appends one row per gate.
- * A graph that cannot be read or holds no gate array leaves the scene without keys and is
- * logged; a gate that is not a gate element, or a count past the capacity, is a misread graph
- * and is logged the same way. Only a topology inconsistency fails the build.
+ * A graph that cannot be read, or one gate that is not a gate element, leaves the scene with no
+ * keys and is logged. Only a topology inconsistency fails the build.
  */
 [[nodiscard]] bool collect_event_keys(const topology::Snapshot& topology,
                                       const squad::DescriptorFact& descriptor,
@@ -401,10 +397,11 @@ void log_scene_graph(const squad::DescriptorFact& descriptor,
                                       std::uint32_t resourceTag,
                                       std::span<const std::byte> resource,
                                       std::vector<EventKey>& output) {
+    const std::size_t start = output.size();
     std::uint32_t graphTag = 0;
     if (!read_value(resource, format::kAuthoredSceneGraphRelativeOffset, graphTag) || graphTag == 0
         || graphTag == format::kAbsentIndex) {
-        log_scene_graph(descriptor, graphTag, "unreferenced", core::log::Level::warn);
+        log_scene_graph(descriptor, graphTag, "unreferenced", core::log::Level::debug);
         return true;
     }
     const PackageRow* graph = nullptr;
@@ -412,11 +409,15 @@ void log_scene_graph(const squad::DescriptorFact& descriptor,
         log_scene_graph(descriptor, graphTag, "unreadable", core::log::Level::warn);
         return true;
     }
+    if (graph->classId != format::kAuthoredSceneGraphClass) {
+        log_scene_graph(descriptor, graphTag, "graph_class", core::log::Level::warn);
+        return true;
+    }
     const auto blob = std::span(graph->bytes);
-    std::size_t first = 0;
+    std::size_t rows = 0;
     std::uint64_t count = 0;
-    if (!find_gate_array(blob, first, count)) {
-        log_scene_graph(descriptor, graphTag, "gate_array", core::log::Level::warn);
+    if (!read_gate_table(blob, rows, count)) {
+        log_scene_graph(descriptor, graphTag, "gate_table", core::log::Level::warn);
         return true;
     }
     if (count > format::kAuthoredSceneGateCapacity) {
@@ -424,18 +425,33 @@ void log_scene_graph(const squad::DescriptorFact& descriptor,
         return true;
     }
     for (std::uint64_t gate = 0; gate < count; ++gate) {
-        const std::size_t element =
-            first + static_cast<std::size_t>(gate) * format::kAuthoredSceneGateSize;
+        const std::size_t tableRow =
+            rows + static_cast<std::size_t>(gate) * format::kAuthoredSceneGateRowSize;
+        std::uint32_t rowOwner = 0;
+        std::uint32_t rowClass = 0;
+        std::uint64_t body = 0;
+        // A row names its body by absolute offset; the body names the row class back.
+        if (!read_value(blob, tableRow, rowOwner)
+            || !read_value(blob, tableRow + format::kAuthoredSceneGateClassOffset, rowClass)
+            || !read_value(blob, tableRow + format::kAuthoredSceneGateBodyOffset, body)
+            || rowOwner != graphTag || rowClass != format::kAuthoredSceneGateBodyClass
+            || body > blob.size() - format::kAuthoredSceneGateSize) {
+            output.resize(start);
+            log_scene_graph(descriptor, graphTag, "gate_row", core::log::Level::warn);
+            return true;
+        }
+        const auto element = static_cast<std::size_t>(body);
         std::uint32_t owner = 0;
-        std::uint32_t elementClass = 0;
+        std::uint32_t bodyClass = 0;
         std::uint32_t key = 0;
         std::int32_t ordinal = 0;
         if (!read_value(blob, element, owner)
-            || !read_value(blob, element + format::kAuthoredSceneGateClassOffset, elementClass)
+            || !read_value(blob, element + format::kAuthoredSceneGateClassOffset, bodyClass)
             || !read_value(blob, element + format::kAuthoredSceneGateKeyOffset, key)
             || !read_value(blob, element + format::kAuthoredSceneGateOrdinalOffset, ordinal)
-            || owner != graphTag || elementClass != format::kAuthoredSceneGateClass || key == 0
+            || owner != graphTag || bodyClass != format::kAuthoredSceneGateRowClass || key == 0
             || key == format::kAbsentIndex) {
+            output.resize(start);
             log_scene_graph(descriptor, graphTag, "gate", core::log::Level::warn);
             return true;
         }
@@ -456,6 +472,9 @@ void log_scene_graph(const squad::DescriptorFact& descriptor,
     return true;
 }
 
+} // namespace
+
+/** Builds both authored-scene sections from complete topology and package facts. */
 bool build(const topology::Snapshot& topology,
            const Facts& facts,
            squad::TagReader reader,

+ 21 - 11
Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp

@@ -395,16 +395,24 @@ bool render_mission(const Source& source,
             output.append(", resource_tag = ");
             append_hex(output, scene.resourceTag);
             // The graph's gates in order: what set_scene_events publishes to run the scene.
-            bool anyKey = false;
-            for (const format::AuthoredSceneEventKey& gate : source.authoredSceneEventKeys) {
-                if (gate.sceneSlotIndex != slotRow) {
-                    continue;
+            // The section is sorted by scene slot, so the slot's rows are one range.
+            const auto& gates = source.authoredSceneEventKeys;
+            const auto firstGate = std::lower_bound(
+                gates.begin(), gates.end(), slotRow, [](const auto& row, std::uint32_t index) {
+                    return row.sceneSlotIndex < index;
+                });
+            const auto lastGate = std::upper_bound(
+                firstGate, gates.end(), slotRow, [](std::uint32_t index, const auto& row) {
+                    return index < row.sceneSlotIndex;
+                });
+            if (firstGate != lastGate) {
+                output.append(", event_keys = { ");
+                for (auto gate = firstGate; gate != lastGate; ++gate) {
+                    if (gate != firstGate) {
+                        output.append(", ");
+                    }
+                    append_hex(output, gate->key);
                 }
-                output.append(anyKey ? ", " : ", event_keys = { ");
-                append_hex(output, gate.key);
-                anyKey = true;
-            }
-            if (anyKey) {
                 output.append(" }");
             }
             output.append(" },\n");
@@ -597,10 +605,12 @@ bool render_mission(const Source& source,
         for (const std::uint32_t rowIndex : found->second) {
             const format::DirectiveElement& row = source.directiveElements[rowIndex];
             const std::string_view progress = text(source, row.progress);
-            // A counter element shares its title with the plain one; its label tells them apart.
+            // Only a row with no description is new; a described row keeps the title key that
+            // shipped scripts use.
+            const bool labelKey = !progress.empty() && text(source, row.description).empty();
             (void)append_unique_key(directiveConstants,
                                     directiveKeys,
-                                    progress.empty() ? text(source, row.title) : progress,
+                                    labelKey ? progress : text(source, row.title),
                                     row.nameHash);
             directiveConstants.append("{ id = ");
             append_string(directiveConstants, text(source, row.id));

+ 0 - 21
Sunrise/src/state/activity_sdk/activity_sdk_catalog.cpp

@@ -814,27 +814,6 @@ slot_directive_elements(const Catalog& catalog, const format::Slot& slot) noexce
                           static_cast<std::size_t>(last - first));
 }
 
-/** Relies on scene-slot ordering to return one contiguous zero-copy key range. */
-std::span<const format::AuthoredSceneEventKey>
-slot_authored_scene_event_keys(const Catalog& catalog, const format::Slot& slot) noexcept {
-    const auto slots = catalog.slots();
-    if (!owns(slots, slot)) {
-        return {};
-    }
-    const auto values = catalog.authored_scene_event_keys();
-    const std::uint32_t slotIndex = static_cast<std::uint32_t>(&slot - slots.data());
-    const auto first =
-        std::lower_bound(values.begin(), values.end(), slotIndex, [](const auto& row, auto index) {
-            return row.sceneSlotIndex < index;
-        });
-    const auto last =
-        std::upper_bound(first, values.end(), slotIndex, [](auto index, const auto& row) {
-            return index < row.sceneSlotIndex;
-        });
-    return values.subspan(static_cast<std::size_t>(first - values.begin()),
-                          static_cast<std::size_t>(last - first));
-}
-
 /** Relies on scene-slot ordering to return one contiguous zero-copy edge range. */
 std::span<const format::AuthoredSceneSquadEdge>
 slot_authored_scene_squad_edges(const Catalog& catalog, const format::Slot& slot) noexcept {

+ 10 - 8
Sunrise/src/state/activity_sdk/format.h

@@ -10,7 +10,7 @@ namespace sunrise::state::activity_sdk::format {
 /** Eight-byte identity at the start of every runtime SDK pack. */
 inline constexpr std::array<char, 8> kMagic{'S', 'R', 'S', 'D', 'K', 'P', '0', '1'};
 /** Runtime-pack schema version accepted by this reader. */
-inline constexpr std::uint32_t kVersion = 41;
+inline constexpr std::uint32_t kVersion = 42;
 /** The ABI contains only activity identity, topology, placement, and panel metadata. */
 inline constexpr std::uint32_t kSectionCount = 51;
 #if defined(SUNRISE_ACTIVITY_SDK_TESTING)
@@ -287,16 +287,18 @@ inline constexpr std::uint32_t kAuthoredSceneAuthSchema = 0x8080626BU;
 inline constexpr std::uint32_t kAuthoredSceneResourceRelativeOffset = 0x60U;
 inline constexpr std::uint32_t kAuthoredSceneResourceClass = 0x80809C0FU;
 /**
- * A scene resource references its event graph at a fixed field. In the graph, the gates form
- * one typed package array: a marker, a 64-bit count, the element class, then the elements. A
- * gate element opens with the graph's own tag and its class, and carries the event key the
- * server publishes and the gate's ordinal in the scene's progression (-1 for the opening gate
- * every scene shares).
+ * The resource names its graph at a fixed field. The graph's gate table is an array field whose
+ * rows each name one gate body by absolute offset. A body opens with the graph's tag and the row
+ * class, then carries the event key and the gate's ordinal (-1 opens every scene).
  */
 inline constexpr std::uint32_t kAuthoredSceneGraphRelativeOffset = 0xC0U;
+inline constexpr std::uint32_t kAuthoredSceneGraphClass = 0x80809C36U;
 inline constexpr std::uint32_t kPackageArrayMarker = 0x80809FBDU;
-inline constexpr std::uint32_t kAuthoredSceneGateArrayClass = 0x8080638AU;
-inline constexpr std::uint32_t kAuthoredSceneGateClass = 0x8080637DU;
+inline constexpr std::uint32_t kAuthoredSceneGateTableOffset = 0xE8U;
+inline constexpr std::uint32_t kAuthoredSceneGateRowClass = 0x8080637DU;
+inline constexpr std::uint32_t kAuthoredSceneGateRowSize = 0xC0U;
+inline constexpr std::uint32_t kAuthoredSceneGateBodyClass = 0x8080638AU;
+inline constexpr std::uint32_t kAuthoredSceneGateBodyOffset = 0x8U;
 inline constexpr std::uint32_t kAuthoredSceneGateSize = 0x60U;
 inline constexpr std::uint32_t kAuthoredSceneGateClassOffset = 0x4U;
 inline constexpr std::uint32_t kAuthoredSceneGateKeyOffset = 0x10U;

+ 0 - 3
Sunrise/src/state/activity_sdk/runtime.h

@@ -514,9 +514,6 @@ slot_authored_scene_squad_edges(const Catalog& catalog, const format::Slot& slot
 /** Returns every HUD element of one type-68 slot, in name hash then element order. */
 [[nodiscard]] std::span<const format::DirectiveElement>
 slot_directive_elements(const Catalog& catalog, const format::Slot& slot) noexcept;
-/** Returns the event keys of one type-43 slot's graph, in gate order. */
-[[nodiscard]] std::span<const format::AuthoredSceneEventKey>
-slot_authored_scene_event_keys(const Catalog& catalog, const format::Slot& slot) noexcept;
 /** Resolves one validated scene edge to its exact type-1 target slot. */
 [[nodiscard]] const format::Slot*
 authored_scene_linked_squad_slot(const Catalog& catalog,