Bladeren bron

Address review of the Strange Terrain runtime

- Append every type-34 filter predicate through a capacity check; `inside`
  could write predicates[8] once `targets` or `squad` filled the body.
- Keep squad_state alive_count and previous_alive_count integers (zero when
  population_available is false) and removal_flag a boolean, so existing
  scripts that compare them do not fault.
- Let rule-less squads take actor commands: one squad_runnable() check now
  serves placement and both actor-command gates.
- Drop the attachment previousRevision compare, which read the revision from
  the snapshot it was checked against and so could never fail. A request is
  stale when its spawn_generation no longer names the retained placement.
- Squad reports: a forward counter gap that confirms the spawn generation
  keeps the population comparison, a report naming a newer placement is never
  a replay, a consecutive root-absent delta continues the retained levels,
  created counts carry across a proven lifetime, and a cost reset publishes.
- Keep entity damage levels in their own table, stop a recycled object row
  from raising an interaction twice, fill entitySpawned.spawn_generation, and
  treat type 12 as raw 64-bit in the encoder as the decoder does.
- Remove dead and write-only additions (a no-op payload gate, unused fields,
  statuses and aliases), revert unrelated churn, and rewrite comments that
  cited addresses, run notes or content names.
Confetti3 4 weken geleden
bovenliggende
commit
8b3a88cc93
51 gewijzigde bestanden met toevoegingen van 408 en 399 verwijderingen
  1. 2 3
      Sunrise/Sunrise.vcxproj
  2. 10 2
      Sunrise/src/client/content/activity/activity_sdk_spawn_reference.h
  3. 71 60
      Sunrise/src/client/content/activity/activity_sdk_squad_inventory_link.cpp
  4. 1 1
      Sunrise/src/core/logging/log.h
  5. 5 2
      Sunrise/src/middleware/bap/activity_message/activity_scriptable_auth_fixed_body_codec.cpp
  6. 3 3
      Sunrise/src/middleware/bap/activity_message/activity_sense_update_decoder.cpp
  7. 1 1
      Sunrise/src/middleware/bap/activity_message/activity_sense_update_reader.h
  8. 5 6
      Sunrise/src/middleware/bap/activity_message/activity_sense_update_root.cpp
  9. 5 4
      Sunrise/src/middleware/bap/activity_message/sense_update.h
  10. 8 5
      Sunrise/src/middleware/bap/activity_message/squad_attachment_auth.h
  11. 4 2
      Sunrise/src/middleware/bap/activity_message/volume_toggle_auth.h
  12. 1 2
      Sunrise/src/middleware/bap/activity_message/wire_schema/activity_wire_codec_decode_runtime.h
  13. 1 1
      Sunrise/src/middleware/bap/activity_message/wire_schema/activity_wire_codec_encode_runtime.h
  14. 8 8
      Sunrise/src/middleware/gameplay/external/composite_entity_baseline_registry.cpp
  15. 2 2
      Sunrise/src/middleware/gameplay/external/composite_entity_codec.cpp
  16. 6 10
      Sunrise/src/server/activity/activity_sdk_device_runtime.cpp
  17. 1 3
      Sunrise/src/server/activity/activity_sdk_device_runtime.h
  18. 1 2
      Sunrise/src/server/activity/activity_sdk_squad_runtime.cpp
  19. 16 15
      Sunrise/src/server/activity/host_runtime.h
  20. 1 1
      Sunrise/src/server/activity/host_runtime_names.cpp
  21. 1 1
      Sunrise/src/server/activity/mission/mission_observation_epoch.h
  22. 1 2
      Sunrise/src/server/activity/mission/mission_script_actor_command_policy.cpp
  23. 49 30
      Sunrise/src/server/activity/mission/mission_script_lua_device_api.cpp
  24. 10 22
      Sunrise/src/server/activity/mission/mission_script_lua_event_derived.cpp
  25. 19 9
      Sunrise/src/server/activity/mission/mission_script_lua_slot_api.cpp
  26. 24 33
      Sunrise/src/server/activity/mission/mission_script_lua_state_api.cpp
  27. 0 3
      Sunrise/src/server/activity/mission/mission_script_object_sense.h
  28. 3 1
      Sunrise/src/server/activity/mission/mission_script_runtime.cpp
  29. 0 1
      Sunrise/src/server/activity/mission/mission_script_runtime.h
  30. 1 5
      Sunrise/src/server/activity/mission/mission_script_runtime_attach.cpp
  31. 12 12
      Sunrise/src/server/activity/mission/mission_script_runtime_combatant_damage.cpp
  32. 13 12
      Sunrise/src/server/activity/mission/mission_script_runtime_dispatch.cpp
  33. 52 29
      Sunrise/src/server/activity/mission/mission_script_runtime_edges.cpp
  34. 1 3
      Sunrise/src/server/activity/mission/mission_script_runtime_feed.cpp
  35. 13 11
      Sunrise/src/server/activity/mission/mission_script_runtime_internal.h
  36. 2 3
      Sunrise/src/server/activity/mission/mission_script_sdk_bridge.cpp
  37. 4 11
      Sunrise/src/server/activity/mission/mission_script_vm.cpp
  38. 0 3
      Sunrise/src/server/activity/mission/mission_script_vm.h
  39. 15 15
      Sunrise/src/server/activity/mission/mission_trigger_observation.h
  40. 9 6
      Sunrise/src/server/activity/squad_attachment_ownership_validation.h
  41. 0 2
      Sunrise/src/server/bap/encrypted/push/snapshot/family4_snapshot_preparer.cpp
  42. 0 2
      Sunrise/src/server/bap/encrypted/push/snapshot/snapshot.h
  43. 0 1
      Sunrise/src/server/bap/encrypted/push/snapshot/snapshot_storage.cpp
  44. 1 2
      Sunrise/src/server/bap/internal.h
  45. 2 2
      Sunrise/src/server/gameplay/actor_command_policy_lane.cpp
  46. 2 2
      Sunrise/src/server/gameplay/actor_command_policy_session.h
  47. 2 5
      Sunrise/src/state/activity/mission/activity_mission_state.cpp
  48. 1 5
      Sunrise/src/state/activity/mission/definition.h
  49. 0 23
      Sunrise/src/state/activity_sdk/attachment_compatibility.h
  50. 15 7
      Sunrise/src/state/activity_sdk/format.h
  51. 4 3
      Sunrise/src/state/activity_sdk/runtime.h

+ 2 - 3
Sunrise/Sunrise.vcxproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup Label="ProjectConfigurations">
     <ProjectConfiguration Include="Debug|x64">
@@ -249,13 +249,13 @@
     <ClCompile Include="src\client\hooks\teleport\teleport_move.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_action_key.cpp" />
     <ClCompile Include="src\client\hooks\world_objects\world_object_registry.cpp" />
-    <ClCompile Include="src\client\memory\current_process_memory.cpp" />
     <ClCompile Include="src\client\hooks\world_objects\world_object_identity_report.cpp" />
     <ClCompile Include="src\client\hooks\world_objects\world_object_purge_policy.cpp" />
     <ClCompile Include="src\client\hooking\detour.cpp" />
     <ClCompile Include="src\client\hooking\detour\protected_detour_uninstall.cpp" />
     <ClCompile Include="src\client\hooking\detour\transaction\detour_thread_transaction.cpp" />
     <ClCompile Include="src\client\executable\image.cpp" />
+    <ClCompile Include="src\client\memory\current_process_memory.cpp" />
     <ClCompile Include="src\client\hooks\package_trust\package_trust_bypass.cpp" />
     <ClCompile Include="src\client\hooks\feature_flags\feature_flags.cpp" />
     <ClCompile Include="src\client\hooks\cine_probe\cine_probe.cpp" />
@@ -2081,7 +2081,6 @@
     <ClInclude Include="src\server\activity\mission\mission_trigger_observation.h" />
     <ClInclude Include="src\server\activity\squad_attachment_ownership_validation.h" />
     <ClInclude Include="src\server\activity\squad_authored_profile.h" />
-    <ClInclude Include="src\state\activity_sdk\attachment_compatibility.h" />
     <ClInclude Include="src\middleware\bap\activity_message\music_section_auth.h" />
     <ClInclude Include="src\middleware\bap\activity_message\roster_presence.h" />
     <ClInclude Include="src\middleware\bap\activity_message\scene_events_auth.h" />

+ 10 - 2
Sunrise/src/client/content/activity/activity_sdk_spawn_reference.h

@@ -4,13 +4,17 @@
 
 namespace sunrise::client::content::activity::sdk_generation::squad_inventory {
 
+/** How one raw 64-bit spawn reference (key32, type8, padding8, index16) reads. */
 enum class SpawnReferenceState : std::uint8_t {
     invalid,
     absent,
     present,
 };
 
-/** Build86657 native unset tests are typeFF or indexFFFF; malformed is not absent. */
+/**
+ * The client treats a reference as unset when its type is 0xFF or its index is 0xFFFF. Nonzero
+ * padding or an out-of-range type is malformed, which is not the same as absent.
+ */
 [[nodiscard]] constexpr SpawnReferenceState spawn_reference_state(std::uint64_t raw) noexcept {
     const auto key = static_cast<std::uint32_t>(raw);
     const auto type = static_cast<std::uint8_t>(raw >> 32U);
@@ -26,7 +30,11 @@ enum class SpawnReferenceState : std::uint8_t {
                                                                      : SpawnReferenceState::invalid;
 }
 
-/** Source proof only: the selected rule and its occurrence still need independent validation. */
+/**
+ * A spawner needs a selected rule when both of its rule references (at offsets 0x98 and 0xA0) are
+ * absent and a complete parse found no inline point set. This proves only the source; the selected
+ * rule and its occurrence are validated separately.
+ */
 [[nodiscard]] constexpr bool requires_selected_spawn_rule(std::uint64_t reference98,
                                                           std::uint64_t referenceA0,
                                                           bool complete,

+ 71 - 60
Sunrise/src/client/content/activity/activity_sdk_squad_inventory_link.cpp

@@ -239,54 +239,63 @@ struct ConfigOccurrenceCountKeyHash final {
                       value.row.spawnRuleConfigTag);
 }
 
-// Squads whose spawner names no rule: emitted without a rule or anchors, placeable only with a
-// selected type-66 rule slot. Shares the linker's private identity and member helpers.
-
-// Match the full descriptor tuple; no schema guesses based on slot type alone.
+/**
+ * Tests that one descriptor's whole schema tuple matches its slot's proven schema. The slot type
+ * alone never implies a schema.
+ */
 [[nodiscard]] bool unbound_descriptor_exact(
     const topology::Snapshot& topology,
     const std::unordered_map<std::uint32_t, const GraphSlotSchemaProvenance*>& schemas,
-    const GraphDescriptor& d) {
-    if (!d.complete || d.slotIndex >= topology.slots.size()
-        || d.objectIndex >= topology.objects.size()) {
+    const GraphDescriptor& descriptor) {
+    if (!descriptor.complete || descriptor.slotIndex >= topology.slots.size()
+        || descriptor.objectIndex >= topology.objects.size()) {
         return false;
     }
-    const auto& slot = topology.slots[d.slotIndex];
-    if (slot.objectIndex != d.objectIndex || slot.slotType != format::kSquadSlotType) {
+    const auto& slot = topology.slots[descriptor.slotIndex];
+    if (slot.objectIndex != descriptor.objectIndex || slot.slotType != format::kSquadSlotType) {
         return false;
     }
-    const auto found = schemas.find(d.slotIndex);
+    const auto found = schemas.find(descriptor.slotIndex);
     const auto* match = found == schemas.end() ? nullptr : found->second;
-    return match != nullptr && match->exact && match->componentClass == d.componentClass
-           && match->senseSchema == d.senseSchema && match->authSchema == d.authSchema;
+    return match != nullptr && match->exact && match->componentClass == descriptor.componentClass
+           && match->senseSchema == descriptor.senseSchema
+           && match->authSchema == descriptor.authSchema;
 }
 
-// A repeated authored config path is ambiguity, even when all paths have equal coordinates.
+/**
+ * Tests that one occurrence places the descriptor's config exactly once. A config path repeated
+ * in one occurrence is ambiguous, even when every copy has the same coordinates.
+ */
 [[nodiscard]] bool unbound_occurrence_exact(const topology::Snapshot& topology,
                                             const std::vector<const GraphConfigContext*>& contexts,
-                                            const GraphDescriptor& d,
+                                            const GraphDescriptor& descriptor,
                                             std::uint32_t occurrenceIndex) {
     if (occurrenceIndex >= topology.occurrences.size()) {
         return false;
     }
-    const auto& o = topology.occurrences[occurrenceIndex];
-    if (o.objectIndex != d.objectIndex) {
+    const auto& occurrence = topology.occurrences[occurrenceIndex];
+    if (occurrence.objectIndex != descriptor.objectIndex) {
         return false;
     }
     std::uint32_t count = 0;
     bool exact = false;
-    for (const auto* cp : contexts) {
-        const auto& c = *cp;
-        if (c.configTag != d.configTag || c.occurrenceIndex != occurrenceIndex) {
+    for (const GraphConfigContext* context : contexts) {
+        if (context->configTag != descriptor.configTag
+            || context->occurrenceIndex != occurrenceIndex) {
             continue;
         }
         ++count;
-        exact = c.complete && c.objectIndex == d.objectIndex && c.scenarioIndex == o.scenarioIndex;
+        exact = context->complete && context->objectIndex == descriptor.objectIndex
+                && context->scenarioIndex == occurrence.scenarioIndex;
     }
     return count == 1 && exact;
 }
 
-// Append independently owned sources. Never construct or modify a GraphEdge.
+/**
+ * Appends one squad per exact occurrence of a spawner that names no rule of its own. Such a squad
+ * has no rule edge or anchors, so it is placeable only with a selected type-66 rule slot. The
+ * authored graph's edges are left untouched.
+ */
 [[nodiscard]] bool project_unbound_squads(const topology::Snapshot& topology,
                                           const Facts& facts,
                                           const GraphSnapshot& graph,
@@ -302,58 +311,59 @@ struct ConfigOccurrenceCountKeyHash final {
     }
     std::unordered_map<std::uint32_t, std::size_t> spawnersByConfig{};
     std::unordered_map<std::uint32_t, std::vector<const GraphConfigContext*>> contextsByConfig{};
-    for (std::size_t i = 0; i < graph.spawners.size(); ++i) {
-        if (!spawnersByConfig.emplace(graph.spawners[i].configTag, i).second) {
+    for (std::size_t index = 0; index < graph.spawners.size(); ++index) {
+        if (!spawnersByConfig.emplace(graph.spawners[index].configTag, index).second) {
             return false;
         }
     }
-    for (const auto& c : graph.configContexts) {
-        contextsByConfig[c.configTag].push_back(&c);
+    for (const auto& context : graph.configContexts) {
+        contextsByConfig[context.configTag].push_back(&context);
     }
-    for (const auto& d : graph.descriptors) {
-        const auto foundSource = spawnersByConfig.find(d.configTag);
-        if (foundSource == spawnersByConfig.end()) {
+    for (const auto& descriptor : graph.descriptors) {
+        const auto foundSpawner = spawnersByConfig.find(descriptor.configTag);
+        if (foundSpawner == spawnersByConfig.end()) {
             continue;
         }
-        const auto i = foundSource->second;
-        if (i >= facts.spawners.size()) {
+        const std::size_t spawnerIndex = foundSpawner->second;
+        if (spawnerIndex >= facts.spawners.size()) {
             return false;
         }
-        const GraphSpawner& spawner = graph.spawners[i];
-        const SpawnerFact& source = facts.spawners[i];
+        const GraphSpawner& spawner = graph.spawners[spawnerIndex];
+        const SpawnerFact& source = facts.spawners[spawnerIndex];
         if (!spawner.requiresSelectedRule) {
             continue;
         }
         if (spawner.configTag != source.configTag) {
             return false;
         }
-        if (d.componentClass != format::kSquadComponentClass
-            || d.senseSchema != format::kSquadSenseSchema
-            || d.authSchema != format::kSquadAuthSchema
-            || !unbound_descriptor_exact(topology, schemas, d)) {
+        if (descriptor.componentClass != format::kSquadComponentClass
+            || descriptor.senseSchema != format::kSquadSenseSchema
+            || descriptor.authSchema != format::kSquadAuthSchema
+            || !unbound_descriptor_exact(topology, schemas, descriptor)) {
             continue;
         }
-        const auto foundContexts = contextsByConfig.find(d.configTag);
+        const auto foundContexts = contextsByConfig.find(descriptor.configTag);
         if (foundContexts == contextsByConfig.end()) {
             continue;
         }
         std::vector<std::uint32_t> occurrenceIndexes{};
-        for (const auto* c : foundContexts->second) {
-            occurrenceIndexes.push_back(c->occurrenceIndex);
+        for (const GraphConfigContext* context : foundContexts->second) {
+            occurrenceIndexes.push_back(context->occurrenceIndex);
         }
         std::sort(occurrenceIndexes.begin(), occurrenceIndexes.end());
         occurrenceIndexes.erase(std::unique(occurrenceIndexes.begin(), occurrenceIndexes.end()),
                                 occurrenceIndexes.end());
-        for (const auto oi : occurrenceIndexes) {
-            if (!unbound_occurrence_exact(topology, foundContexts->second, d, oi)) {
+        for (const std::uint32_t occurrenceIndex : occurrenceIndexes) {
+            if (!unbound_occurrence_exact(
+                    topology, foundContexts->second, descriptor, occurrenceIndex)) {
                 continue;
             }
-            const auto& occurrence = topology.occurrences[oi];
+            const auto& occurrence = topology.occurrences[occurrenceIndex];
             std::string_view occurrenceId{};
             if (!text_view(occurrence.id, occurrenceId)) {
                 return false;
             }
-            const std::array<std::string_view, 2> parts{d.id, occurrenceId};
+            const std::array<std::string_view, 2> parts{descriptor.id, occurrenceId};
             std::string digest{};
             if (!domain_hash(hasher, "sunrise-runtime-unbound-squad-v1", parts, digest)) {
                 return false;
@@ -361,20 +371,21 @@ struct ConfigOccurrenceCountKeyHash final {
             PendingSquad squad{};
             squad.row.id = "squad/" + digest;
             squad.row.scenarioIndex = occurrence.scenarioIndex;
-            squad.row.objectIndex = d.objectIndex;
-            squad.row.slotIndex = d.slotIndex;
-            squad.row.spawnerConfigTag = d.configTag;
+            squad.row.objectIndex = descriptor.objectIndex;
+            squad.row.slotIndex = descriptor.slotIndex;
+            squad.row.spawnerConfigTag = descriptor.configTag;
             squad.row.spawnRuleConfigTag = format::kAbsentIndex;
-            squad.row.occurrenceIndex = oi;
+            squad.row.occurrenceIndex = occurrenceIndex;
             squad.row.flags = format::kSquadRequiresSelectedRule
                               | format::kSquadSourceDescriptorExact
                               | format::kSquadScenarioOccurrenceExact;
             bool sourceActorLinksComplete = true;
-            for (std::uint32_t mi = 0; mi < source.members.size(); ++mi) {
+            for (std::uint32_t memberIndex = 0; memberIndex < source.members.size();
+                 ++memberIndex) {
                 SquadMember member{};
-                if (!detail::build_member(source.members[mi],
+                if (!detail::build_member(source.members[memberIndex],
                                           digest,
-                                          mi,
+                                          memberIndex,
                                           actorResolver,
                                           actorContext,
                                           member,
@@ -383,7 +394,7 @@ struct ConfigOccurrenceCountKeyHash final {
                 }
                 squad.members.push_back(std::move(member));
             }
-            // An unresolved unbound source must not disable the authored bound squads.
+            // An unresolved rule-less squad is skipped; it must not fail the authored squads.
             if (!sourceActorLinksComplete) {
                 continue;
             }
@@ -394,22 +405,22 @@ struct ConfigOccurrenceCountKeyHash final {
             }
             if (countValid
                 && std::all_of(
-                    squad.members.begin(), squad.members.end(), [](const SquadMember& m) {
-                        return (m.flags & format::kSquadMemberInvariantReadyMask)
+                    squad.members.begin(), squad.members.end(), [](const SquadMember& member) {
+                        return (member.flags & format::kSquadMemberInvariantReadyMask)
                                    == format::kSquadMemberInvariantReadyMask
-                               && m.defaultCount > 0;
+                               && member.defaultCount > 0;
                     })) {
                 squad.row.flags |= format::kSquadCandidateCountsInvariantComplete;
             }
-            std::vector<std::uint32_t> keys{};
-            for (const auto& m : squad.members) {
-                keys.push_back(m.memberKey);
+            std::vector<std::uint32_t> memberKeys{};
+            for (const SquadMember& member : squad.members) {
+                memberKeys.push_back(member.memberKey);
             }
-            std::sort(keys.begin(), keys.end());
-            if (std::adjacent_find(keys.begin(), keys.end()) != keys.end()) {
+            std::sort(memberKeys.begin(), memberKeys.end());
+            if (std::adjacent_find(memberKeys.begin(), memberKeys.end()) != memberKeys.end()) {
                 return false;
             }
-            // Empty anchors and an absent rule are intentional; kSquadRunnableMask cannot pass.
+            // No rule edge or anchors: placement refuses this squad unless a rule slot is selected.
             pending.push_back(std::move(squad));
         }
     }

+ 1 - 1
Sunrise/src/core/logging/log.h

@@ -26,7 +26,7 @@ enum class Level : unsigned char {
     warn,
     info,
     debug,
-    /** Per-row dumps (every roster Auth on every push). Off at the shipped `debug` threshold. */
+    /** Per-row dumps, such as every roster Auth on every push. Off at the default threshold. */
     trace,
     off,
 };

+ 5 - 2
Sunrise/src/middleware/bap/activity_message/activity_scriptable_auth_fixed_body_codec.cpp

@@ -455,7 +455,10 @@ bool encode_type24(const Type24Body& body,
     return true;
 }
 
-/** Exact955A descriptor: registry32, biased signed type7/index16, biased signed state2. */
+/**
+ * Encodes one schema-0x8080955A body: registry key (32 bits), volume slot type + 1 (7), slot index
+ * + 32768 (16), then state + 1 (2), where state 1 turns the volume on and 0 turns it off.
+ */
 bool encode_type32_volume(const Type32VolumeBody& body,
                           std::span<std::byte> output,
                           std::size_t& written) noexcept {
@@ -471,7 +474,7 @@ bool encode_type32_volume(const Type32VolumeBody& body,
            && writer.finish(written) && written == kType32ByteCount;
 }
 
-/** Rejects no-op/reserved states, non-volume references and noncanonical trailing bits. */
+/** Decodes one volume toggle; the no-op state, a non-volume target and nonzero padding fail. */
 bool decode_type32_volume(std::span<const std::byte> input,
                           std::size_t bitCount,
                           Type32VolumeBody& body) noexcept {

+ 3 - 3
Sunrise/src/middleware/bap/activity_message/activity_sense_update_decoder.cpp

@@ -500,8 +500,8 @@ real_value(std::uint64_t raw, std::uint8_t width, std::uint32_t maximumBits) noe
     return NativeStatus::complete;
 }
 /**
- * The observed engagement list is empty. Nonempty custom35 elements remain unsupported until
- * their native codec is established; a storage size is not a wire width.
+ * Decodes the engagement body. Only an empty element list is supported: the element codec is
+ * unknown, and its storage size says nothing about its wire width.
  */
 [[nodiscard]] NativeStatus decode_engagement(Reader& reader, Values& values) noexcept {
     constexpr std::uint64_t capacity = 16;
@@ -522,7 +522,7 @@ real_value(std::uint64_t raw, std::uint8_t width, std::uint32_t maximumBits) noe
                : NativeStatus::malformed;
 }
 
-/** Build86657's attachment Sense has three required s32 counters and one bool. */
+/** Decodes the type-26 attachment body: three required signed 32-bit counters, then one flag. */
 [[nodiscard]] NativeStatus decode_attachment(Reader& reader, Values& values) noexcept {
     for (std::uint16_t ordinal = 0; ordinal < 3; ++ordinal) {
         if (!read_signed(reader,

+ 1 - 1
Sunrise/src/middleware/bap/activity_message/activity_sense_update_reader.h

@@ -56,7 +56,7 @@ private:
 }
 
 /**
- * Consumes the present build86657 80809445 root before the registry group stream.
+ * Skips the packet's root Sense body (schema 0x80809445), which precedes the registry groups.
  * @return False when the root's framing does not fit the reader's budget.
  */
 [[nodiscard]] bool consume_root_sense(Reader& reader) noexcept;

+ 5 - 6
Sunrise/src/middleware/bap/activity_message/activity_sense_update_root.cpp

@@ -14,8 +14,8 @@ namespace {
 }
 
 /**
- * Build86657 schemas 8080944C/80809452: optional word list, fixed words, and byte list.
- * Only their framing is consumed; these root channels do not become squad observations.
+ * Skips one root channel block (schemas 0x8080944C and 0x80809452): an optional word list, fixed
+ * words, and a byte list. None of it becomes a mission observation.
  */
 [[nodiscard]] bool consume_root_channels(Reader& reader,
                                          std::uint8_t countWidth,
@@ -36,9 +36,8 @@ namespace {
 } // namespace
 
 /**
- * Consumes the present build86657 80809445 root before the registry group stream.
- * Its global channels hold 256 entries; up to 64 keyed rows each hold 96 local entries.
- * Counts and nested presence come from the exact native reflection descriptors.
+ * Skips the root Sense body. Its global channels hold up to 256 entries, and up to 64 keyed rows
+ * hold 96 entries each; the counts and presence bits follow the client's schema descriptors.
  */
 bool consume_root_sense(Reader& reader) noexcept {
     constexpr std::uint32_t globalCapacity = 256, keyedCapacity = 64, localCapacity = 96;
@@ -57,7 +56,7 @@ bool consume_root_sense(Reader& reader) noexcept {
     if (!reader.read(7, count) || count > keyedCapacity) {
         return false;
     }
-    for (std::uint64_t i = 0; i < count; ++i) {
+    for (std::uint64_t row = 0; row < count; ++row) {
         if (!present(reader, true, exists) || (exists && !reader.skip(32))) {
             return false;
         }

+ 5 - 4
Sunrise/src/middleware/bap/activity_message/sense_update.h

@@ -21,12 +21,13 @@ inline constexpr std::uint8_t kLiteralZeroWidth = 1;
 inline constexpr std::size_t kOuterByteCapacity = 514'048;
 /** The client's per-group scratch bounds one group substream. This capacity is also in bytes. */
 inline constexpr std::size_t kGroupByteCapacity = 102'400;
-/** Owned object rows per packet, shared by diagnostics and accepted mission input.
- * Build-86657 region transitions exceed 128 rows. Overflow remains explicit and
- * incomplete values are never delivered; this is a storage budget, not a wire limit.
+/**
+ * Object rows retained per packet, shared by diagnostics and mission input. A region transition
+ * can report more than 128 objects. This is a storage budget, not a wire limit: overflow is
+ * reported and an incomplete object is never delivered.
  */
 inline constexpr std::size_t kDecodedObjectCapacity = 256;
-/** Owned scalar rows, including absent fields, in the bounded packet storage budget. */
+/** Scalar rows retained per packet, absent optional fields included. */
 inline constexpr std::size_t kDecodedValueCapacity = 4096;
 /** Runtime SDK row sentinel shared without depending on the State layer. */
 inline constexpr std::uint32_t kAbsentRuntimeRow = (std::numeric_limits<std::uint32_t>::max)();

+ 8 - 5
Sunrise/src/middleware/bap/activity_message/squad_attachment_auth.h

@@ -8,14 +8,18 @@
 
 namespace sunrise::middleware::bap::activity_message::scriptable_auth {
 
-/** Type-26 target selection with the two arena shield resources' neutral authored prefix. */
+/**
+ * Type-26 attachment Auth: the authored prefix left neutral, then an optional squad selection. The
+ * slot type and schema are the mission-effect ones; only the selection tail differs.
+ */
+inline constexpr std::uint8_t kType26SlotType = mission_effect::kSlotType;
 inline constexpr std::uint32_t kType26Schema = mission_effect::kSchema;
 inline constexpr std::uint32_t kType26SquadSelectionSchema = 0x80809157U;
 inline constexpr std::size_t kType26EmptyBitCount = mission_effect::kBits;
 inline constexpr std::size_t kType26SquadBitCount = 273;
 inline constexpr std::size_t kType26MaximumByteCount = (kType26SquadBitCount + 7U) / 8U;
 
-/** No raw actor handles: the native selector enumerates this exact squad ClientRef. */
+/** Selects a squad by its ClientRef; the client resolves its members, never a raw actor handle. */
 struct Type26SquadSelection final {
     std::uint32_t registryKey{0x811C9DC5U};
     std::int16_t slotIndex{-1};
@@ -23,9 +27,8 @@ struct Type26SquadSelection final {
 };
 
 /**
- * Encodes the neutral-prefix source selector or its exact empty removal form.
- * Callers must establish the authored prefix and retain ownership of the attachment source.
- * This body changes target attachment; it does not assert damage immunity.
+ * Encodes the squad selection, or the empty body that clears it. The caller owns the attachment
+ * source; the body only moves the attachment and does not make the squad immune to damage.
  */
 [[nodiscard]] bool encode_type26_squad_selection(const Type26SquadSelection& selection,
                                                  std::span<std::byte> output,

+ 4 - 2
Sunrise/src/middleware/bap/activity_message/volume_toggle_auth.h

@@ -6,22 +6,24 @@
 
 namespace sunrise::middleware::bap::activity_message::scriptable_auth {
 
-/** Build-86657 toggle sensor: an EntryRef followed by signed state {-1,0,1}. */
+/** Type-32 toggle Auth: a volume reference followed by a signed state in {-1, 0, 1}. */
 inline constexpr std::uint8_t kType32SlotType = 32;
 inline constexpr std::uint32_t kType32ComponentClass = 0x80809556;
 inline constexpr std::uint32_t kType32Schema = 0x8080955A;
 inline constexpr std::uint8_t kVolumeSlotType = 60;
 inline constexpr std::size_t kType32BitCount = 57;
 inline constexpr std::size_t kType32ByteCount = 8;
-/** This adapter intentionally supports only a real volume target and an explicit boolean state. */
+/** One volume and an explicit on/off state; the no-op state -1 is not representable. */
 struct Type32VolumeBody final {
     std::uint32_t registryKey{};
     std::int16_t slotIndex{-1};
     bool active{};
 };
+/** Encodes one volume toggle into exactly kType32ByteCount bytes. */
 [[nodiscard]] bool encode_type32_volume(const Type32VolumeBody& body,
                                         std::span<std::byte> output,
                                         std::size_t& written) noexcept;
+/** Decodes one exact volume toggle body. */
 [[nodiscard]] bool decode_type32_volume(std::span<const std::byte> input,
                                         std::size_t bitCount,
                                         Type32VolumeBody& body) noexcept;

+ 1 - 2
Sunrise/src/middleware/bap/activity_message/wire_schema/activity_wire_codec_decode_runtime.h

@@ -828,8 +828,7 @@ private:
         const bool raw64 = is_raw64(field.typeCode);
         const std::uint8_t nativeWidth = raw64 ? 64 : storage_width(field.typeCode);
         const std::uint8_t width = raw64 ? 64 : runtime_declared_width(field);
-        // Native scalar readers read param[1] bits; a zero width (a union field's own params) reads
-        // none.
+        // A declared width of zero, as a union field's own parameters give, reads no bits.
         if (nativeWidth == 0) {
             return RuntimeWalkStatus::unsupportedField;
         }

+ 1 - 1
Sunrise/src/middleware/bap/activity_message/wire_schema/activity_wire_codec_encode_runtime.h

@@ -851,7 +851,7 @@ private:
         const bool nestedSchemaKind =
             nested || command || entityReference || schemaReference || custom;
         const bool unknownOptional = field.presence != 0 && !nestedSchemaKind && !selected
-                                     && !zeroBit && field.typeCode != 35
+                                     && !zeroBit && !is_raw64(field.typeCode)
                                      && storage_width(field.typeCode) == 0;
         const bool structural = nestedSchemaKind || selected || zeroBit || unknownOptional;
         if (!nestedSchemaKind && field.nestedSchemaRow != runtime::kAbsentRuntimeRow) {

+ 8 - 8
Sunrise/src/middleware/gameplay/external/composite_entity_baseline_registry.cpp

@@ -371,9 +371,9 @@ bool stage_entity_baseline_mutation(const CompositeEntityCodecContext& context,
         }
         return context.registry->slots[slot];
     };
-    // The native anchor pass (0x141713250) runs before its remove pass: an entity that receives a
-    // record detaches to root every descendant that received none in the same batch. A parent sent
-    // as a direct element therefore loses its children, and they outlive its removal as roots.
+    // The client's anchor pass runs before its remove pass: an entity that receives a record
+    // detaches to root every descendant that received none in the same batch. A parent sent as a
+    // direct element therefore loses its children, and they outlive its removal as roots.
     std::bitset<kMaximumEntitySlot + 1U> recorded{};
     for (std::size_t index = 0; index < count; ++index) {
         if (!candidate.ignoredRecordMask.test(index)) {
@@ -385,15 +385,15 @@ bool stage_entity_baseline_mutation(const CompositeEntityCodecContext& context,
         if (recorded.test(slot) || !retained[slot].occupied || !retained[slot].anchorPresent) {
             continue;
         }
-        // The native walk detaches a stale node and does not descend into it, so a stale node's own
-        // subtree leaves with it: only the direct parent's record matters.
+        // The walk detaches such a child without descending into it, so the child's own subtree
+        // moves with it: only the direct parent's record matters.
         const EntityToken parent = retained[slot].anchor;
-        bool stale = false;
+        bool detach = false;
         if (recorded.test(parent.slot)) {
             const auto& staged = current_slot(parent.slot);
-            stale = staged.occupied && staged.incarnation == parent.incarnation;
+            detach = staged.occupied && staged.incarnation == parent.incarnation;
         }
-        if (!stale) {
+        if (!detach) {
             continue;
         }
         if (changes == kEntityBatchCapacity) {

+ 2 - 2
Sunrise/src/middleware/gameplay/external/composite_entity_codec.cpp

@@ -106,8 +106,8 @@ read_schema(const void* raw, std::uint32_t rowIndex, wire::runtime::SchemaView&
                                       format::RuntimeCodecFamily family,
                                       wire::runtime::FieldView& output) noexcept {
     output.typeCode = reference_alias(family, output.typeCode);
-    // The family-1/4 NESTED_CUSTOM reader (0x1409F7400) is a plain nested walk of one fixed schema,
-    // the one its global at 0x141FA32E0 names on 86657: a 19-bit real and a 5-bit int8, 24 bits.
+    // In mode one, a type-37 custom field is a plain nested walk of one fixed schema: a 19-bit real
+    // and a 5-bit int8, 24 bits in all.
     constexpr std::uint32_t kNestedCustomSchema = 0x808092FFU;
     if (family == format::RuntimeCodecFamily::sobjectModeOne && output.typeCode == 37) {
         wire::runtime::SchemaView nested{};

+ 6 - 10
Sunrise/src/server/activity/activity_sdk_device_runtime.cpp

@@ -184,7 +184,10 @@ Status apply_auth_reserved(const sdk::BoundView& view,
     return Status::refused;
 }
 
-/** Selects a same-owner squad through the audited build-86657 attachment Auth schema. */
+/**
+ * Selects or clears the squad a type-26 attachment follows. Both slots must belong to one authored
+ * object in the same held state, and the source must still be the placement the script named.
+ */
 Status
 set_squad_attachment_reserved(const sdk::BoundView& view,
                               std::uint32_t slotRow,
@@ -213,8 +216,8 @@ set_squad_attachment_reserved(const sdk::BoundView& view,
     }
     std::copy(digest.begin(), digest.end(), owned.sdkBuildSha256.begin());
     const auto payload = view.catalog->payload_sha256();
-    if (!state::activity_sdk::attachment_compatibility::supports(payload)) {
-        return Status::unsupportedAttachmentPayload;
+    if (payload.size() != owned.sdkPayloadSha256.size()) {
+        return Status::wrongSdkBuild;
     }
     std::copy(payload.begin(), payload.end(), owned.sdkPayloadSha256.begin());
     owned.source = source.target;
@@ -227,11 +230,6 @@ set_squad_attachment_reserved(const sdk::BoundView& view,
     if (!host::scriptable_auth_estate(view.binding, target.activityClientGeneration, estate)) {
         return Status::staleBinding;
     }
-    for (const auto& previous : estate) {
-        if (host::attachments::same_ref(previous.target, target.target)) {
-            owned.previousRevision = previous.revision;
-        }
-    }
     const auth::Type26SquadSelection selection =
         active ? auth::Type26SquadSelection{source.target.registryKey,
                                             static_cast<std::int16_t>(source.target.slotIndex),
@@ -673,8 +671,6 @@ const char* status_name(Status status) noexcept {
         return "refused";
     case Status::refusedSlotType:
         return "refused_slot_type";
-    case Status::unsupportedAttachmentPayload:
-        return "unsupported_attachment_payload";
     }
     return "unknown";
 }

+ 1 - 3
Sunrise/src/server/activity/activity_sdk_device_runtime.h

@@ -37,8 +37,6 @@ enum class Status : std::uint8_t {
     refused,
     /** The slot type faults the stock client from wire-legal values, so no body reaches it. */
     refusedSlotType,
-    /** Loaded SDK data has not been audited for owned squad attachments. */
-    unsupportedAttachmentPayload,
 };
 
 /**
@@ -131,7 +129,7 @@ play_combatant_sequence_reserved(const state::activity_sdk::BoundView& view,
     const host::ScriptableOutputReservation& reservation,
     host::ScriptableOverrideKind kind = host::ScriptableOverrideKind::sdkAuth) noexcept;
 
-/** Selects or clears an audited attachment using one delivered squad spawn generation. */
+/** Selects or clears the squad a type-26 attachment follows, for one placement spawn generation. */
 [[nodiscard]] Status
 set_squad_attachment_reserved(const state::activity_sdk::BoundView& view,
                               std::uint32_t slotRow,

+ 1 - 2
Sunrise/src/server/activity/activity_sdk_squad_runtime.cpp

@@ -301,8 +301,7 @@ struct PreparedSquad final {
     if (squad.scenarioIndex != view.scenarioRow) {
         return Status::wrongScenario;
     }
-    const auto requiredMask = format::squad_required_mask(squad.flags);
-    if ((squad.flags & requiredMask) != requiredMask) {
+    if (!format::squad_runnable(squad.flags)) {
         return Status::notRunnable;
     }
     const Status members = member_status(catalog, squad, squadRow, requestedCounts);

+ 16 - 15
Sunrise/src/server/activity/host_runtime.h

@@ -4,7 +4,6 @@
 #include <cstdint>
 #include <optional>
 #include <span>
-#include <type_traits>
 #include <vector>
 
 #include "../../middleware/bap/activity_message/cinematic_incident.h"
@@ -529,7 +528,9 @@ struct Event final {
     std::int32_t triggerValue{};
     /** True when the whole watched set is inside the volume. */
     bool triggerAll{};
+    /** For triggerState events: whether a level was reported, and the volume's occupancy. */
     bool triggerAvailable{}, triggerOccupied{};
+    /** trigger_observation::Continuity of the report, for triggerState events. */
     std::uint8_t triggerContinuity{};
     /** Health and shield fractions, for damageState events. Negative until published. */
     float damageHealth{-1.0F};
@@ -574,11 +575,12 @@ struct Event final {
     bool squadObjectiveCostQualified{};
     /** Per-slot member counts the client published, for squadState events. */
     std::array<std::int32_t, kSquadSlotCapacity> squadSlotCounts{};
-    /** Native Sense .0 echo of the squad Auth .6 spawn generation, not the wire counter. */
+    /** Spawn generation the squad Sense echoes from its placement Auth; not the record counter. */
     std::int32_t squadSpawnGeneration{};
     bool squadHasSpawnGeneration{};
+    /** The alive counts were reported; otherwise they are zero placeholders. */
     bool squadPopulationAvailable{};
-    /** A backwards report counter requires retiring any existing objective job. */
+    /** The report counter went backwards or the spawn generation changed: a new registration. */
     bool squadRegistrationReset{};
     /** Alive members the client published. Six bits on the wire, so 0 through 63. */
     std::int32_t squadAliveCount{};
@@ -594,12 +596,12 @@ struct Event final {
     std::uint8_t squadSlotOrdinal{};
     /** Client flag written on its actor death or removal path. The meaning is unproved. */
     bool squadRemovalFlag{};
-    /** The raw flag is known for this exact observation lifetime; absence is not false. */
+    /** The removal flag was reported for this spawn lifetime. */
     bool squadHasRemovalFlag{};
-    /** Generic msg-6 record counter plus one; not a squad/actor lifetime identity. */
+    /** Msg-6 record counter plus one; not a squad or actor lifetime identity. */
     std::uint32_t senseGenerationPlusOne{};
     bool hasSenseGeneration{};
-    /** First/replacement observation reconciles current state instead of proving a spawn/death. */
+    /** A baseline report: it restates current levels and never proves a death. */
     bool initialObservation{};
     /** Authored entry index and spawn-mask words, for objectState events. */
     std::int32_t objectEntryIndex{};
@@ -873,15 +875,15 @@ struct PendingIncident final {
     std::uint64_t revision{};
 };
 
-/** Owned source selection for a neutral-prefix type-26 attachment, never a raw actor handle. */
+/** The squad a type-26 attachment follows, and the SDK and placement lifetime it was chosen in. */
 struct SquadAttachmentOwnership final {
     std::array<std::byte, 32> sdkBuildSha256{};
-    /** Authenticated catalog data, independent of the source-derived SDK identity. */
+    /** Catalog payload digest; an update must match the one the retained attachment used. */
     std::array<std::byte, 32> sdkPayloadSha256{};
     ScriptableTarget source{};
+    /** Placement spawn generation of the source squad. */
     std::uint64_t sourceSpawnGeneration{};
-    /** Compare-and-swap revision of the last delivered attachment body, zero for a fresh claim. */
-    std::uint64_t previousRevision{};
+    /** True to select the source, false to clear the selection. */
     bool active{};
     bool operator==(const SquadAttachmentOwnership&) const = default;
 };
@@ -916,9 +918,9 @@ struct PendingScriptableOverride final {
     /** Activity lifetime state for a lifetime request; ignored by every other kind. */
     std::uint8_t lifetimeState{kDefaultLifetimeState};
     bool sdkCompiled{};
+    /** Present only for a type-26 attachment written through set_squad_attachment. */
     std::optional<SquadAttachmentOwnership> squadAttachment{};
-    /** Spawn generation of the placement a squadObjective row replaced on this ClientRef; zero
-     * otherwise. */
+    /** For a squadObjective row: spawn generation of the placement it replaced. Zero otherwise. */
     std::uint64_t squadSpawnGeneration{};
 };
 
@@ -977,9 +979,8 @@ enum class IngressRefusal : std::uint8_t {
 };
 
 /**
- * Reports which ingress gate declined, for the skip diagnostic.
- * Every name keeps the `host_ingress_refused` prefix the earlier single reason used, so a search
- * for the collapsed name still finds all of them.
+ * Names the ingress gate that declined, for the skip diagnostic. Every name starts with
+ * `host_ingress_refused`.
  * @param refusal Gate reported by one failed submission.
  * @return Stable reason name.
  */

+ 1 - 1
Sunrise/src/server/activity/host_runtime_names.cpp

@@ -60,7 +60,7 @@ const char* event_name(EventKind kind) noexcept {
     case EventKind::phaseEntered:
         return "mission phase entered";
     case EventKind::triggerState:
-        return "triggerState";
+        return "trigger volume state";
     case EventKind::triggerEntered:
         return "trigger volume entered";
     case EventKind::triggerExited:

+ 1 - 1
Sunrise/src/server/activity/mission/mission_observation_epoch.h

@@ -18,7 +18,7 @@ enum class Transition : std::uint8_t { discard, baseline, continuous };
 /**
  * Counts from different reported object generations cannot form a gameplay edge. Missing
  * generations also require reconciliation, following Host's same_sense_scalar_generations rule.
- * The caller publishes the baseline as state, without inventing a spawn or death.
+ * The caller publishes a baseline as state and never derives a death from it.
  */
 [[nodiscard]] inline Transition accept(Cursor& retained,
                                        std::uint64_t expectedSourceGeneration,

+ 1 - 2
Sunrise/src/server/activity/mission/mission_script_actor_command_policy.cpp

@@ -31,8 +31,7 @@ dispatch_actor_command(const RuntimeInstance& instance, const lua_vm::Intent& in
     }
     const auto squads = published->squads();
     if (intent.firstRow >= squads.size()
-        || (squads[intent.firstRow].flags & format::kSquadRunnableMask)
-               != format::kSquadRunnableMask) {
+        || !format::squad_runnable(squads[intent.firstRow].flags)) {
         return ActorCommandPolicyStatus::refused;
     }
     const auto commands = published->actor_command_definitions();

+ 49 - 30
Sunrise/src/server/activity/mission/mission_script_lua_device_api.cpp

@@ -116,12 +116,22 @@ constexpr std::int32_t kUncommittedCounter = 1;
 constexpr std::size_t kOccupancyAuthBitCount = 87;
 constexpr std::size_t kOccupancyAuthByteCount = 11;
 
-/** Volumes one filter may test; leaves room for the players, target and inside predicates. */
+/** Volumes one filter may test. Every predicate still shares the type-34 capacity. */
 constexpr std::size_t kMaximumFilterVolumes = 5;
 /** Type-34 predicate modes: 0 tests the flag or reference as given, 1 tests inside a volume. */
 constexpr std::int8_t kFilterModeDirect = 0;
 constexpr std::int8_t kFilterModeInside = 1;
 
+/** Appends one predicate. @return False when the type-34 body is already full. */
+[[nodiscard]] bool append_predicate(scriptable_auth::Type34Body& body,
+                                    const scriptable_auth::Type34Predicate& predicate) noexcept {
+    if (body.count >= scriptable_auth::kType34PredicateCapacity) {
+        return false;
+    }
+    body.predicates[body.count++] = predicate;
+    return true;
+}
+
 } // namespace
 
 /**
@@ -191,7 +201,10 @@ constexpr std::int8_t kFilterModeInside = 1;
     return queue_slot_auth(state, slot, darkness::kSchema, darkness::kBits, body);
 }
 
-/** Native typed object filters: players, one object, and volume intersection. */
+/**
+ * Native typed object filters: players, one or several objects, a squad, and volume intersection.
+ * Every predicate shares the type-34 capacity; a filter that would exceed it is refused.
+ */
 [[nodiscard]] int slot_set_object_filter(lua_State* state) {
     namespace auth = scriptable_auth;
     const auto* const handle =
@@ -205,6 +218,7 @@ constexpr std::int8_t kFilterModeInside = 1;
         || slot.authSchema != auth::kType34Schema) {
         return luaL_error(state, "object filter requires an authored type-34 sensor");
     }
+    constexpr const char* kCapacityError = "object filter exceeds the type-34 predicate capacity";
     auth::Type34Body body{};
     lua_getfield(state, 2, "inside_any");
     const bool volumes = !lua_isnil(state, -1);
@@ -223,34 +237,38 @@ constexpr std::int8_t kFilterModeInside = 1;
                 || volume.slotType != auth::kType60SlotType) {
                 return luaL_error(state, "inside_any requires authored type-60 volumes");
             }
-            body.predicates[body.count++] =
-                auth::Type34ModeFlagSlotRef{kFilterModeDirect,
-                                            true,
-                                            {volume.registryKey,
-                                             static_cast<std::int8_t>(auth::kType60SlotType),
-                                             static_cast<std::int16_t>(volume.slotIndex)}};
+            if (!append_predicate(
+                    body,
+                    auth::Type34ModeFlagSlotRef{kFilterModeDirect,
+                                                true,
+                                                {volume.registryKey,
+                                                 static_cast<std::int8_t>(auth::kType60SlotType),
+                                                 static_cast<std::int16_t>(volume.slotIndex)}})) {
+                return luaL_error(state, kCapacityError);
+            }
             lua_pop(state, 1);
         }
     }
     lua_pop(state, 1);
-    if (optional_boolean_argument(state, "players", false)) {
-        body.predicates[body.count++] =
-            auth::Type34ModeOnlyB{static_cast<std::int8_t>(volumes ? 1 : 0)};
+    if (optional_boolean_argument(state, "players", false)
+        && !append_predicate(body, auth::Type34ModeOnlyB{static_cast<std::int8_t>(volumes)})) {
+        return luaL_error(state, kCapacityError);
     }
     auth::Type2LaneClientRef target{};
     if (!optional_slot_reference(state, "target", auth::kType4SlotType, target)) {
         return luaL_error(state, "filter target must be an authored type-4 object");
     }
-    if (target.slotIndex >= 0) {
-        body.predicates[body.count++] = auth::Type34ModeSlotRefC{kFilterModeDirect, target};
+    if (target.slotIndex >= 0
+        && !append_predicate(body, auth::Type34ModeSlotRefC{kFilterModeDirect, target})) {
+        return luaL_error(state, kCapacityError);
     }
     // Several authored objects at once (a beam hop-on names every crystal it tethers).
     lua_getfield(state, 2, "targets");
     if (!lua_isnil(state, -1)) {
         luaL_checktype(state, -1, LUA_TTABLE);
         const std::size_t count = lua_rawlen(state, -1);
-        if (count == 0 || count + body.count > auth::kType34PredicateCapacity) {
-            return luaL_error(state, "targets count is outside the filter capacity");
+        if (count == 0) {
+            return luaL_error(state, "targets must name at least one object");
         }
         for (std::size_t index = 1; index <= count; ++index) {
             lua_rawgeti(state, -1, static_cast<lua_Integer>(index));
@@ -262,33 +280,34 @@ constexpr std::int8_t kFilterModeInside = 1;
                 || object.slotIndex > auth_fields::kMaximumClientRefIndex) {
                 return luaL_error(state, "targets require authored type-4 objects");
             }
-            body.predicates[body.count++] =
-                auth::Type34ModeSlotRefC{kFilterModeDirect,
-                                         {object.registryKey,
-                                          static_cast<std::int8_t>(auth::kType4SlotType),
-                                          static_cast<std::int16_t>(object.slotIndex)}};
+            if (!append_predicate(
+                    body,
+                    auth::Type34ModeSlotRefC{kFilterModeDirect,
+                                             {object.registryKey,
+                                              static_cast<std::int8_t>(auth::kType4SlotType),
+                                              static_cast<std::int16_t>(object.slotIndex)}})) {
+                return luaL_error(state, kCapacityError);
+            }
             lua_pop(state, 1);
         }
     }
     lua_pop(state, 1);
-    // A squad reference selects the boss a shield/tether hop-on hosts on.
+    // A squad reference selects the boss a shield or tether hop-on hosts on.
     auth::Type2LaneClientRef squad{};
-    if (!optional_slot_reference(state, "squad", 1U, squad)) {
+    if (!optional_slot_reference(state, "squad", format::kSquadSlotType, squad)) {
         return luaL_error(state, "filter squad must be an authored type-1 squad");
     }
-    if (squad.slotIndex >= 0) {
-        if (body.count >= auth::kType34PredicateCapacity) {
-            return luaL_error(state, "squad does not fit the filter capacity");
-        }
-        body.predicates[body.count++] = auth::Type34ModeSlotRefC{kFilterModeDirect, squad};
+    if (squad.slotIndex >= 0
+        && !append_predicate(body, auth::Type34ModeSlotRefC{kFilterModeDirect, squad})) {
+        return luaL_error(state, kCapacityError);
     }
     auth::Type2LaneClientRef inside{};
     if (!optional_slot_reference(state, "inside", auth::kType60SlotType, inside)) {
         return luaL_error(state, "filter inside must be an authored type-60 volume");
     }
-    if (inside.slotIndex >= 0) {
-        body.predicates[body.count++] =
-            auth::Type34ModeFlagSlotRef{kFilterModeInside, false, inside};
+    if (inside.slotIndex >= 0
+        && !append_predicate(body, auth::Type34ModeFlagSlotRef{kFilterModeInside, false, inside})) {
+        return luaL_error(state, kCapacityError);
     }
     std::array<std::byte, auth::kType34MaximumByteCount> bytes{};
     std::size_t written = 0;

+ 10 - 22
Sunrise/src/server/activity/mission/mission_script_lua_event_derived.cpp

@@ -97,7 +97,11 @@ push_trigger_member(lua_State* state, const host::Event& event, std::string_view
     return true;
 }
 
-/** Squad totals and native objective queries share the exact reported slot identity. */
+/**
+ * Squad totals and native objective queries share the exact reported slot identity. The counts
+ * stay integers so existing scripts can compare them; population_available says whether they are
+ * a reported population or zero placeholders for a report that carried none.
+ */
 [[nodiscard]] bool
 push_squad_state_member(lua_State* state, const host::Event& event, std::string_view key) {
     if (key == "task_cost") {
@@ -111,11 +115,7 @@ push_squad_state_member(lua_State* state, const host::Event& event, std::string_
     } else if (key == "population_available") {
         lua_pushboolean(state, event.squadPopulationAvailable);
     } else if (key == "alive_count") {
-        if (event.squadPopulationAvailable) {
-            lua_pushinteger(state, event.squadAliveCount);
-        } else {
-            lua_pushnil(state);
-        }
+        lua_pushinteger(state, event.squadPopulationAvailable ? event.squadAliveCount : 0);
     } else if (key == "objective_revision") {
         // Script-side assignment revision: one while a script assignment is retained, else zero.
         lua_pushinteger(state, event.squadObjectiveRegistryKey != 0 ? 1 : 0);
@@ -129,17 +129,9 @@ push_squad_state_member(lua_State* state, const host::Event& event, std::string_
             lua_rawseti(state, -2, static_cast<lua_Integer>(group) + 1);
         }
     } else if (key == "previous_alive_count") {
-        if (event.squadPopulationAvailable) {
-            lua_pushinteger(state, event.squadPreviousAliveCount);
-        } else {
-            lua_pushnil(state);
-        }
+        lua_pushinteger(state, event.squadPopulationAvailable ? event.squadPreviousAliveCount : 0);
     } else if (key == "removal_flag") {
-        if (event.squadHasRemovalFlag) {
-            lua_pushboolean(state, event.squadRemovalFlag);
-        } else {
-            lua_pushnil(state);
-        }
+        lua_pushboolean(state, event.squadHasRemovalFlag && event.squadRemovalFlag);
     } else if (key == "slot_counts") {
         const std::size_t count = event.squadSlotCountLength;
         lua_createtable(state, static_cast<int>(count), 0);
@@ -168,15 +160,11 @@ push_entity_spawned_member(lua_State* state, const host::Event& event, std::stri
     return true;
 }
 
-/** The alive count after the death, and the count it replaced. */
+/** The alive count after the death, the count it replaced, and the squad's removal flag. */
 [[nodiscard]] bool
 push_entity_died_member(lua_State* state, const host::Event& event, std::string_view key) {
     if (key == "removal_flag") {
-        if (event.squadHasRemovalFlag) {
-            lua_pushboolean(state, event.squadRemovalFlag);
-        } else {
-            lua_pushnil(state);
-        }
+        lua_pushboolean(state, event.squadHasRemovalFlag && event.squadRemovalFlag);
     } else if (key == "alive_count") {
         lua_pushinteger(state, event.squadAliveCount);
     } else if (key == "previous_alive_count") {

+ 19 - 9
Sunrise/src/server/activity/mission/mission_script_lua_slot_api.cpp

@@ -103,11 +103,13 @@ namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
     return value;
 }
 
-/** Sends a native type-32 toggle for one current same-owner authored volume. */
+/** Turns one authored type-60 volume of the same object on or off through its type-32 toggle. */
 [[nodiscard]] int slot_set_volume_active(lua_State* state) {
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 2> declared{"volume", "active"};
-    refuse_unknown_arguments(state, declared);
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    // Only these named arguments belong to this API.
+    static constexpr std::array<std::string_view, 2> kDeclared{"volume", "active"};
+    refuse_unknown_arguments(state, kDeclared);
     const SlotHandle target = checked_argument<SlotHandle>(state, "volume", kSlotMetatable);
     lua_getfield(state, 2, "active");
     if (!lua_isboolean(state, -1)) {
@@ -143,12 +145,17 @@ namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
         state, source, scriptable_auth::kType32Schema, scriptable_auth::kType32BitCount, body);
 }
 
-/** Selects an audited same-owner squad lifetime for a type-26 attachment. */
+/**
+ * Selects or clears the squad a type-26 attachment follows. spawn_generation names the source
+ * placement; a request for any other placement is refused as stale.
+ */
 [[nodiscard]] int slot_set_squad_attachment(lua_State* state) {
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 3> declared{
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    // Only these named arguments belong to this API.
+    static constexpr std::array<std::string_view, 3> kDeclared{
         "source", "spawn_generation", "active"};
-    refuse_unknown_arguments(state, declared);
+    refuse_unknown_arguments(state, kDeclared);
     const SlotHandle sourceHandle = checked_argument<SlotHandle>(state, "source", kSlotMetatable);
     const lua_Integer generation = checked_integer_argument(state, "spawn_generation");
     lua_getfield(state, 2, "active");
@@ -161,7 +168,8 @@ namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
     if (!current_slot(state, *handle, target) || !current_slot(state, sourceHandle, source)) {
         return luaL_error(state, "attachment or squad source is stale or invalid");
     }
-    if (target.slotType != 26 || target.authSchema != scriptable_auth::kType26Schema
+    if (target.slotType != scriptable_auth::kType26SlotType
+        || target.authSchema != scriptable_auth::kType26Schema
         || (target.flags & format::kSlotSchemaJoinExact) == 0
         || source.slotType != format::kSquadSlotType
         || source.authSchema != format::kSquadAuthSchema
@@ -187,6 +195,7 @@ namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
     namespace effect = middleware::bap::activity_message::mission_effect;
     const auto* const handle =
         static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    // Only these named arguments belong to this API.
     static constexpr std::array<std::string_view, 3> kDeclared{"filter", "enabled", "revision"};
     refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
@@ -217,6 +226,7 @@ namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
     }
     return queue_slot_auth(state, slot, effect::kSchema, effect::kBits, body);
 }
+
 /** Reads one Slot row member, its syntax methods, and its authorized actions. */
 [[nodiscard]] int slot_index(lua_State* state) {
     const auto* const handle =

+ 24 - 33
Sunrise/src/server/activity/mission/mission_script_lua_state_api.cpp

@@ -163,50 +163,41 @@ void push_variable_value(lua_State* state, const VariableValue& value) {
     frame.candidate.phaseChanged = true;
     return 0;
 }
-/**
- * Emits one script variable write. Controllers park their gate state in named variables
- * (`later.<name>.status`, `later.<name>.reason`, `route.invalidated`, ...); without this row a
- * stalled encounter can only be described as "no further requests", never explained.
- */
+/** Logs one staged variable write, so the gate state a controller keeps in variables is visible. */
 void log_variable_write(const StateKey& key, const VariableValue& value) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const std::string_view name = state_key_view(key);
-    int written = 0;
+    std::array<char, 32> scalar{};
+    std::string_view text{};
+    int scalarWritten = 0;
     switch (value.kind) {
     case VariableValueKind::boolean:
-        written = std::snprintf(line.data(),
-                                line.size(),
-                                "ev=mission_script stage=variable result=set name=%.*s value=%s",
-                                static_cast<int>(name.size()),
-                                name.data(),
-                                value.booleanValue ? "true" : "false");
+        text = value.booleanValue ? "true" : "false";
         break;
     case VariableValueKind::integer:
-        written = std::snprintf(line.data(),
-                                line.size(),
-                                "ev=mission_script stage=variable result=set name=%.*s value=%lld",
-                                static_cast<int>(name.size()),
-                                name.data(),
-                                static_cast<long long>(value.integerValue));
+        scalarWritten = std::snprintf(
+            scalar.data(), scalar.size(), "%lld", static_cast<long long>(value.integerValue));
         break;
     case VariableValueKind::real:
-        written = std::snprintf(line.data(),
-                                line.size(),
-                                "ev=mission_script stage=variable result=set name=%.*s value=%.6g",
-                                static_cast<int>(name.size()),
-                                name.data(),
-                                value.realValue);
+        scalarWritten = std::snprintf(scalar.data(), scalar.size(), "%.6g", value.realValue);
         break;
     case VariableValueKind::string:
-        written = std::snprintf(line.data(),
-                                line.size(),
-                                "ev=mission_script stage=variable result=set name=%.*s value=%.*s",
-                                static_cast<int>(name.size()),
-                                name.data(),
-                                static_cast<int>(value.stringLength),
-                                value.stringValue.data());
+        text = {value.stringValue.data(),
+                (std::min)(std::size_t{value.stringLength}, value.stringValue.size())};
         break;
     }
+    if (scalarWritten > 0) {
+        text = {scalar.data(),
+                (std::min)(static_cast<std::size_t>(scalarWritten), scalar.size() - 1)};
+    }
+    const std::string_view name = state_key_view(key);
+    std::array<char, core::log::kLineCapacity> line{};
+    const int written =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=mission_script stage=variable result=set name=%.*s value=%.*s",
+                      static_cast<int>(name.size()),
+                      name.data(),
+                      static_cast<int>(text.size()),
+                      text.data());
     if (written > 0) {
         core::log::write(
             core::log::Channel::server,

+ 0 - 3
Sunrise/src/server/activity/mission/mission_script_object_sense.h

@@ -14,9 +14,6 @@ inline constexpr std::uint16_t kObjectGenerationOrdinal = 0;
 /** Ordinal 1 is the property the mode-2 interaction effect clears. The meaning is assumed. */
 inline constexpr std::uint16_t kObjectInteractionOpenOrdinal = 1;
 inline constexpr std::uint16_t kObjectExistsOrdinal = 2;
-/** Local aliases: ordinal 1 is read as the alive level, ordinal 2 as presence. */
-inline constexpr std::uint16_t kObjectAliveOrdinal = kObjectInteractionOpenOrdinal;
-inline constexpr std::uint16_t kObjectPresentOrdinal = kObjectExistsOrdinal;
 /** Authored entry the object instantiated, and its two 32-bit spawn-mask words. */
 inline constexpr std::uint16_t kObjectEntryIndexOrdinal = 3;
 inline constexpr std::uint32_t kObjectSpawnMaskSchema = 0x80809E1BU;

+ 3 - 1
Sunrise/src/server/activity/mission/mission_script_runtime.cpp

@@ -299,7 +299,7 @@ void report_squad_damage(const state::activity::SessionBinding& binding,
                            static_cast<float>(shield) / 1023.0F,
                            GetTickCount64());
     } else {
-        // Bounded evidence: a report that finds no mission is otherwise silent.
+        // Log the first few reports that find no mission; nothing else records them.
         static unsigned refused = 0;
         if (refused < 8) {
             ++refused;
@@ -385,6 +385,7 @@ void clear_instance(RuntimeInstance& instance, bool clearPending) noexcept {
     instance.objectInteractionObservations = {};
     instance.objectObservationHeldRegion = -1;
     instance.combatantDamageObservations = {};
+    instance.entityDamageObservations = {};
     instance.deviceObservations = {};
     instance.sceneObservations = {};
     instance.objectiveObservations = {};
@@ -431,6 +432,7 @@ void accept_mission_state(RuntimeInstance& instance,
         instance.ghostObservations = {};
         instance.damageObservations = {};
         instance.combatantDamageObservations = {};
+        instance.entityDamageObservations = {};
         instance.deviceObservations = {};
         instance.objectInteractionObservations = {};
         instance.actorPathObservations = {};

+ 0 - 1
Sunrise/src/server/activity/mission/mission_script_runtime.h

@@ -12,7 +12,6 @@ namespace sunrise::server::activity::mission {
 /** One bounded, value-owned diagnostic row for an attached mission program. */
 struct InstanceDiagnostics final {
     state::activity::SessionBinding binding{};
-    std::array<std::byte, 32> scriptSourceSha256{}, sdkBuildSha256{};
     std::array<char, 64> activityId{};
     std::array<char, 24> programStatus{};
     std::array<char, 32> deliveryStage{};

+ 1 - 5
Sunrise/src/server/activity/mission/mission_script_runtime_attach.cpp

@@ -86,10 +86,8 @@ reload_authorization(const state::activity::SessionBinding& binding) noexcept {
     return module;
 }
 
-} // namespace
-
 /** True when the link, the SDK view and the world view still match what the instance bound. */
-bool still_exact(RuntimeInstance& instance) noexcept {
+[[nodiscard]] bool still_exact(RuntimeInstance& instance) noexcept {
     server::bap::ActivityLinkView link{};
     lua_vm::WorldGenerationIdentity worldGeneration{};
     return server::bap::activity_link_view(instance.view.binding, link)
@@ -106,8 +104,6 @@ bool still_exact(RuntimeInstance& instance) noexcept {
            && sdk_bridge::world_generation_identity(instance.worldView, worldGeneration);
 }
 
-namespace {
-
 /**
  * Re-points one open program at the current ActivityClient generation.
  * @return True when the instance holds an exact view of the same program.

+ 12 - 12
Sunrise/src/server/activity/mission/mission_script_runtime_combatant_damage.cpp

@@ -9,14 +9,15 @@ namespace {
 
 /**
  * Retains each combatant under its complete authored slot identity.
- * @param instance Bound mission instance.
- * @param key Exact accepted Sense source.
+ * @param rows Watch table the source belongs to.
+ * @param key Exact accepted source.
  * @return The matching or newly allocated observation, or null at capacity.
  */
 [[nodiscard]] CombatantDamageObservation*
-find_combatant_damage(RuntimeInstance& instance, const host::SenseObservationKey& key) noexcept {
+find_combatant_damage(std::span<CombatantDamageObservation> rows,
+                      const host::SenseObservationKey& key) noexcept {
     CombatantDamageObservation* spare = nullptr;
-    for (auto& row : instance.combatantDamageObservations) {
+    for (auto& row : rows) {
         if (row.used && row.registryKey == key.registryKey && row.objectTag == key.objectTag
             && row.slotIndex == key.slotIndex) {
             return &row;
@@ -105,7 +106,8 @@ void push_combatant_damage_edges(RuntimeInstance& instance,
             || observation.valueCount > sense.valueCount - observation.firstValue) {
             continue;
         }
-        auto* const row = find_combatant_damage(instance, observation.key);
+        auto* const row =
+            find_combatant_damage(instance.combatantDamageObservations, observation.key);
         if (row == nullptr) {
             log_line(core::log::Level::warn, &instance, "combatant_damage", "watch_capacity");
             continue;
@@ -122,8 +124,8 @@ void push_combatant_damage_edges(RuntimeInstance& instance,
 
 /**
  * Raises a damage state from an entity's replicated damage component when its levels change.
- * Entity rows share the Sense watch table; they have no object tag, so the slot type stands in its
- * place in the row key.
+ * Entity rows have no object tag, so the slot type fills that key field and keeps a squad and a
+ * combatant with the same authored index apart.
  */
 void push_entity_damage(RuntimeInstance& instance,
                         std::uint32_t registryKey,
@@ -138,21 +140,19 @@ void push_entity_damage(RuntimeInstance& instance,
     key.senseSchema = 0;
     key.slotIndex = slotIndex;
     key.slotType = slotType;
-    auto* const row = find_combatant_damage(instance, key);
+    auto* const row = find_combatant_damage(instance.entityDamageObservations, key);
     if (row == nullptr) {
         log_line(core::log::Level::warn, &instance, "combatant_damage", "watch_capacity");
         return;
     }
-    if (row->level.observed && row->level.primary == primary
-        && row->level.secondary == secondary) {
+    if (row->level.observed && row->level.primary == primary && row->level.secondary == secondary) {
         return;
     }
     row->level.primary = primary;
     row->level.secondary = secondary;
     row->level.observed = true;
     ++row->level.revision;
-    push_damage_state(
-        instance, key, row->level, instance.missionStateRevision, tick, "entity");
+    push_damage_state(instance, key, row->level, instance.missionStateRevision, tick, "entity");
 }
 
 } // namespace sunrise::server::activity::mission

+ 13 - 12
Sunrise/src/server/activity/mission/mission_script_runtime_dispatch.cpp

@@ -313,18 +313,19 @@ void dispatch_intent(RuntimeInstance& instance, std::uint64_t now) noexcept {
             }
             return;
         }
-        const squads::Status status = squads::place_reserved(
-            instance.view,
-            intent.firstRow,
-            counts,
-            mode,
-            reservation,
-            std::nullopt,
-            intent.squadRetireOnReturn,
-            std::nullopt,
-            intent.squadSpawnRuleRow < 0 ? std::nullopt
-                                         : std::optional<std::uint32_t>{static_cast<std::uint32_t>(
-                                               intent.squadSpawnRuleRow)});
+        const squads::Status status =
+            squads::place_reserved(instance.view,
+                                   intent.firstRow,
+                                   counts,
+                                   mode,
+                                   reservation,
+                                   std::nullopt,
+                                   intent.squadRetireOnReturn,
+                                   std::nullopt,
+                                   intent.squadSpawnRuleRow < 0
+                                       ? std::nullopt
+                                       : std::optional<std::uint32_t>{
+                                             static_cast<std::uint32_t>(intent.squadSpawnRuleRow)});
         if (status == squads::Status::queued) {
             await_host_commit(instance, now, "squad_enqueued");
         } else if (!abandon_reserved_delivery(instance, reservation)) {

+ 52 - 29
Sunrise/src/server/activity/mission/mission_script_runtime_edges.cpp

@@ -20,7 +20,7 @@ namespace {
 
 /** Root ordinal of the squad removal flag. The rest of the root is inert on this build. */
 constexpr std::uint16_t kSquadRemovalOrdinal = 8;
-/** Root ordinal 0 echoes the squad Auth .6 spawn generation on build 86657. */
+/** Root ordinal 0 echoes the spawn generation the squad's placement Auth carries. */
 constexpr std::uint16_t kSquadSpawnGenerationOrdinal = 0;
 /** Root ordinals of the type-20 damage Sense body: health, shield, then the echoed revision. */
 constexpr std::uint16_t kDamageHealthOrdinal = 0;
@@ -446,10 +446,9 @@ void push_damage_edges(RuntimeInstance& instance,
 
 /**
  * Finds the retained row for one interactable object, allocating a free row on a first
- * observation. At capacity the least recently observed row is recycled: a mission streams more
- * objects than the table holds, and a later-authored object such as a door crystal must still
- * publish its levels rather than silently vanish. Recycling drops that row's level history, so the
- * next report republishes as a baseline.
+ * observation. At capacity the least recently observed row is recycled, so an object first seen
+ * late in a mission still publishes its levels. The recycled row starts without history: its next
+ * report publishes as a baseline and raises no interaction.
  */
 [[nodiscard]] ObjectInteractionObservation*
 find_object_row(RuntimeInstance& instance, const host::SenseObservation& observation) noexcept {
@@ -474,8 +473,10 @@ find_object_row(RuntimeInstance& instance, const host::SenseObservation& observa
     if (row == nullptr) {
         return nullptr;
     }
+    const bool recycled = spare == nullptr;
     *row = {};
     row->used = true;
+    row->interactionBaseline = recycled;
     row->sequence = observation.sequence;
     row->registryKey = observation.key.registryKey;
     row->objectTag = observation.key.objectTag;
@@ -542,10 +543,11 @@ void push_object_interaction_edges(RuntimeInstance& instance,
             }
             push_script_event(instance, event);
         }
-        if (interacted) {
+        if (interacted && !slot->interactionBaseline) {
             event.kind = host::EventKind::objectInteracted;
             push_script_event(instance, event);
         }
+        slot->interactionBaseline = false;
     }
 }
 
@@ -610,12 +612,6 @@ void push_squad_edges(RuntimeInstance& instance,
         if (!observe_population(instance, observation, body)) {
             return;
         }
-        // A root-absent delta carries no squad state and cannot replace the previous levels.
-        if (std::none_of(body.begin(), body.end(), [root](const sense_values::DecodedValue& value) {
-                return value.schemaRow == root;
-            })) {
-            continue;
-        }
         SquadObservation* const squad = find_squad(instance, observation.key);
         if (squad == nullptr) {
             continue;
@@ -631,30 +627,42 @@ void push_squad_edges(RuntimeInstance& instance,
             || (sameSource && observation.sequence <= squad->epoch.sequence)) {
             continue;
         }
+        const sense_values::DecodedValue* const generationValue =
+            sense_value(body, root, kSquadSpawnGenerationOrdinal);
+        const bool bodyHasGeneration = generationValue != nullptr && generationValue->present
+                                       && generationValue->signedValue >= 0
+                                       && generationValue->signedValue <= kMaximumCounter;
+        // A body naming a newer placement than the retained one is never a duplicate or replay.
+        const bool newerLifetime = bodyHasGeneration && squad->hasSpawnGeneration
+                                   && generationValue->signedValue > squad->spawnGeneration;
         if (sameSource && squad->hasReportCounter && observation.hasGeneration
-            && observation.generationPlusOne == squad->reportCounter) {
+            && observation.generationPlusOne == squad->reportCounter && !newerLifetime) {
             continue;
         }
         // After a Sense rebase the client re-sends already delivered counters, often with a fuller
         // delta against an older baseline. A replay repeats known state; treating it as a counter
         // reset tells the scripts the squad lost its registration and spawn echo. A native
-        // re-registration restarts at one and still reconciles below.
+        // re-registration restarts at one, or names a newer placement, and still reconciles below.
         if (sameSource && squad->hasReportCounter && observation.hasGeneration
             && observation.generationPlusOne > 1
-            && observation.generationPlusOne < squad->reportCounter) {
+            && observation.generationPlusOne < squad->reportCounter && !newerLifetime) {
             continue;
         }
-        // Retained levels only bridge a proven consecutive report. A gap, wrap or re-registration
-        // reconciles from this body alone.
+        // Retained levels only bridge a proven consecutive report; after a gap, wrap or
+        // re-registration a field this body omits is unknown.
         const bool consecutive = sameSource && squad->hasReportCounter && observation.hasGeneration
                                  && squad->reportCounter != UINT32_MAX
                                  && observation.generationPlusOne == squad->reportCounter + 1;
+        // A root-absent delta (a cost-only change, say) only continues the retained levels. After
+        // a gap it carries nothing to reconcile from, so it is ignored.
+        const bool rootPresent =
+            std::any_of(body.begin(), body.end(), [root](const sense_values::DecodedValue& value) {
+                return value.schemaRow == root;
+            });
+        if (!rootPresent && !consecutive) {
+            continue;
+        }
 
-        const sense_values::DecodedValue* const generationValue =
-            sense_value(body, root, kSquadSpawnGenerationOrdinal);
-        const bool bodyHasGeneration = generationValue != nullptr && generationValue->present
-                                       && generationValue->signedValue >= 0
-                                       && generationValue->signedValue <= kMaximumCounter;
         std::int32_t spawnGeneration =
             bodyHasGeneration ? static_cast<std::int32_t>(generationValue->signedValue) : 0;
         bool hasSpawnGeneration = bodyHasGeneration;
@@ -666,10 +674,14 @@ void push_squad_edges(RuntimeInstance& instance,
             bodyHasGeneration
             && (!squad->hasSpawnGeneration || squad->spawnGeneration != spawnGeneration);
         const bool sameLifetime = consecutive && !lifetimeChanged;
+        // Costs are deltas, so they only carry over a consecutive report of the same lifetime. A
+        // reset counts as a change even when this body reports no cost.
+        const SquadObjectiveCosts priorCosts = squad->objectiveCosts;
         if (!sameLifetime) {
             squad->objectiveCosts = {};
         }
-        const bool costsChanged = update_squad_objective_costs(squad->objectiveCosts, body, root);
+        static_cast<void>(update_squad_objective_costs(squad->objectiveCosts, body, root));
+        const bool costsChanged = squad->objectiveCosts != priorCosts;
 
         std::int32_t alive = sameLifetime ? squad->aliveCount : 0;
         bool hasAlive = read_squad_alive(body, root, alive);
@@ -684,8 +696,17 @@ void push_squad_edges(RuntimeInstance& instance,
             removal = squad->removalFlag;
             hasRemoval = squad->hasRemoval;
         }
-        auto counts = sameLifetime ? squad->slotCounts : decltype(squad->slotCounts){};
-        auto countLength = sameLifetime ? squad->slotCountLength : std::uint8_t{};
+        // A forward gap whose body names the retained spawn generation is still that lifetime: its
+        // reported population compares with the retained one, so a death in the gap is not lost.
+        const bool gapInLifetime =
+            !consecutive && sameSource && squad->hasReportCounter && observation.hasGeneration
+            && observation.generationPlusOne > squad->reportCounter && bodyHasGeneration
+            && squad->hasSpawnGeneration && squad->spawnGeneration == spawnGeneration;
+        // Created counts only rise within one lifetime, so a report proven to continue it keeps the
+        // retained counts it omits and compares new ones against them.
+        const bool countsContinue = squad->used && (sameLifetime || gapInLifetime);
+        auto counts = countsContinue ? squad->slotCounts : decltype(squad->slotCounts){};
+        auto countLength = countsContinue ? squad->slotCountLength : std::uint8_t{};
         std::array<std::int32_t, host::kSquadSlotCapacity> incomingCounts{};
         const auto incomingLength = read_squad_created_counts(body, incomingCounts);
         if (incomingLength != 0) {
@@ -699,7 +720,7 @@ void push_squad_edges(RuntimeInstance& instance,
                                                     observation.sequence,
                                                     static_cast<std::uint32_t>(spawnGeneration),
                                                     hasSpawnGeneration && spawnGeneration > 0);
-        const bool counterReset = hadPrevious && !consecutive;
+        const bool counterReset = hadPrevious && !consecutive && !gapInLifetime;
         if (transition != observation_epoch::Transition::discard
             && (counterReset || !squad->hasAlive || !hasAlive)) {
             transition = observation_epoch::Transition::baseline;
@@ -711,7 +732,6 @@ void push_squad_edges(RuntimeInstance& instance,
         squad->hasReportCounter = observation.hasGeneration;
 
         const bool first = transition == observation_epoch::Transition::baseline;
-        const bool wasUsed = squad->used;
         squad->used = true;
         const std::int32_t previousAlive = first ? alive : squad->aliveCount;
         const bool changed =
@@ -746,7 +766,7 @@ void push_squad_edges(RuntimeInstance& instance,
 
         for (std::uint8_t slot = 0; slot < countLength; ++slot) {
             const std::int32_t previous =
-                wasUsed && !first && slot < squad->slotCountLength ? squad->slotCounts[slot] : 0;
+                countsContinue && slot < squad->slotCountLength ? squad->slotCounts[slot] : 0;
             if (counts[slot] <= previous) {
                 continue;
             }
@@ -754,12 +774,15 @@ void push_squad_edges(RuntimeInstance& instance,
             spawned.squadSlotOrdinal = slot;
             spawned.squadSlotValue = counts[slot];
             spawned.squadPreviousSlotValue = previous;
+            spawned.squadSpawnGeneration = spawnGeneration;
+            spawned.squadHasSpawnGeneration = hasSpawnGeneration;
             spawned.kind = host::EventKind::entitySpawned;
             if (instance.programStatus == ProgramStatus::loaded) {
                 push_script_event(instance, spawned);
             }
         }
-        // Only consecutive reports of one spawn lifetime may prove a population decrease.
+        // Only reports of one spawn lifetime, consecutive or across a gap the body confirms, may
+        // prove a population decrease.
         if (!first && hasAlive && squad->hasAlive && alive < squad->aliveCount) {
             host::Event died = sense_edge_event(instance, observation);
             died.squadAliveCount = alive;

+ 1 - 3
Sunrise/src/server/activity/mission/mission_script_runtime_feed.cpp

@@ -128,7 +128,6 @@ void clear_pending_event(PendingMissionEvent& pending) noexcept {
            || event.kind == host::EventKind::triggerEntered
            || event.kind == host::EventKind::triggerState
            || event.kind == host::EventKind::triggerExited
-           || event.kind == host::EventKind::objectState
            || event.kind == host::EventKind::squadState
            || event.kind == host::EventKind::squadProvoked
            || event.kind == host::EventKind::entitySpawned
@@ -567,8 +566,7 @@ void clear_feed_cursors() noexcept {
     g_missionInputCursor = {};
 }
 
-/** A committed held-region change requires fresh native object levels, preserving replay cursors.
- */
+/** A committed held-region change re-baselines object and trigger levels; replay cursors stay. */
 void reconcile_object_region(RuntimeInstance& instance, const host::Event& event) noexcept {
     if (event.kind != host::EventKind::clientStateChanged
         || instance.objectObservationHeldRegion == event.heldRegionIndex) {

+ 13 - 11
Sunrise/src/server/activity/mission/mission_script_runtime_internal.h

@@ -91,11 +91,7 @@ constexpr std::size_t kTriggerOccupancyCapacity = 32;
 constexpr std::size_t kSquadObservationCapacity = 160;
 /** Watched authored scenes retained per instance. */
 constexpr std::size_t kSceneObservationCapacity = 32;
-/**
- * Watched objective sensors retained per instance. Strange Terrain reports eleven distinct
- * sensors before the relic phase; keep route-wide headroom aligned with the other authored-watch
- * tables so later crystals cannot lose their first baseline.
- */
+/** Watched objective sensors retained per instance. */
 constexpr std::size_t kObjectiveObservationCapacity = 32;
 static_assert(kSquadObjectiveGroupCount == host::kSquadObjectiveGroupCount);
 /** Watched Ghost links, damage monitors, interactable objects and named actors per instance. */
@@ -166,6 +162,11 @@ struct ObjectInteractionObservation final {
     ObjectInteractionLevel level{};
     /** Sense sequence of the last accepted observation; the oldest row yields at capacity. */
     std::uint64_t sequence{};
+    /**
+     * Set on a row recycled at capacity: its first report cannot tell a new interaction from one
+     * the evicted row already raised, so that report only records the interaction latch.
+     */
+    bool interactionBaseline{};
     std::uint32_t registryKey{};
     std::uint32_t objectTag{};
     std::uint16_t slotIndex{};
@@ -185,13 +186,15 @@ struct GhostObservation final {
 struct SquadObservation final {
     SquadObjectiveCosts objectiveCosts{};
     std::array<std::int32_t, host::kSquadSlotCapacity> slotCounts{};
-    /** Source, sequence and spawn-echo continuity; only consecutive reports bridge absent levels.
-     */
+    /** Source, sequence and spawn-generation continuity of the last accepted report. */
     observation_epoch::Cursor epoch{};
+    /** Sense record counter plus one of the last accepted report. */
     std::uint32_t reportCounter{};
     bool hasReportCounter{};
+    /** Spawn generation the squad Sense echoes from its placement Auth. */
     std::int32_t spawnGeneration{};
     bool hasSpawnGeneration{};
+    /** The alive count and removal flag below were reported, not defaulted. */
     bool hasAlive{};
     bool hasRemoval{};
     std::uint32_t registryKey{};
@@ -268,6 +271,8 @@ struct RuntimeInstance final {
     std::array<GhostObservation, kGhostObservationCapacity> ghostObservations{};
     std::array<DamageObservation, kDamageObservationCapacity> damageObservations{};
     std::array<CombatantDamageObservation, kSquadObservationCapacity> combatantDamageObservations{};
+    /** Damage levels replicated by authored entities, kept apart from the Type-2 Sense rows. */
+    std::array<CombatantDamageObservation, kSquadObservationCapacity> entityDamageObservations{};
     std::array<DeviceObservation, kDeviceObservationCapacity> deviceObservations{};
     std::array<ObjectInteractionObservation, kObjectInteractionObservationCapacity>
         objectInteractionObservations{};
@@ -300,7 +305,6 @@ struct RuntimeInstance final {
     std::int32_t activeRegion{-1};
     ProgramStatus programStatus{ProgramStatus::none};
     DeliveryStage deliveryStage{DeliveryStage::idle};
-    /** Last logged native reaction drain class; 0 is ready/empty, otherwise status+1. */
     /** The player key the bound link's message 5 binds, read at attach. */
     std::uint64_t playerKey{};
     bool publicTarget{};
@@ -492,12 +496,10 @@ void note_vm_status(RuntimeInstance& instance,
 
 /** Resolves the script root and the SDK Lua search path. Logs its own refusal. */
 [[nodiscard]] bool resolve_script_paths() noexcept;
-/** Clears both script buffers and both paths. */
+/** Clears the script buffer, both paths and every reload authorization. */
 void clear_script_paths() noexcept;
 /** @return False when no authorization slot is free, so the reload cannot replace this program. */
 [[nodiscard]] bool authorize_reload(const RuntimeInstance& instance) noexcept;
-/** @return True while the binding, link, SDK view and generated world still match the instance. */
-[[nodiscard]] bool still_exact(RuntimeInstance& instance) noexcept;
 /** Drops slots that no longer match, publishes the roster, and attaches active host instances. */
 void synchronize_instances(std::uint64_t now) noexcept;
 /** Advances fresh programs only when their declared state roster has reached transport output. */

+ 2 - 3
Sunrise/src/server/activity/mission/mission_script_sdk_bridge.cpp

@@ -122,9 +122,8 @@ resolve_activity_binding_locator(const void* context,
     const auto allSquads = catalog.squads();
     const auto members = sdk::squad_members(catalog, squad);
     if (members.empty() || members.size() > output.defaultCounts.size()
-        || (squad.flags & format::squad_required_mask(squad.flags))
-               != format::squad_required_mask(squad.flags)
-        || &squad < allSquads.data() || &squad >= allSquads.data() + allSquads.size()) {
+        || !format::squad_runnable(squad.flags) || &squad < allSquads.data()
+        || &squad >= allSquads.data() + allSquads.size()) {
         return false;
     }
     for (std::size_t index = 0; index < members.size(); ++index) {

+ 4 - 11
Sunrise/src/server/activity/mission/mission_script_vm.cpp

@@ -547,14 +547,15 @@ OpenStatus open(Vm& vm,
         preserve_failure_and_close(impl);
         return status;
     }
+
     CallFrame loadFrame{};
     loadFrame.remainingInstructions = kInitializationInstructionBudget;
     impl.frame = &loadFrame;
     lua_sethook(impl.state, &instruction_hook, LUA_MASKCOUNT, kHookInterval);
     const int executed = lua_pcall(impl.state, 0, 1, 0);
+    lua_sethook(impl.state, nullptr, 0, 0);
+    impl.frame = nullptr;
     if (executed != LUA_OK) {
-        lua_sethook(impl.state, nullptr, 0, 0);
-        impl.frame = nullptr;
         detail::set_error(
             impl,
             existing_error(impl.state, "mission initialization failed with a non-string error"));
@@ -564,14 +565,10 @@ OpenStatus open(Vm& vm,
         preserve_failure_and_close(impl);
         return status;
     }
-    // The returned program table stays above the capture/seal frames so a declared preload is
-    // charged to the same initialization budget as the opening chunk.
     lua_pushcfunction(impl.state, &capture_program);
-    lua_pushvalue(impl.state, -2);
+    lua_insert(impl.state, -2);
     const int captured = lua_pcall(impl.state, 1, 0, 0);
     if (captured != LUA_OK) {
-        lua_sethook(impl.state, nullptr, 0, 0);
-        impl.frame = nullptr;
         detail::set_error(
             impl, existing_error(impl.state, "mission declaration failed with a non-string error"));
         const OpenStatus status =
@@ -579,8 +576,6 @@ OpenStatus open(Vm& vm,
         preserve_failure_and_close(impl);
         return status;
     }
-    lua_sethook(impl.state, nullptr, 0, 0);
-    impl.frame = nullptr;
     lua_settop(impl.state, 0);
     impl.active = true;
     impl.faulted = false;
@@ -908,8 +903,6 @@ const char* status_name(OpenStatus status) noexcept {
         return "runtime_error";
     case OpenStatus::invalidProgram:
         return "invalid_program";
-    case OpenStatus::dependencyRefused:
-        return "dependency_refused";
     }
     return "unknown";
 }

+ 0 - 3
Sunrise/src/server/activity/mission/mission_script_vm.h

@@ -74,7 +74,6 @@ struct SceneDefinition final {
 
 /** One scenario-local reusable object slot resolved against the pinned SDK view. */
 struct SlotDefinition final {
-    std::uint32_t objectiveCount{};
     std::string_view id{};
     std::string_view name{};
     std::string_view objectId{};
@@ -445,8 +444,6 @@ enum class OpenStatus : std::uint8_t {
     compileError,
     runtimeError,
     invalidProgram,
-    /** The returned sealed dependency declaration was missing, malformed or incomplete. */
-    dependencyRefused,
 };
 
 /** Stable protected-callback outcome. */

+ 15 - 15
Sunrise/src/server/activity/mission/mission_trigger_observation.h

@@ -10,10 +10,11 @@
 namespace sunrise::server::activity::mission::trigger_observation {
 
 namespace sense = middleware::bap::activity_message::sense_update;
-/** Build-86657 type30 Sense: two required bits followed by two required signed words. */
+/** Type-30 occupancy Sense: two required flags, then two required signed 32-bit values. */
 constexpr std::uint32_t kSchema = 0x80809531U;
 constexpr std::uint8_t kSlotType = 30;
 
+/** One complete occupancy report. */
 struct Snapshot final {
     bool occupied{};
     bool all{};
@@ -21,7 +22,7 @@ struct Snapshot final {
     std::int32_t threshold{};
 };
 
-/** Rejects incomplete/ambiguous bodies; required fields never inherit invented defaults. */
+/** Reads one occupancy body. An incomplete or ambiguous body is refused, never defaulted. */
 [[nodiscard]] inline bool read(std::span<const sense::DecodedValue> body,
                                Snapshot& output) noexcept {
     if (body.size() != 4) {
@@ -53,6 +54,7 @@ struct Snapshot final {
 }
 
 enum class Edge : std::uint8_t { none, entered, exited };
+/** How one report relates to the last accepted one, as scripts see it. */
 enum class Continuity : std::uint8_t {
     invalid,
     baseline,
@@ -64,6 +66,7 @@ enum class Continuity : std::uint8_t {
     missingCounter,
     wrap
 };
+/** @return The stable script-facing name of one continuity value. */
 [[nodiscard]] inline const char* name(Continuity value) noexcept {
     switch (value) {
     case Continuity::baseline:
@@ -86,13 +89,20 @@ enum class Continuity : std::uint8_t {
         return "invalid";
     }
 }
+/** What one report means: an optional edge, its continuity, and whether scripts hear of it. */
 struct Observation final {
     Edge edge{Edge::none};
     Continuity continuity{Continuity::invalid};
-    bool notify{}, available{};
+    /** Scripts receive a triggerState event for this report. */
+    bool notify{};
+    /** The report carried a complete level the event may expose. */
+    bool available{};
 };
 
-/** Adjacent native reports establish continuity; repeated envelopes never confirm a level. */
+/**
+ * Tracks one volume's reports. Only consecutive record counters from one source prove an edge; a
+ * repeated counter never confirms a level, and a conflicting repeat invalidates it.
+ */
 struct Tracker final {
     std::uint64_t source{}, sequence{};
     std::uint32_t counter{};
@@ -102,6 +112,7 @@ struct Tracker final {
         known = false;
     }
 
+    /** @param current The decoded level, or null when the body was malformed. */
     [[nodiscard]] Observation observe(const Snapshot* current,
                                       std::uint64_t expectedSource,
                                       std::uint64_t nextSource,
@@ -154,17 +165,6 @@ struct Tracker final {
         snapshot = *current;
         return {edge, continuity, true, nextHasCounter};
     }
-    /** Compatibility entry point for edge-only consumers. */
-    [[nodiscard]] Edge accept(const Snapshot& current,
-                              std::uint64_t expectedSource,
-                              std::uint64_t nextSource,
-                              std::uint64_t nextSequence,
-                              std::uint32_t nextCounter,
-                              bool nextHasCounter) noexcept {
-        return observe(
-                   &current, expectedSource, nextSource, nextSequence, nextCounter, nextHasCounter)
-            .edge;
-    }
 };
 
 } // namespace sunrise::server::activity::mission::trigger_observation

+ 9 - 6
Sunrise/src/server/activity/squad_attachment_ownership_validation.h

@@ -3,7 +3,7 @@
 #include <algorithm>
 
 #include "../../middleware/bap/activity_message/squad_attachment_auth.h"
-#include "../../state/activity_sdk/attachment_compatibility.h"
+#include "../../state/activity_sdk/format.h"
 #include "host_runtime.h"
 
 namespace sunrise::server::activity::host::attachments {
@@ -15,11 +15,11 @@ namespace sunrise::server::activity::host::attachments {
 [[nodiscard]] inline bool same_owner_pair(const ScriptableTarget& target,
                                           const SquadAttachmentOwnership& owned) noexcept {
     const auto& source = owned.source;
+    namespace auth = middleware::bap::activity_message::scriptable_auth;
     return owned.sdkBuildSha256 != std::array<std::byte, 32>{}
-           && state::activity_sdk::attachment_compatibility::supports(owned.sdkPayloadSha256)
+           && owned.sdkPayloadSha256 != std::array<std::byte, 32>{}
            && owned.sourceSpawnGeneration > 0 && owned.sourceSpawnGeneration <= 0x7FFFFFFFU
-           && target.slotType == 26
-           && target.authSchema == middleware::bap::activity_message::scriptable_auth::kType26Schema
+           && target.slotType == auth::kType26SlotType && target.authSchema == auth::kType26Schema
            && source.objectTag == target.objectTag && source.registryKey == target.registryKey
            && source.slotType == state::activity_sdk::format::kSquadSlotType
            && source.authSchema == state::activity_sdk::format::kSquadAuthSchema
@@ -119,12 +119,15 @@ namespace sunrise::server::activity::host::attachments {
         || sourceSpawn != owned.sourceSpawnGeneration) {
         return false;
     }
+    // A request is stale when its source lifetime moved on (checked above) or when it would move an
+    // active attachment to another lifetime without an owned clear first. A fresh claim must
+    // select.
     if (previous == nullptr) {
-        if (!owned.active || owned.previousRevision != 0) {
+        if (!owned.active) {
             return false;
         }
     } else {
-        if (!previous->squadAttachment || previous->revision != owned.previousRevision
+        if (!previous->squadAttachment
             || previous->squadAttachment->sdkBuildSha256 != owned.sdkBuildSha256
             || previous->squadAttachment->sdkPayloadSha256 != owned.sdkPayloadSha256
             || previous->squadAttachment->source != owned.source

+ 0 - 2
Sunrise/src/server/bap/encrypted/push/snapshot/family4_snapshot_preparer.cpp

@@ -83,8 +83,6 @@ bool prepare(Scratch& scratch,
     }
 
     Prepared staged{};
-    staged.accountIdentity = {account.primarySoid,
-                              state::account::selected_character_soid(account)};
     staged.rawClearSize =
         (std::max)(reservation.rawClearSize,
                    reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);

+ 0 - 2
Sunrise/src/server/bap/encrypted/push/snapshot/snapshot.h

@@ -28,8 +28,6 @@ struct Prepared {
     middleware::queuez::Family family{};
     std::size_t rawClearSize{};
     std::size_t compressedClearSize{};
-    /** Exact account/character used to encode a Family-4 image, zero for other families. */
-    std::array<std::uint64_t, 2> accountIdentity{};
 
     // Default copying would leave family.objects pointing into the source descriptor array.
     Prepared() noexcept = default;

+ 0 - 1
Sunrise/src/server/bap/encrypted/push/snapshot/snapshot_storage.cpp

@@ -105,7 +105,6 @@ bool commit(const Prepared& staged, Prepared& output) noexcept {
     output.objects = staged.objects;
     output.rawClearSize = staged.rawClearSize;
     output.compressedClearSize = staged.compressedClearSize;
-    output.accountIdentity = staged.accountIdentity;
     output.family = middleware::queuez::Family{
         staged.family.type,
         staged.family.rootSoid,

+ 1 - 2
Sunrise/src/server/bap/internal.h

@@ -114,8 +114,7 @@ struct RosterGroupLease {
     /** Bubbles this key was ever registered under; a later push keeps it in every one of them. */
     std::uint64_t bubbles{};
     std::uint8_t sequence{};
-    /** Group revision the client last received; it rebuilds the group, and loses Sense, on a
-     * change. */
+    /** Group revision the client last received. A new revision rebuilds the group and its Sense. */
     std::uint8_t publishedSequence{};
     bool published{};
     bool used{};

+ 2 - 2
Sunrise/src/server/gameplay/actor_command_policy_lane.cpp

@@ -288,8 +288,8 @@ stage_damage_report(const SessionRow& session,
     // A session learns its binding only from a policy; without one the packet's source names it.
     report.policyBinding = session.policyActive && session.bindingRetained;
     report.binding = session.binding;
-    report.generation = report.policyBinding ? session.activityClientGeneration
-                                             : origin.activityClientGeneration;
+    report.generation =
+        report.policyBinding ? session.activityClientGeneration : origin.activityClientGeneration;
     return report;
 }
 

+ 2 - 2
Sunrise/src/server/gameplay/actor_command_policy_session.h

@@ -93,8 +93,8 @@ struct SelectedSquad final {
     output = {};
     const auto slots = catalog.slots();
     const auto objects = catalog.objects();
-    if ((squad.flags & format::kSquadRunnableMask) != format::kSquadRunnableMask
-        || squad.slotIndex >= slots.size() || squad.objectIndex >= objects.size()) {
+    if (!format::squad_runnable(squad.flags) || squad.slotIndex >= slots.size()
+        || squad.objectIndex >= objects.size()) {
         return false;
     }
     const auto& slot = slots[squad.slotIndex];

+ 2 - 5
Sunrise/src/state/activity/mission/activity_mission_state.cpp

@@ -132,12 +132,9 @@ namespace {
            && left.authBody == right.authBody && left.sequenceOwner == right.sequenceOwner
            && left.sdkBuildSha256 == right.sdkBuildSha256 && left.kind == right.kind
            && left.firstRow == right.firstRow && left.secondRow == right.secondRow
-           && left.spawnRuleSlotRow == right.spawnRuleSlotRow
-           && left.expectedObjectiveRevision == right.expectedObjectiveRevision
            && left.sourceSpawnGeneration == right.sourceSpawnGeneration
-           && left.objectTag == right.objectTag
-           && left.registryKey == right.registryKey && left.sceneEventKey == right.sceneEventKey
-           && left.authSchema == right.authSchema
+           && left.objectTag == right.objectTag && left.registryKey == right.registryKey
+           && left.sceneEventKey == right.sceneEventKey && left.authSchema == right.authSchema
            && left.actorCommandSelector == right.actorCommandSelector
            && left.actorCommandValue == right.actorCommandValue
            && left.authBitCount == right.authBitCount

+ 1 - 5
Sunrise/src/state/activity/mission/definition.h

@@ -244,12 +244,8 @@ struct TypedIntent final {
     std::uint64_t requestKey{kAbsentIntentKey};
     IntentKind kind{IntentKind::placeSquad};
     std::uint32_t firstRow{};
-    /** placeSquad with active=true: director SDK row; slotIndex is its authored objective ordinal.
-     */
     std::uint32_t secondRow{};
-    /** Placement-only exact SDK slot row; all other intent kinds leave this absent. */
-    std::uint32_t spawnRuleSlotRow{0xFFFFFFFFU};
-    /** Attachment source lifetime observed from an exact type-1 Sense baseline. */
+    /** Placement spawn generation of a setSquadAttachment source squad. */
     std::uint64_t sourceSpawnGeneration{};
     std::uint32_t sceneEventKey{};
     std::uint32_t objectTag{};

+ 0 - 23
Sunrise/src/state/activity_sdk/attachment_compatibility.h

@@ -1,23 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <span>
-
-#include "format.h"
-
-namespace sunrise::state::activity_sdk::attachment_compatibility {
-
-static_assert(format::kVersion == 41,
-              "Re-audit attachment compatibility when the SDK format changes");
-
-/**
- * Capability compatibility for the audited attachment pairs.
- * The generated SDK digest is only known at runtime, so no audited-payload digest is pinned here.
- * The type-26/type-1 same-owner checks in the slot API and attachments::admits still validate
- * every attachment; pin the digest here once a catalog digest has been logged and audited.
- */
-[[nodiscard]] inline bool supports(std::span<const std::byte> payloadSha256) noexcept {
-    return !payloadSha256.empty();
-}
-
-} // namespace sunrise::state::activity_sdk::attachment_compatibility

+ 15 - 7
Sunrise/src/state/activity_sdk/format.h

@@ -9,8 +9,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. */
-/** Version 41 adds independently verified common squad-member profiles. */
+/** Runtime-pack schema version accepted by this reader. 41 adds squad-member spawn profiles. */
 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;
@@ -246,10 +245,15 @@ inline constexpr std::uint32_t kSquadRequiresSelectedRule = 0x40U;
 /** Runnable without the authored edge and its points, which a selected rule replaces. */
 inline constexpr std::uint32_t kSquadSourcePrerequisiteMask =
     kSquadRunnableMask & ~(kSquadSpawnerRuleEdgeExact | kSquadAllPointsExact);
-/** The mask one squad row must satisfy before it can be placed. */
-[[nodiscard]] constexpr std::uint32_t squad_required_mask(std::uint32_t flags) noexcept {
-    return (flags & kSquadRequiresSelectedRule) != 0 ? kSquadSourcePrerequisiteMask
-                                                     : kSquadRunnableMask;
+/**
+ * @return True when a squad row may be placed or commanded. A rule-less squad is exempt from the
+ * rule edge and points that its selected rule supplies.
+ */
+[[nodiscard]] constexpr bool squad_runnable(std::uint32_t flags) noexcept {
+    const std::uint32_t required = (flags & kSquadRequiresSelectedRule) != 0
+                                       ? kSquadSourcePrerequisiteMask
+                                       : kSquadRunnableMask;
+    return (flags & required) == required;
 }
 /** Member flags separate actor resolution from count-array completeness. */
 inline constexpr std::uint32_t kSquadMemberActorClassExact = 0x1U;
@@ -259,7 +263,10 @@ inline constexpr std::uint32_t kSquadMemberNoNullCandidates = 0x8U;
 inline constexpr std::uint32_t kSquadMemberAuthoredProfileExact = 0x10U;
 inline constexpr std::uint32_t kSquadMemberFlagMask = 0x1FU;
 
-/** Auth field 5 lanes 1..4 retain the validated package logical ranges. */
+/**
+ * @return True when every lane of a squad member's authored spawn profile (the four lanes the
+ * placement Auth carries in field 5) is within the range the package format allows.
+ */
 [[nodiscard]] constexpr bool
 valid_authored_spawn_profile(const std::array<std::int8_t, 4>& profile) noexcept {
     constexpr std::array<std::int8_t, 4> maximum{2, 6, 2, 6};
@@ -270,6 +277,7 @@ valid_authored_spawn_profile(const std::array<std::int8_t, 4>& profile) noexcept
     }
     return true;
 }
+
 inline constexpr std::uint32_t kSquadMemberInvariantReadyMask =
     kSquadMemberCandidateCountsComplete | kSquadMemberCandidateCountsInvariant
     | kSquadMemberNoNullCandidates;

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

@@ -314,9 +314,9 @@ void shutdown() noexcept;
 [[nodiscard]] const char* status_name(MissionSeedStatus value) noexcept;
 /** @return Stable diagnostic text for one authored-scene seed result. */
 [[nodiscard]] const char* status_name(AuthoredSceneSeedStatus value) noexcept;
-/** @return Shared ownership of the current validated catalog. */
-/** Keeps catalog publication stable through one transport transaction.
- * Ordinary snapshot reads do not acquire this lock. Do not reload or nest this guard while held.
+/**
+ * Keeps catalog publication stable through one transport transaction. Ordinary snapshot reads do
+ * not take this lock. Do not reload or nest the guard while one is held.
  */
 class CatalogPublicationGuard final {
 public:
@@ -326,6 +326,7 @@ public:
     CatalogPublicationGuard& operator=(const CatalogPublicationGuard&) = delete;
 };
 
+/** @return Shared ownership of the current validated catalog. */
 [[nodiscard]] Snapshot snapshot() noexcept;
 /** Resolves one route only from the exact loaded SDK registry. */
 [[nodiscard]] bool executable_communication_route(