Browse Source

Record authored scenes that reference no resource in the SDK pack

A type-43 scene slot can be authored with an empty resource reference: the
slot, its placed config and its descriptor all exist, but the resource field
holds no tag. The generator dropped the resource row and nothing else, so the
pack could not tell such a scene from one whose resource failed to extract,
and the server refused the whole object as a missing resource.

The generator now records the fact instead of losing it. A descriptor whose
resource field is absent flags its slot row (kSlotAuthoredSceneUnresourced,
exported to Lua as `unresourced = true`); a resource that is referenced but
unreadable or of the wrong class still yields no row and is logged as the
generation defect it is. The scene inventory carries the list, validation
checks it, and the pack version moves to 39 so existing packs regenerate.

Across the installed content this marks 50 scenes in 10 packages, including
Homecoming's sc_underwatch_intro; the server-side reader follows separately.
Fozkais 1 week ago
parent
commit
520ed5928f

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

@@ -191,6 +191,30 @@ void log_performance_edge(const squad::DescriptorFact& descriptor, const char* r
     }
 }
 
+/** Logs a scene descriptor that produced no resource row, and why. */
+void log_scene_resource(const squad::DescriptorFact& descriptor,
+                        std::uint32_t resourceTag,
+                        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_resource result=%s config=0x%08X offset=0x%X "
+                      "slot_row=%u resource=0x%08X",
+                      result,
+                      static_cast<unsigned>(descriptor.configTag),
+                      static_cast<unsigned>(descriptor.descriptorOffset),
+                      static_cast<unsigned>(descriptor.slotIndex),
+                      static_cast<unsigned>(resourceTag));
+    if (written > 0) {
+        core::log::write(
+            core::log::Channel::client,
+            level,
+            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
+    }
+}
+
 /** Reads one tag once and retains the physical class beside its bytes. */
 [[nodiscard]] bool package_row(squad::TagReader reader,
                                void* readerContext,
@@ -474,11 +498,19 @@ bool build(const topology::Snapshot& topology,
             if (!read_value(blob, resourceField, resourceTag)) {
                 continue;
             }
-            if (resourceTag != 0 && resourceTag != format::kAbsentIndex) {
+            if (resourceTag == 0 || resourceTag == format::kAbsentIndex) {
+                log_scene_resource(
+                    descriptor, resourceTag, "unresourced", core::log::Level::debug);
+                pending.unresourcedSlots.push_back(descriptor.slotIndex);
+            } else {
                 const PackageRow* resourcePackage = nullptr;
-                if (package_row(reader, readerContext, resourceTag, cache, resourcePackage)
-                    && resourcePackage != nullptr
-                    && resourcePackage->classId == format::kAuthoredSceneResourceClass) {
+                if (!package_row(reader, readerContext, resourceTag, cache, resourcePackage)
+                    || resourcePackage == nullptr) {
+                    log_scene_resource(
+                        descriptor, resourceTag, "unreadable", core::log::Level::warn);
+                } else if (resourcePackage->classId != format::kAuthoredSceneResourceClass) {
+                    log_scene_resource(descriptor, resourceTag, "class", core::log::Level::warn);
+                } else {
                     Resource row{};
                     if (resource_id(topology, descriptor, row.id)) {
                         row.slotIndex = descriptor.slotIndex;
@@ -595,6 +627,10 @@ bool build(const topology::Snapshot& topology,
                                                   return task_natural(left) == task_natural(right);
                                               }),
                                   pending.taskTargets.end());
+        std::sort(pending.unresourcedSlots.begin(), pending.unresourcedSlots.end());
+        pending.unresourcedSlots.erase(
+            std::unique(pending.unresourcedSlots.begin(), pending.unresourcedSlots.end()),
+            pending.unresourcedSlots.end());
         pending.complete = true;
         output = std::move(pending);
         return true;

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

@@ -107,6 +107,11 @@ struct Snapshot final {
     std::vector<format::DialogueCue> dialogueCues{};
     std::vector<format::CombatObjectiveGroup> combatObjectiveGroups{};
     std::vector<DirectiveElement> directiveElements{};
+    /**
+     * Scene slots whose package config references no resource, as topology rows, ascending
+     * and unique. The server seeds nothing for them instead of refusing the whole object.
+     */
+    std::vector<std::uint32_t> unresourcedSlots{};
     bool complete{};
 };
 

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

@@ -1,3 +1,4 @@
+#include <algorithm>
 #include <array>
 #include <cstddef>
 #include <cstdint>
@@ -207,6 +208,25 @@ bool validate(const topology::Snapshot& topology,
             return false;
         }
     }
+    for (std::size_t index = 0; index < snapshot.unresourcedSlots.size(); ++index) {
+        const std::uint32_t slotRow = snapshot.unresourcedSlots[index];
+        // A slot is unresourced or resourced, never both: one descriptor per scene slot.
+        const bool resourced =
+            std::any_of(snapshot.resources.begin(),
+                        snapshot.resources.end(),
+                        [slotRow](const Resource& row) { return row.slotIndex == slotRow; });
+        if (resourced
+            || !slot_shape(topology,
+                           schemas,
+                           slotRow,
+                           format::kAuthoredSceneSlotType,
+                           format::kAuthoredSceneComponentClass,
+                           format::kAuthoredSceneSenseSchema,
+                           format::kAuthoredSceneAuthSchema)
+            || (index != 0 && snapshot.unresourcedSlots[index - 1] >= slotRow)) {
+            return false;
+        }
+    }
     for (std::size_t index = 0; index < snapshot.squadEdges.size(); ++index) {
         const SquadEdge& row = snapshot.squadEdges[index];
         const squad::DescriptorFact* descriptor =

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

@@ -245,6 +245,7 @@ local EventKind = {
 ---@field auth_component_offset integer|nil
 ---@field auth_dynamic boolean|nil
 ---@field auth_writable boolean|nil
+---@field unresourced boolean|nil
 ---@field set_object_active fun(SunriseSlot, SunriseObjectArguments?): SunriseRequestKey
 ---@field applied fun(self: SunriseSlot, args: {channel: any}): boolean
 ---@field run_atoms fun(self: SunriseSlot, )lua"

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

@@ -223,6 +223,9 @@ bool render_mission(const Source& source,
         append_uint(output, slot.senseSchema);
         output.append(", auth_schema = ");
         append_uint(output, slot.authSchema);
+        if ((slot.flags & format::kSlotAuthoredSceneUnresourced) != 0) {
+            output.append(", unresourced = true");
+        }
         if (auth != nullptr) {
             output.append(", auth_type = ");
             append_string(output, auth->name);

+ 6 - 0
Sunrise/src/client/content/activity/activity_sdk_native_pack_internal.h

@@ -64,4 +64,10 @@ struct PackageContext final {
                                               topology_enrichment::Snapshot& enrichment,
                                               authored_scene::Snapshot& authored);
 
+/** Marks the SDK slot row of every type-43 scene whose descriptor names no resource. */
+[[nodiscard]] bool
+attach_unresourced_scenes(const topology_inventory::Snapshot& topology,
+                          const authored_scene::Snapshot& scenes,
+                          topology_enrichment::Snapshot& enrichment) noexcept;
+
 } // namespace sunrise::client::content::activity::sdk_generation::native_pack_pipeline

+ 3 - 0
Sunrise/src/client/content/activity/activity_sdk_native_pack_pipeline.cpp

@@ -371,6 +371,9 @@ Status stage(const wchar_t* sdkDirectory,
             return cancelled(cancel, cancelContext) ? Status::cancelled
                                                     : Status::authoredSceneLinks;
         }
+        if (!attach_unresourced_scenes(topology, sceneRows, topologyDetails)) {
+            return Status::authoredSceneLinks;
+        }
         report(progress, progressContext, Phase::dialogueCues);
         if (!attach_dialogue_cue_counts(
                 topology, squadFacts, packageContext, topologyDetails, sceneRows)) {

+ 22 - 0
Sunrise/src/client/content/activity/activity_sdk_native_pack_text.cpp

@@ -562,4 +562,26 @@ bool attach_dialogue_cue_counts(const topology_inventory::Snapshot& topology,
     }
 }
 
+/** Flags the slot rows of the scenes the inventory found without a resource. */
+bool attach_unresourced_scenes(const topology_inventory::Snapshot& topology,
+                               const authored_scene::Snapshot& scenes,
+                               topology_enrichment::Snapshot& enrichment) noexcept {
+    if (enrichment.slots.size() != topology.slots.size()) {
+        return false;
+    }
+    for (const std::uint32_t slotRow : scenes.unresourcedSlots) {
+        if (slotRow >= topology.slots.size()
+            || topology.slots[slotRow].slotType != format::kAuthoredSceneSlotType) {
+            return false;
+        }
+        topology_enrichment::Slot& enriched = enrichment.slots[slotRow];
+        if (enriched.componentClass != format::kAuthoredSceneComponentClass
+            || (enriched.flags & format::kSlotSchemaJoinExact) == 0) {
+            return false;
+        }
+        enriched.flags |= format::kSlotAuthoredSceneUnresourced;
+    }
+    return true;
+}
+
 } // namespace sunrise::client::content::activity::sdk_generation::native_pack_pipeline

+ 11 - 4
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 = 40;
+inline constexpr std::uint32_t kVersion = 41;
 /** The ABI contains only activity identity, topology, placement, and panel metadata. */
 inline constexpr std::uint32_t kSectionCount = 50;
 #if defined(SUNRISE_ACTIVITY_SDK_TESTING)
@@ -80,9 +80,16 @@ inline constexpr std::uint32_t kSlotReaderVerified = 0x1U;
 inline constexpr std::uint32_t kSlotSchemaJoinExact = 0x2U;
 /** A type-53 descriptor resolved an exact authored cue list and bounded cue count. */
 inline constexpr std::uint32_t kSlotDialogueCuesExact = 0x4U;
-/** Slot rows expose only reader, schema-join, and authored-dialogue facts. */
-inline constexpr std::uint32_t kSlotFlagMask =
-    kSlotReaderVerified | kSlotSchemaJoinExact | kSlotDialogueCuesExact;
+/**
+ * The scene slot's package config references no resource at all: the scene exists but was
+ * authored empty. A resource that is referenced but cannot be read is a generation error and
+ * gets no flag.
+ */
+inline constexpr std::uint32_t kSlotAuthoredSceneUnresourced = 0x8U;
+/** Slot rows expose only reader, schema-join, authored-dialogue, and unresourced-scene facts. */
+inline constexpr std::uint32_t kSlotFlagMask = kSlotReaderVerified | kSlotSchemaJoinExact
+                                               | kSlotDialogueCuesExact
+                                               | kSlotAuthoredSceneUnresourced;
 /** Exact generated slot tuples for authored sequence and cinematic actions. */
 inline constexpr std::uint32_t kObjectSlotType = 4U;
 inline constexpr std::uint32_t kObjectComponentClass = 0x80809927U;