Browse Source

Extract authored scene event keys into the SDK pack

The generator follows each scene resource to its graph, requires exactly one
typed array of gate elements, checks every element opens with the graph's tag
and the gate class, and records its key and ordinal. A graph that cannot be
read, or a malformed one, is logged and leaves the scene without keys; only a
topology inconsistency fails the build.

Rows are checked against the inventory before composition and published to Lua
as event_keys on each scene symbol, with the count in the manifest.
Fozkais 1 week ago
parent
commit
de82c6c061

+ 12 - 0
Sunrise/src/client/content/activity/activity_sdk_authored_scene_internal.h

@@ -95,6 +95,11 @@ is_performance_descriptor(const topology::Snapshot& topology,
     return std::tie(row.sceneSlotIndex, row.configTag, row.descriptorOffset, row.squadSlotIndex);
     return std::tie(row.sceneSlotIndex, row.configTag, row.descriptorOffset, row.squadSlotIndex);
 }
 }
 
 
+/** Sort key matching the final event-key pack order: the scene, then the gate. */
+[[nodiscard]] inline auto event_key_natural(const EventKey& row) noexcept {
+    return std::tie(row.slotIndex, row.gateOffset);
+}
+
 /** Sort key matching the final task-target pack order. */
 /** Sort key matching the final task-target pack order. */
 [[nodiscard]] inline auto task_natural(const TaskTarget& row) noexcept {
 [[nodiscard]] inline auto task_natural(const TaskTarget& row) noexcept {
     return std::tie(row.taskSlotIndex, row.configTag, row.descriptorOffset);
     return std::tie(row.taskSlotIndex, row.configTag, row.descriptorOffset);
@@ -176,6 +181,13 @@ schema_index(const topology::Snapshot& topology, const Facts& facts, SchemaIndex
                                const squad::DescriptorFact& descriptor,
                                const squad::DescriptorFact& descriptor,
                                Text& output) noexcept;
                                Text& output) noexcept;
 
 
+/** Formats one event-key ID from the resource, its graph, the scene slot and the gate. */
+[[nodiscard]] bool event_key_id(const topology::Snapshot& topology,
+                                const squad::DescriptorFact& descriptor,
+                                std::uint32_t graphTag,
+                                std::uint32_t gateOffset,
+                                Text& output) noexcept;
+
 /** Formats one scene-to-squad edge ID from its descriptor tuple and the squad slot it names. */
 /** Formats one scene-to-squad edge ID from its descriptor tuple and the squad slot it names. */
 [[nodiscard]] bool edge_id(const topology::Snapshot& topology,
 [[nodiscard]] bool edge_id(const topology::Snapshot& topology,
                            const squad::DescriptorFact& descriptor,
                            const squad::DescriptorFact& descriptor,

+ 163 - 0
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.cpp

@@ -32,6 +32,25 @@ bool resource_id(const topology::Snapshot& topology,
                        static_cast<unsigned>(slot.slotType));
                        static_cast<unsigned>(slot.slotType));
 }
 }
 
 
+/** Formats one event-key ID from the resource, its graph, the scene slot and the gate. */
+bool event_key_id(const topology::Snapshot& topology,
+                  const squad::DescriptorFact& descriptor,
+                  std::uint32_t graphTag,
+                  std::uint32_t gateOffset,
+                  Text& output) noexcept {
+    if (descriptor.slotIndex >= topology.slots.size()) {
+        return false;
+    }
+    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
+    return format_text(output,
+                       "authored-scene-event-key/%08x/%08x/%04x/%04x/%08x",
+                       static_cast<unsigned>(descriptor.configTag),
+                       static_cast<unsigned>(graphTag),
+                       static_cast<unsigned>(slot.slotIndex),
+                       static_cast<unsigned>(slot.slotType),
+                       static_cast<unsigned>(gateOffset));
+}
+
 /** Formats one scene-to-squad edge ID from its descriptor tuple and the squad slot it names. */
 /** Formats one scene-to-squad edge ID from its descriptor tuple and the squad slot it names. */
 bool edge_id(const topology::Snapshot& topology,
 bool edge_id(const topology::Snapshot& topology,
              const squad::DescriptorFact& descriptor,
              const squad::DescriptorFact& descriptor,
@@ -219,6 +238,59 @@ void log_scene_resource(const squad::DescriptorFact& descriptor,
     }
     }
 }
 }
 
 
+/** Logs a scene graph that yielded no event keys, and why. */
+void log_scene_graph(const squad::DescriptorFact& descriptor,
+                     std::uint32_t graphTag,
+                     const char* result,
+                     core::log::Level level) noexcept {
+    std::array<char, 176> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=activity_sdk_scene_graph result=%s config=0x%08X "
+                                      "slot_row=%u graph=0x%08X",
+                                      result,
+                                      static_cast<unsigned>(descriptor.configTag),
+                                      static_cast<unsigned>(descriptor.slotIndex),
+                                      static_cast<unsigned>(graphTag));
+    if (written > 0) {
+        core::log::write(
+            core::log::Channel::client,
+            level,
+            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
+    }
+}
+
+/**
+ * Finds the one typed array of gate elements in 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.
+ */
+[[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;
+    }
+    return found;
+}
+
 /** Reads one tag once and retains the physical class beside its bytes. */
 /** Reads one tag once and retains the physical class beside its bytes. */
 [[nodiscard]] bool package_row(squad::TagReader reader,
 [[nodiscard]] bool package_row(squad::TagReader reader,
                                void* readerContext,
                                void* readerContext,
@@ -315,6 +387,75 @@ void log_scene_resource(const squad::DescriptorFact& descriptor,
 } // namespace
 } // namespace
 
 
 /** Builds both authored-scene sections from complete topology and package facts. */
 /** 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.
+ */
+[[nodiscard]] bool collect_event_keys(const topology::Snapshot& topology,
+                                      const squad::DescriptorFact& descriptor,
+                                      squad::TagReader reader,
+                                      void* readerContext,
+                                      PackageCache& cache,
+                                      std::uint32_t resourceTag,
+                                      std::span<const std::byte> resource,
+                                      std::vector<EventKey>& output) {
+    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);
+        return true;
+    }
+    const PackageRow* graph = nullptr;
+    if (!package_row(reader, readerContext, graphTag, cache, graph) || graph == nullptr) {
+        log_scene_graph(descriptor, graphTag, "unreadable", core::log::Level::warn);
+        return true;
+    }
+    const auto blob = std::span(graph->bytes);
+    std::size_t first = 0;
+    std::uint64_t count = 0;
+    if (!find_gate_array(blob, first, count)) {
+        log_scene_graph(descriptor, graphTag, "gate_array", core::log::Level::warn);
+        return true;
+    }
+    if (count > format::kAuthoredSceneGateCapacity) {
+        log_scene_graph(descriptor, graphTag, "capacity", core::log::Level::warn);
+        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;
+        std::uint32_t owner = 0;
+        std::uint32_t elementClass = 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::kAuthoredSceneGateKeyOffset, key)
+            || !read_value(blob, element + format::kAuthoredSceneGateOrdinalOffset, ordinal)
+            || owner != graphTag || elementClass != format::kAuthoredSceneGateClass || key == 0
+            || key == format::kAbsentIndex) {
+            log_scene_graph(descriptor, graphTag, "gate", core::log::Level::warn);
+            return true;
+        }
+        EventKey row{};
+        if (!event_key_id(
+                topology, descriptor, graphTag, static_cast<std::uint32_t>(element), row.id)) {
+            return false;
+        }
+        row.slotIndex = descriptor.slotIndex;
+        row.resourceTag = resourceTag;
+        row.graphTag = graphTag;
+        row.gateOffset = static_cast<std::uint32_t>(element);
+        row.ordinal = ordinal;
+        row.key = key;
+        row.flags = format::kAuthoredSceneEventKeyExact;
+        output.push_back(row);
+    }
+    return true;
+}
+
 bool build(const topology::Snapshot& topology,
 bool build(const topology::Snapshot& topology,
            const Facts& facts,
            const Facts& facts,
            squad::TagReader reader,
            squad::TagReader reader,
@@ -525,6 +666,16 @@ bool build(const topology::Snapshot& topology,
                         row.flags = format::kAuthoredSceneResourceExact;
                         row.flags = format::kAuthoredSceneResourceExact;
                         pending.resources.push_back(row);
                         pending.resources.push_back(row);
                     }
                     }
+                    if (!collect_event_keys(topology,
+                                            descriptor,
+                                            reader,
+                                            readerContext,
+                                            cache,
+                                            resourceTag,
+                                            std::span(resourcePackage->bytes),
+                                            pending.eventKeys)) {
+                        return false;
+                    }
                 }
                 }
             }
             }
 
 
@@ -635,6 +786,11 @@ bool build(const topology::Snapshot& topology,
             }
             }
         }
         }
         std::sort(pending.resources.begin(), pending.resources.end(), resource_less);
         std::sort(pending.resources.begin(), pending.resources.end(), resource_less);
+        std::sort(pending.eventKeys.begin(),
+                  pending.eventKeys.end(),
+                  [](const EventKey& left, const EventKey& right) {
+                      return event_key_natural(left) < event_key_natural(right);
+                  });
         std::sort(pending.squadEdges.begin(), pending.squadEdges.end(), edge_less);
         std::sort(pending.squadEdges.begin(), pending.squadEdges.end(), edge_less);
         std::sort(pending.taskTargets.begin(), pending.taskTargets.end(), task_less);
         std::sort(pending.taskTargets.begin(), pending.taskTargets.end(), task_less);
         pending.resources.erase(std::unique(pending.resources.begin(),
         pending.resources.erase(std::unique(pending.resources.begin(),
@@ -644,6 +800,13 @@ bool build(const topology::Snapshot& topology,
                                                        == resource_natural(right);
                                                        == resource_natural(right);
                                             }),
                                             }),
                                 pending.resources.end());
                                 pending.resources.end());
+        pending.eventKeys.erase(std::unique(pending.eventKeys.begin(),
+                                            pending.eventKeys.end(),
+                                            [](const auto& left, const auto& right) {
+                                                return event_key_natural(left)
+                                                       == event_key_natural(right);
+                                            }),
+                                pending.eventKeys.end());
         pending.squadEdges.erase(std::unique(pending.squadEdges.begin(),
         pending.squadEdges.erase(std::unique(pending.squadEdges.begin(),
                                              pending.squadEdges.end(),
                                              pending.squadEdges.end(),
                                              [](const auto& left, const auto& right) {
                                              [](const auto& left, const auto& right) {

+ 15 - 0
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.h

@@ -37,6 +37,19 @@ struct Resource final {
     std::uint32_t reserved{};
     std::uint32_t reserved{};
 };
 };
 
 
+/** One event gate of a scene's graph before its ID is linked. */
+struct EventKey final {
+    Text id{};
+    std::uint32_t slotIndex{};
+    std::uint32_t resourceTag{};
+    std::uint32_t graphTag{};
+    std::uint32_t gateOffset{};
+    std::int32_t ordinal{};
+    std::uint32_t key{};
+    std::uint32_t flags{};
+    std::uint32_t reserved{};
+};
+
 /** One exact format-v12 same-object scene-to-squad edge before its ID is linked. */
 /** One exact format-v12 same-object scene-to-squad edge before its ID is linked. */
 struct SquadEdge final {
 struct SquadEdge final {
     Text id{};
     Text id{};
@@ -101,6 +114,8 @@ struct Facts final {
 /** Canonical section-19 and section-22 rows with unresolved string references. */
 /** Canonical section-19 and section-22 rows with unresolved string references. */
 struct Snapshot final {
 struct Snapshot final {
     std::vector<Resource> resources{};
     std::vector<Resource> resources{};
+    /** Every gate of every resourced scene, in slot then gate order. */
+    std::vector<EventKey> eventKeys{};
     std::vector<SquadEdge> squadEdges{};
     std::vector<SquadEdge> squadEdges{};
     std::vector<TaskTarget> taskTargets{};
     std::vector<TaskTarget> taskTargets{};
     std::vector<DialogueCueText> dialogueCueTexts{};
     std::vector<DialogueCueText> dialogueCueTexts{};

+ 25 - 0
Sunrise/src/client/content/activity/activity_sdk_authored_scene_validation.cpp

@@ -227,6 +227,31 @@ bool validate(const topology::Snapshot& topology,
             return false;
             return false;
         }
         }
     }
     }
+    for (std::size_t index = 0; index < snapshot.eventKeys.size(); ++index) {
+        const EventKey& row = snapshot.eventKeys[index];
+        // A key belongs to a resourced scene and names the resource its graph hangs off.
+        const auto resource = std::find_if(
+            snapshot.resources.begin(),
+            snapshot.resources.end(),
+            [&row](const Resource& candidate) { return candidate.slotIndex == row.slotIndex; });
+        const squad::DescriptorFact* descriptor = resource == snapshot.resources.end()
+                                                      ? nullptr
+                                                      : find_descriptor(descriptors,
+                                                                        resource->slotIndex,
+                                                                        resource->configTag,
+                                                                        resource->descriptorOffset);
+        Text expectedId{};
+        if (descriptor == nullptr || resource->resourceTag != row.resourceTag || row.graphTag == 0
+            || row.graphTag == format::kAbsentIndex || row.key == 0
+            || row.key == format::kAbsentIndex || row.flags != format::kAuthoredSceneEventKeyExact
+            || row.reserved != 0
+            || !event_key_id(topology, *descriptor, row.graphTag, row.gateOffset, expectedId)
+            || expectedId.value != row.id.value || expectedId.length != row.id.length
+            || (index != 0
+                && event_key_natural(snapshot.eventKeys[index - 1]) >= event_key_natural(row))) {
+            return false;
+        }
+    }
     for (std::size_t index = 0; index < snapshot.squadEdges.size(); ++index) {
     for (std::size_t index = 0; index < snapshot.squadEdges.size(); ++index) {
         const SquadEdge& row = snapshot.squadEdges[index];
         const SquadEdge& row = snapshot.squadEdges[index];
         const squad::DescriptorFact* descriptor =
         const squad::DescriptorFact* descriptor =

+ 1 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_artifacts.h

@@ -79,6 +79,7 @@ struct Source final {
     std::span<const ScenarioWorldSource> scenarioWorldSources{};
     std::span<const ScenarioWorldSource> scenarioWorldSources{};
     std::span<const format::CombatObjectiveGroup> combatObjectiveGroups{};
     std::span<const format::CombatObjectiveGroup> combatObjectiveGroups{};
     std::span<const format::ActorAbility> actorAbilities{};
     std::span<const format::ActorAbility> actorAbilities{};
+    std::span<const format::AuthoredSceneEventKey> authoredSceneEventKeys{};
 };
 };
 
 
 /** One readable generated Lua module with a filesystem-safe canonical name. */
 /** One readable generated Lua module with a filesystem-safe canonical name. */

+ 2 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp

@@ -49,6 +49,7 @@ bool render_contract_files(const Source& source, Bundle& output) noexcept {
                  {"actor_sequence_tables", number(source.actorSequenceTables.size())},
                  {"actor_sequence_tables", number(source.actorSequenceTables.size())},
                  {"actor_sequence_entries", number(source.actorSequenceEntries.size())},
                  {"actor_sequence_entries", number(source.actorSequenceEntries.size())},
                  {"actor_sequence_bindings", number(source.actorSequenceBindings.size())},
                  {"actor_sequence_bindings", number(source.actorSequenceBindings.size())},
+                 {"authored_scene_event_keys", number(source.authoredSceneEventKeys.size())},
              })},
              })},
         });
         });
         if (!render_json(manifest, 0, output.manifestJson)) {
         if (!render_json(manifest, 0, output.manifestJson)) {
@@ -360,6 +361,7 @@ local EventKind = {
 ---@field activate fun(self: SunriseScene, args: {spawn: boolean?}?): SunriseRequestKey
 ---@field activate fun(self: SunriseScene, args: {spawn: boolean?}?): SunriseRequestKey
 ---@field stop fun(self: SunriseScene, args: table?): SunriseRequestKey
 ---@field stop fun(self: SunriseScene, args: table?): SunriseRequestKey
 ---@field send_event fun(self: SunriseScene, args: {key: integer}): SunriseRequestKey
 ---@field send_event fun(self: SunriseScene, args: {key: integer}): SunriseRequestKey
+---@field event_keys integer[]|nil
 
 
 ---@class SunriseTaskTarget
 ---@class SunriseTaskTarget
 ---@field id string
 ---@field id string

+ 13 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp

@@ -394,6 +394,19 @@ bool render_mission(const Source& source,
             append_hex(output, scene.configTag);
             append_hex(output, scene.configTag);
             output.append(", resource_tag = ");
             output.append(", resource_tag = ");
             append_hex(output, scene.resourceTag);
             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;
+                }
+                output.append(anyKey ? ", " : ", event_keys = { ");
+                append_hex(output, gate.key);
+                anyKey = true;
+            }
+            if (anyKey) {
+                output.append(" }");
+            }
             output.append(" },\n");
             output.append(" },\n");
             sceneConstants.append("    ");
             sceneConstants.append("    ");
             sceneConstants.append(key);
             sceneConstants.append(key);

+ 1 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_publish.cpp

@@ -427,6 +427,7 @@ bool is_current(const wchar_t* sdkDirectory, const state::activity_sdk::Catalog&
         source.actorSequenceTables = catalog.actor_sequence_tables();
         source.actorSequenceTables = catalog.actor_sequence_tables();
         source.actorSequenceEntries = catalog.actor_sequence_entries();
         source.actorSequenceEntries = catalog.actor_sequence_entries();
         source.actorSequenceBindings = catalog.actor_sequence_bindings();
         source.actorSequenceBindings = catalog.actor_sequence_bindings();
+        source.authoredSceneEventKeys = catalog.authored_scene_event_keys();
         Bundle expected{};
         Bundle expected{};
         if (!internal::render_contract_files(source, expected)) {
         if (!internal::render_contract_files(source, expected)) {
             return false;
             return false;

+ 2 - 1
Sunrise/src/client/content/activity/activity_sdk_native_pack_pipeline.cpp

@@ -212,7 +212,8 @@ lua_source(const state::activity_sdk::identity::Expected& identity,
             storage.actorSequenceBindings,
             storage.actorSequenceBindings,
             worldSources,
             worldSources,
             storage.combatObjectiveGroups,
             storage.combatObjectiveGroups,
-            storage.actorAbilities};
+            storage.actorAbilities,
+            storage.authoredSceneEventKeys};
 }
 }
 
 
 } // namespace
 } // namespace

+ 6 - 0
Sunrise/src/client/content/activity/activity_sdk_pack_composer.cpp

@@ -696,6 +696,11 @@ link_text(const StringLinker& linker, const Text& input, format::StringRef& outp
             return false;
             return false;
         }
         }
     }
     }
+    for (const authored_scene_inventory::EventKey& row : authoredScenes.eventKeys) {
+        if (!keep_legacy(linker, row.id)) {
+            return false;
+        }
+    }
     for (const authored_scene_inventory::SquadEdge& row : authoredScenes.squadEdges) {
     for (const authored_scene_inventory::SquadEdge& row : authoredScenes.squadEdges) {
         if (!keep_legacy(linker, row.id)) {
         if (!keep_legacy(linker, row.id)) {
             return false;
             return false;
@@ -775,6 +780,7 @@ pack::Tables Storage::tables() const noexcept {
         combatObjectiveGroups,
         combatObjectiveGroups,
         actorAbilities,
         actorAbilities,
         actorAbilityTargets,
         actorAbilityTargets,
+        authoredSceneEventKeys,
     };
     };
 }
 }
 
 

+ 1 - 0
Sunrise/src/client/content/activity/activity_sdk_pack_composer.h

@@ -113,6 +113,7 @@ struct Storage final {
     std::vector<format::CombatObjectiveGroup> combatObjectiveGroups{};
     std::vector<format::CombatObjectiveGroup> combatObjectiveGroups{};
     std::vector<format::ActorAbility> actorAbilities{};
     std::vector<format::ActorAbility> actorAbilities{};
     std::vector<format::ActorAbilityTarget> actorAbilityTargets{};
     std::vector<format::ActorAbilityTarget> actorAbilityTargets{};
+    std::vector<format::AuthoredSceneEventKey> authoredSceneEventKeys{};
 
 
     /** @return Borrowed sections in exact current format order. */
     /** @return Borrowed sections in exact current format order. */
     [[nodiscard]] pack::Tables tables() const noexcept;
     [[nodiscard]] pack::Tables tables() const noexcept;

+ 17 - 0
Sunrise/src/client/content/activity/activity_sdk_pack_composer_rows.cpp

@@ -631,6 +631,23 @@ translate_native_rows(const Inputs& inputs, const detail::StringResolver& linker
         }
         }
     }
     }
 
 
+    output.authoredSceneEventKeys.resize(authoredScenes.eventKeys.size());
+    for (std::size_t index = 0; index < authoredScenes.eventKeys.size(); ++index) {
+        const authored_scene_inventory::EventKey& input = authoredScenes.eventKeys[index];
+        format::AuthoredSceneEventKey& target = output.authoredSceneEventKeys[index];
+        target.sceneSlotIndex = input.slotIndex;
+        target.resourceTag = input.resourceTag;
+        target.graphTag = input.graphTag;
+        target.gateOffset = input.gateOffset;
+        target.ordinal = input.ordinal;
+        target.key = input.key;
+        target.flags = input.flags;
+        target.reserved = input.reserved;
+        if (!link_text(linker, input.id, target.id)) {
+            return false;
+        }
+    }
+
     output.authoredSceneSquadEdges.resize(authoredScenes.squadEdges.size());
     output.authoredSceneSquadEdges.resize(authoredScenes.squadEdges.size());
     for (std::size_t index = 0; index < authoredScenes.squadEdges.size(); ++index) {
     for (std::size_t index = 0; index < authoredScenes.squadEdges.size(); ++index) {
         const authored_scene_inventory::SquadEdge& input = authoredScenes.squadEdges[index];
         const authored_scene_inventory::SquadEdge& input = authoredScenes.squadEdges[index];

+ 1 - 0
Sunrise/src/client/content/activity/activity_sdk_pack_composer_validate.cpp

@@ -61,6 +61,7 @@ bool validate_storage(const Inputs&,
         value.combatObjectiveGroups.size(),
         value.combatObjectiveGroups.size(),
         value.actorAbilities.size(),
         value.actorAbilities.size(),
         value.actorAbilityTargets.size(),
         value.actorAbilityTargets.size(),
+        value.authoredSceneEventKeys.size(),
     };
     };
     const bool bounded = std::all_of(counts.begin(), counts.end(), [](std::size_t count) {
     const bool bounded = std::all_of(counts.begin(), counts.end(), [](std::size_t count) {
         return count <= (std::numeric_limits<std::uint32_t>::max)();
         return count <= (std::numeric_limits<std::uint32_t>::max)();