Просмотр исходного кода

Recover Ember placement context and prepare cinematic opening playtest

Millie 4 дней назад
Родитель
Сommit
995bd52d3f

+ 37 - 0
MISSION_EMBER_REFERENCE.md

@@ -0,0 +1,37 @@
+# 1AU reference and implementation evidence
+
+Reference: https://www.youtube.com/watch?v=PqurUhqC2CE (36:42).
+A local low-resolution video copy was retrieved on 2026-09-05 for timestamped frame inspection.
+The recording starts with player control at Starboard Landing; it does not show the arrival
+cinematic. Its absence here is not evidence that the original mission lacks one.
+
+| Video time | Visible event | Script/package counterpart | Verification |
+| --- | --- | --- | --- |
+| 00:00 | Starboard Landing, initial objective | powerhouse state, region 64 | video and SDK |
+| ~00:05–00:15 | Ghost arrival line and Ikora central-core guidance | dialog sensor 80B3C90A, cue 0 | video subtitles and SDK variants |
+| ~00:25 onward | Landing enemies, including shielded Cabal and melee enemies | landing squad family | individual squad-to-actor mapping still under audit |
+| ~01:35–01:45 | Weapon active / disconnect it from the sun exchange | cue 2 | video subtitles and SDK variants |
+| ~03:12–03:15 | Player approaches console and deploys Ghost | console ghost-link slot, type 65 | frame inspection at 1-second intervals |
+| ~03:16–03:22 | Bridge machinery moves; crossing becomes available | six bridge devices | direction and live Auth delivery still need confirmation |
+| ~03:25–05:20 | Crossing/far-side combat | bridge crossing and landing-sun families | visible encounter, not a reason to spawn all later enemies at launch |
+| ~05:30 | Mineral Processing transition | link state, region 56 | video and SDK |
+| ~07:15–09:45 | Processing fight and machinery progression | processing families, fusion-cell objectives | coarse frame pass; precise triggers pending |
+| ~10:10 | Zavala/Ikora radio-silence exchange | cues 19–20 | video subtitles and SDK variants |
+| ~11:00 | Ghost identifies exterior route | cue 23 | video subtitles and SDK variants |
+| ~11:25–13:30 | Sunside traversal and combat | cinder state, sunburn families | coarse frame pass |
+| ~13:55–20:10 | Internal combat and machinery, ending with the Bruiser fight | cinder later encounters | coarse frame pass |
+| ~20:35 | Energy-stream travel | chute/tube progression | coarse frame pass |
+| ~21:00–22:40 | Dark corridors, then Interceptor and Light’s End | apex access/security encounters | coarse frame pass |
+| ~23:00–26:00 | Reactor combat and vent attacks | reactor clamshell/coffin families | coarse frame pass |
+| ~26:00–27:30 | Fusion-cell pickup, carrying and deposit | reactor carry/deposit objectives | coarse frame pass |
+| ~27:48–28:42 | Overload, escape run, cinematic transition | reactor escape and bookend state | coarse frame pass |
+| ~28:42–35:27 | City counterattack and Ghaul cinematics | two apex cinematic states are candidates; identity needs confirmation | video only for ordering |
+| ~35:54–36:42 | City arrival and Chosen gameplay | next mission | excluded from Ember scripting scope |
+
+Times are observations from this player's run, not timers to encode in Lua. Cues and waves must
+follow the corresponding spatial, interaction or combat event. The SDK contains declarations,
+identities and data; those alone do not establish every authored script condition.
+
+Live log evidence from the previous build: bridge Auth requests were staged at service ticks
+97082–97268, before mission world entry at 102262 and held region 64 at 104814. Ten squad
+requests were staged at 104915–107153. Transport staging does not establish visible state or AI.

+ 190 - 0
MISSION_EMBER_SQUADS.md

@@ -0,0 +1,190 @@
+# Ember squad coverage audit
+
+Generated from the corrected SDK on 2026-09-05. Readiness means static definition and
+actor-profile checks pass; it does not prove live AI or Lua activation.
+
+The current controller activates only the landing Mercury encounter. All other runnable
+rows still need their encounter triggers wired. This table deliberately includes every
+type-1 sensor, including rows that have no runnable definition.
+
+## Region 0 — apex
+
+| Authored squad sensor | SDK binding |
+| --- | --- |
+| `reactor_clamshell_east_anchor_a_squad` | Ready |
+| `reactor_clamshell_east_support_a_squad` | Ready |
+| `reactor_clamshell_east_support_b_squad` | Ready |
+| `reactor_clamshell_east_support_c_squad` | Ready |
+| `reactor_clamshell_east_support_d_squad` | Ready |
+| `reactor_clamshell_east_support_e_squad` | Ready |
+| `reactor_clamshell_east_support_f_squad` | Ready |
+| `reactor_clamshell_east_melee_a_squad` | Ready |
+| `reactor_clamshell_east_melee_b_squad` | Ready |
+| `reactor_clamshell_east_melee_c_squad` | Ready |
+| `reactor_clamshell_east_melee_d_squad` | Ready |
+| `reactor_clamshell_east_melee_e_squad` | Ready |
+| `reactor_clamshell_east_melee_f_squad` | Ready |
+| `reactor_clamshell_east_defense_a_squad` | Ready |
+| `reactor_clamshell_east_defense_b_squad` | Ready |
+| `reactor_clamshell_west_anchor_a_squad` | Ready |
+| `reactor_clamshell_west_support_a_squad` | Ready |
+| `reactor_clamshell_west_support_b_squad` | Ready |
+| `reactor_clamshell_west_support_c_squad` | Ready |
+| `reactor_clamshell_west_support_d_squad` | Ready |
+| `reactor_clamshell_west_support_e_squad` | Ready |
+| `reactor_clamshell_west_support_f_squad` | Ready |
+| `reactor_clamshell_west_defense_a_squad` | Ready |
+| `reactor_clamshell_west_defense_b_squad` | Ready |
+| `reactor_clamshell_west_melee_a_squad` | Ready |
+| `reactor_clamshell_west_melee_b_squad` | Ready |
+| `reactor_clamshell_west_melee_c_squad` | Ready |
+| `reactor_clamshell_west_melee_d_squad` | Ready |
+| `reactor_clamshell_west_melee_e_squad` | Ready |
+| `reactor_clamshell_west_melee_f_squad` | Ready |
+| `reactor_coffin_east_anchor_a_squad` | Ready |
+| `reactor_coffin_west_anchor_a_squad` | Ready |
+| `reactor_coffin_east_defense_a_squad` | Ready |
+| `reactor_coffin_east_defense_b_squad` | Ready |
+| `reactor_coffin_east_support_a_squad` | Ready |
+| `reactor_coffin_east_support_b_squad` | Ready |
+| `reactor_coffin_east_support_c_squad` | Ready |
+| `reactor_coffin_east_support_d_squad` | Ready |
+| `reactor_coffin_west_defense_a_squad` | Ready |
+| `reactor_coffin_west_defense_b_squad` | Ready |
+| `reactor_coffin_west_support_a_squad` | Ready |
+| `reactor_coffin_west_support_b_squad` | Ready |
+| `reactor_coffin_west_support_c_squad` | Ready |
+| `reactor_coffin_west_support_d_squad` | Ready |
+| `reactor_coffin_interior_support_squad` | Ready |
+| `access_jump_one_support_a_squad` | Missing definition |
+| `access_jump_one_support_b_squad` | Missing definition |
+| `access_jump_one_support_c_squad` | Missing definition |
+| `access_jump_two_support_a_squad` | Missing definition |
+| `access_jump_two_support_b_squad` | Missing definition |
+| `access_jump_two_support_c_squad` | Missing definition |
+| `dispenser_support_a_squad` | Missing definition |
+| `dispenser_support_b_squad` | Missing definition |
+| `dispenser_support_c_squad` | Missing definition |
+| `security_ledge_support_a_squad` | Missing definition |
+| `security_ledge_support_b_squad` | Missing definition |
+
+## Region 40 — cinder
+
+| Authored squad sensor | SDK binding |
+| --- | --- |
+| `foundry_support_a_squad` | Ready |
+| `foundry_support_b_squad` | Ready |
+| `foundry_support_c_squad` | Ready |
+| `foundry_support_d_squad` | Ready |
+| `foundry_support_e_squad` | Ready |
+| `foundry_support_f_squad` | Ready |
+| `foundry_support_g_squad` | Ready |
+| `foundry_support_h_squad` | Ready |
+| `foundry_support_i_squad` | Ready |
+| `foundry_ranged_a_squad` | Ready |
+| `foundry_ranged_b_squad` | Ready |
+| `foundry_anchor_a_squad` | Ready |
+| `ascent_squad_retreat_instance_a_prefab.retreat_01_squad` | Ready |
+| `ascent_squad_retreat_instance_b_prefab.retreat_01_squad` | Ready |
+| `ascent_squad_retreat_instance_c_prefab.retreat_01_squad` | Ready |
+| `chamber_defense_a_squad` | Ready |
+| `chamber_support_a_squad` | Ready |
+| `meat_grinder_defense_a_squad` | Ready |
+| `meat_grinder_defense_b_squad` | Ready |
+| `meat_grinder_support_a_squad` | Ready |
+| `meat_grinder_support_b_squad` | Ready |
+| `meat_grinder_melee_a_squad` | Ready |
+| `meat_grinder_melee_b_squad` | Ready |
+| `ascent_support_a_squad` | Ready |
+| `ascent_support_b_squad` | Ready |
+| `tumbler_support_a_squad` | Ready |
+| `tumbler_support_b_squad` | Ready |
+| `ready_room_01_support_a_squad` | Missing definition |
+| `ready_room_01_support_b_squad` | Ready |
+| `ready_room_01_support_c_squad` | Missing definition |
+| `ready_room_01_melee_a_squad` | Ready |
+| `ready_room_02_anchor_squad` | Ready |
+| `ready_room_02_melee_a_squad` | Ready |
+| `ready_room_02_melee_b_squad` | Ready |
+| `ready_room_02_melee_c_squad` | Ready |
+| `ready_room_02_ranged_a_squad` | Ready |
+| `ready_room_02_ranged_b_squad` | Ready |
+| `sunburn_squad_retreat_instance_a_prefab.retreat_01_squad` | Ready |
+| `sunburn_squad_retreat_instance_b_prefab.retreat_01_squad` | Ready |
+| `sunburn_squad_retreat_instance_c_prefab.retreat_01_squad` | Ready |
+| `sunburn_bridge_support_a_squad` | Ready |
+| `sunburn_bridge_support_b_squad` | Ready |
+| `sunburn_deck_east_support_a_squad` | Ready |
+| `sunburn_deck_east_support_b_squad` | Ready |
+| `sunburn_deck_west_support_a_squad` | Ready |
+| `sunburn_deck_west_support_b_squad` | Ready |
+| `sunburn_deck_west_support_c_squad` | Ready |
+| `sunburn_deck_west_support_d_squad` | Ready |
+| `sunburn_secret_support_a_squad` | Ready |
+
+## Region 56 — link
+
+| Authored squad sensor | SDK binding |
+| --- | --- |
+| `processing_entry_support_a_squad` | Ready |
+| `processing_entry_support_b_squad` | Ready |
+| `processing_entry_support_c_squad` | Ready |
+| `processing_entry_support_d_squad` | Ready |
+| `processing_defend_defense_a_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_defense_b_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_defense_c_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_defense_d_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_melee_a_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_melee_b_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_melee_c_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_melee_d_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_anchor_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_support_a_squad` | Refused: two distinct spawn-rule targets |
+| `processing_defend_support_b_squad` | Refused: two distinct spawn-rule targets |
+
+## Region 64 — powerhouse
+
+| Authored squad sensor | SDK binding |
+| --- | --- |
+| `bridge_crossing_dropship_a_squad` | Ready |
+| `bridge_crossing_dropship_a_support_a_squad` | Ready |
+| `bridge_crossing_dropship_a_support_b_squad` | Ready |
+| `bridge_crossing_dropship_a_melee_a_squad` | Ready |
+| `bridge_crossing_dropship_a_melee_b_squad` | Ready |
+| `bridge_crossing_dropship_b_squad` | Ready |
+| `bridge_crossing_dropship_c_squad` | Ready |
+| `bridge_crossing_dropship_d_squad` | Ready |
+| `bridge_crossing_support_a_squad` | Ready |
+| `bridge_crossing_support_b_squad` | Ready |
+| `bridge_crossing_defense_a_squad` | Ready |
+| `bridge_crossing_defense_b_squad` | Ready |
+| `bridge_crossing_ranged_a_squad` | Ready |
+| `bridge_vignette_support_a_squad` | Ready |
+| `bridge_vignette_support_b_squad` | Ready |
+| `landing_mercury_anchor_squad` | Ready |
+| `landing_mercury_bonus_anchor_a_squad` | Ready |
+| `landing_mercury_bonus_support_a_squad` | Ready |
+| `landing_mercury_bonus_support_b_squad` | Ready |
+| `landing_mercury_bonus_support_c_squad` | Ready |
+| `landing_mercury_bonus_support_d_squad` | Ready |
+| `landing_mercury_support_a_squad` | Ready |
+| `landing_mercury_support_b_squad` | Ready |
+| `landing_mercury_support_c_squad` | Ready |
+| `landing_mercury_ranged_a_squad` | Ready |
+| `landing_mercury_ranged_b_squad` | Ready |
+| `landing_sun_support_a_squad` | Ready |
+| `landing_sun_support_b_squad` | Ready |
+| `pipe_crossing_support_a_squad` | Ready |
+| `pipe_crossing_support_b_squad` | Ready |
+| `catwalk_entry_ranged_a_squad` | Ready |
+| `catwalk_entry_ranged_b_squad` | Ready |
+| `catwalk_entry_support_a_squad` | Ready |
+| `catwalk_entry_support_b_squad` | Ready |
+| `catwalk_entry_support_c_squad` | Ready |
+| `catwalk_mid_support_a_squad` | Ready |
+| `catwalk_mid_support_b_squad` | Ready |
+| `catwalk_mid_support_c_squad` | Ready |
+| `catwalk_bonus_support_a_squad` | Ready |
+| `helipad_sun_anchor_a_squad` | Ready |
+| `helipad_sun_support_a_squad` | Ready |
+| `helipad_sun_support_b_squad` | Ready |

+ 41 - 0
MISSION_EMBER_STATUS.md

@@ -1,5 +1,46 @@
 # Ember implementation status
 # Ember implementation status
 
 
+## Current work — 2026-09-05
+
+The sections below this update record earlier playtest builds. Current investigation found
+that the offline SDK generator omitted the scenario-layout catalogue needed by container
+and spatial extraction. Initializing it and rebuilding the Ember shard recovered 83 additional
+squad definitions: 160 definitions, 138 runnable, across 162 squad sensors. Thirteen sensors
+still have no definition, and eleven processing sensors each have two refused alternatives.
+See [the complete squad census](MISSION_EMBER_SQUADS.md) and
+[the timestamped video comparison](MISSION_EMBER_REFERENCE.md).
+
+The production generator now prepares the missing catalogue in offline mode, waits for it
+in game mode, and rejects cached shards that lack placement context for a known map stem.
+
+The controller draft uses the separate arrival-cinematic state (49), waits for its exact
+termination event, then selects playable state 64. Bridge reset is delayed until region 64
+is held. Region-less client deltas preserve encounter ownership. Arrival dialogue waits
+for spawn settlement; the console-guidance cue follows combat clear. Regression tests pass,
+but these changes have not yet been validated visually in game.
+
+The Ghost console interaction, later encounter activation, later dialogue and full mission
+completion are still unfinished. The reference video begins after arrival and cannot verify
+the identity of the arrival movie. No claim of complete 1AU parity is made.
+
+Validation: Release build, both Lua controller/encounter suites, six Python SDK checks and
+eight sanitized production-linker regressions pass. A fresh native offline process loaded
+the scenario catalogue itself and rebuilt the old cache successfully into `build/sdk-corrected`
+(138 runnable Ember definitions). The script binds successfully to that SDK, including the
+newly recovered eleventh landing squad. The runtime logs typed Ghost-link Sense fields for
+the next console playtest without interpreting them as an interaction yet.
+
+Installation is pending: Destiny 2 was running, so the installer exited before changing any
+game file. Prepared installer: `/tmp/install-ember-corrected.py`. It backs up the previous
+runtime, SDK, scripts and settings, then verifies installed file hashes.
+
+Workspace recovery: the parent checkout was moved to Trash during the audit. The mission
+folder now has its own recovered `.git` directory, retaining branch `mission-ember` and all
+prior commits. Compiler dependencies were copied into `build/recovered-dependencies/xwin`;
+the local `.xwin-cache` points there. The trashed checkout was not modified.
+
+
+
 ## First test build
 ## First test build
 
 
 The opening landing controller is implemented and loads with the regenerated SDK. It publishes
 The opening landing controller is implemented and loads with the regenerated SDK. It publishes

+ 2 - 2
Sunrise/resources/default_settings.json

@@ -107,8 +107,8 @@
       "arrival_overrides": [
       "arrival_overrides": [
         {
         {
           "package_name": "mission_ember",
           "package_name": "mission_ember",
-          "bubble": 8,
-          "slice_set": 64
+          "bubble": 6,
+          "slice_set": 49
         },
         },
         {
         {
           "package_name": "raid_envy_v310",
           "package_name": "raid_envy_v310",

+ 392 - 5
Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp

@@ -25,8 +25,10 @@
 #include "../../../state/activity_sdk/generation/internal.h"
 #include "../../../state/activity_sdk/generation/internal.h"
 #include "../../../state/activity_sdk/identity.h"
 #include "../../../state/activity_sdk/identity.h"
 #include "../../../state/activity_sdk/runtime.h"
 #include "../../../state/activity_sdk/runtime.h"
+#include "../../../state/build_data/runtime.h"
 #include "../../../state/content_manifest/content_manifest_state_runtime.h"
 #include "../../../state/content_manifest/content_manifest_state_runtime.h"
 #include "../items/packages/internal.h"
 #include "../items/packages/internal.h"
+#include "../scenarios/scenario_build.h"
 #include "activity_sdk_activity_inventory.h"
 #include "activity_sdk_activity_inventory.h"
 #include "activity_sdk_generation_report.h"
 #include "activity_sdk_generation_report.h"
 #include "activity_sdk_generation_worker_internal.h"
 #include "activity_sdk_generation_worker_internal.h"
@@ -213,6 +215,378 @@ void log_stage_duration(std::string_view stage,
     return cancelled();
     return cancelled();
 }
 }
 
 
+/** Counts the rows behind a refused shard, which the coverage verdict alone cannot name. */
+void report_shard_diagnostics(const Scenario& scenario,
+                              const catalog::Snapshot& snapshot) noexcept {
+    std::size_t incompleteObjects = 0;
+    std::size_t incompleteSafety = 0;
+    std::size_t leaves = 0;
+    std::size_t bareTargets = 0;
+    for (const catalog::Object& object : snapshot.objects) {
+        if (!object.complete) {
+            ++incompleteObjects;
+        }
+        if (object.safety == catalog::GroupSafety::incomplete) {
+            ++incompleteSafety;
+        }
+        leaves += object.placedLeafCount;
+        bareTargets += object.bareTargetCount;
+    }
+    std::size_t unresolvedStates = 0;
+    for (const catalog::State& state : snapshot.states) {
+        if (!state.resolved) {
+            ++unresolvedStates;
+        }
+    }
+    const catalog::ContainerPlacementDiagnostics& placements =
+        snapshot.containerPlacementDiagnostics;
+    std::array<char, 1024> line{};
+    const int written =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=sdk_shard_diag scenario=0x%08X objects=%zu "
+                      "obj_incomplete=%zu obj_safety_incomplete=%zu leaves=%zu "
+                      "bare=%zu states_unresolved=%zu "
+                      "unresolved_reads=%llu spatial_ctx=%u spatial_na=%u spatial_complete=%u "
+                      "spatial_unresolved=%llu spatial_dropped=%llu spatial_semantic=%llu "
+                      "spatial_tables=%zu spatial_owners=%zu spatial_instances=%zu "
+                      "cp_ctx=%u cp_na=%u cp_complete=%u cp_owner_complete=%u "
+                      "cp_unresolved=%llu cp_semantic=%llu cp_dropped=%llu/%llu/%llu/%llu/%llu "
+                      "cp_rows=%zu/%zu/%zu/%zu/%zu",
+                      static_cast<unsigned>(scenario.tag),
+                      snapshot.objects.size(),
+                      incompleteObjects,
+                      incompleteSafety,
+                      leaves,
+                      bareTargets,
+                      unresolvedStates,
+                      static_cast<unsigned long long>(snapshot.unresolvedReads),
+                      snapshot.staticSpatialContextResolved ? 1U : 0U,
+                      snapshot.staticSpatialNotApplicable ? 1U : 0U,
+                      snapshot.staticSpatialComplete ? 1U : 0U,
+                      static_cast<unsigned long long>(snapshot.staticSpatialUnresolvedReads),
+                      static_cast<unsigned long long>(snapshot.staticSpatialDropped),
+                      static_cast<unsigned long long>(snapshot.staticSpatialSemanticUnresolved),
+                      snapshot.staticSpatialTables.size(),
+                      snapshot.staticSpatialOwners.size(),
+                      snapshot.staticSpatialInstances.size(),
+                      placements.contextResolved ? 1U : 0U,
+                      placements.contextNotApplicable ? 1U : 0U,
+                      placements.complete ? 1U : 0U,
+                      placements.identityOwnerInventoryComplete ? 1U : 0U,
+                      static_cast<unsigned long long>(placements.unresolvedReads),
+                      static_cast<unsigned long long>(placements.semanticUnresolved),
+                      static_cast<unsigned long long>(placements.droppedLists),
+                      static_cast<unsigned long long>(placements.droppedOwners),
+                      static_cast<unsigned long long>(placements.droppedPlacements),
+                      static_cast<unsigned long long>(placements.droppedConfigs),
+                      static_cast<unsigned long long>(placements.droppedComponents),
+                      snapshot.containerPlacementLists.size(),
+                      snapshot.containerPlacementOwners.size(),
+                      snapshot.containerPlacements.size(),
+                      snapshot.containerPlacementConfigs.size(),
+                      snapshot.containerPlacementComponents.size());
+    if (written > 0) {
+        core::log::write(
+            core::log::Channel::client,
+            core::log::Level::error,
+            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
+    }
+}
+
+/** Writes one new shard under its deterministic digest name. */
+[[nodiscard]] bool build_record(const Work& work,
+                                const package_reader::Source& source,
+                                const builder::ContainerIndex& containers,
+                                package_reader::Scratch& scratch,
+                                builder::ScenarioAnalysisCache& analyses,
+                                const Scenario& scenario,
+                                manifest::Record& record,
+                                std::shared_ptr<const catalog::Snapshot>& output,
+                                std::array<char, kDetailCapacity>& detailScratch,
+                                const char*& detail) {
+    // Every arm below reported the same word, so a failure named the step it reached and nothing
+    // about which of five things went wrong. Each one now says which.
+    const std::string_view name(scenario.name.data(), scenario.nameLength);
+    const auto snapshot = builder::build_scenario_catalog(
+        source, containers, scratch, analyses, scenario.tag, name, &cancelled);
+    if (cancelled()) {
+        detail = "shard_cancelled";
+        return false;
+    }
+    if (!snapshot) {
+        detail = "shard_catalog_null";
+        return false;
+    }
+    if (snapshot->status != catalog::BuildStatus::ready) {
+        const char* stage = "shard_catalog_not_ready";
+        switch (snapshot->status) {
+        case catalog::BuildStatus::failed:
+            stage = "shard_catalog_failed";
+            break;
+        case catalog::BuildStatus::idle:
+            stage = "shard_catalog_idle";
+            break;
+        case catalog::BuildStatus::queued:
+            stage = "shard_catalog_queued";
+            break;
+        case catalog::BuildStatus::building:
+            stage = "shard_catalog_building";
+            break;
+        default:
+            break;
+        }
+        report_shard_diagnostics(scenario, *snapshot);
+        // The builder already names its own refusal. Carry that text out instead of dropping it.
+        const char* reason = snapshot->detail[0] != '\0' ? snapshot->detail.data() : "no detail";
+        detailScratch = {};
+        (void)std::snprintf(detailScratch.data(), detailScratch.size(), "%s:%s", stage, reason);
+        detail = detailScratch.data();
+        return false;
+    }
+    generated::PreparedShard prepared{};
+    if (!generated::prepare(work.sourceFingerprint, *snapshot, prepared)) {
+        detail = "shard_prepare_failed";
+        return false;
+    }
+    const generated::Digest payload = prepared.payload_sha256();
+    std::wstring finalPath;
+    generated::Digest written{};
+    if (!shard_path(work.scenarioDirectory, scenario.tag, payload, finalPath)) {
+        detail = "shard_path_failed";
+        return false;
+    }
+    if (!generated::publish(finalPath.c_str(), std::move(prepared), written)) {
+        detail = "shard_write_failed";
+        return false;
+    }
+    record = {};
+    record.scenarioTag = scenario.tag;
+    record.scenarioName = scenario.name;
+    record.scenarioNameLength = scenario.nameLength;
+    record.shardPayloadSha256 = payload;
+    output = snapshot;
+    return true;
+}
+
+/** Rewrites one loaded cache hit into an isolated output tree. */
+[[nodiscard]] bool
+materialize_cached_record(const Work& work,
+                          const Scenario& scenario,
+                          const manifest::Record& record,
+                          const std::shared_ptr<const catalog::Snapshot>& cached,
+                          std::shared_ptr<const catalog::Snapshot>& output) noexcept {
+    if (cached == nullptr) {
+        return false;
+    }
+    if (work.cacheScenarioDirectory == work.scenarioDirectory) {
+        output = cached;
+        return true;
+    }
+    std::wstring finalPath;
+    generated::Digest written{};
+    if (!shard_path(work.scenarioDirectory, scenario.tag, record.shardPayloadSha256, finalPath)
+        || !generated::write(finalPath.c_str(), work.sourceFingerprint, *cached, written)) {
+        return false;
+    }
+    output = cached;
+    return true;
+}
+
+/** One indexed result keeps parallel work deterministic when threads finish out of order. */
+struct ScenarioBuildResult final {
+    manifest::Record record{};
+    std::shared_ptr<const catalog::Snapshot> snapshot{};
+    std::array<char, kDetailCapacity> detail{};
+    bool attempted{};
+    bool ready{};
+    bool reused{};
+};
+
+/** Shared immutable inputs and atomic scheduling state for one scenario batch. */
+struct ScenarioBuildBatch final {
+    const Work* work{};
+    const package_reader::Source* source{};
+    const builder::ContainerIndex* containers{};
+    const manifest::Catalog* prior{};
+    std::vector<ScenarioBuildResult>* results{};
+    std::atomic_size_t next{};
+    std::atomic_size_t completed{};
+    SRWLOCK progressLock = SRWLOCK_INIT;
+    std::size_t progressPublished{};
+    std::size_t firstScenario{};
+    std::size_t blockCacheSlots{};
+    std::size_t tableCacheSlots{};
+    bool priorReady{};
+};
+
+/** Caps workers while leaving cores for the game and the server. */
+[[nodiscard]] std::size_t scenario_worker_count(std::size_t scenarios) noexcept {
+    SYSTEM_INFO info{};
+    GetSystemInfo(&info);
+    const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
+    const std::size_t available = processors > 2U ? processors - 2U : 1U;
+    return (std::min)(scenarios, (std::min)(available, kMaximumScenarioWorkers));
+}
+
+/** Divides one cache budget across active workers without leaving a worker uncached. */
+[[nodiscard]] std::size_t worker_cache_slots(std::size_t budget, std::size_t workers) noexcept {
+    return (std::max)(std::size_t{1}, (budget + workers - 1U) / workers);
+}
+
+/** Publishes only increasing completion counts from out-of-order workers. */
+void publish_parallel_progress(ScenarioBuildBatch& batch, const Scenario& scenario) noexcept {
+    const std::size_t complete = batch.completed.fetch_add(1U) + 1U;
+    AcquireSRWLockExclusive(&batch.progressLock);
+    if (complete <= batch.progressPublished) {
+        ReleaseSRWLockExclusive(&batch.progressLock);
+        return;
+    }
+    batch.progressPublished = complete;
+    publish_progress(state::activity_sdk::generation::Status::building,
+                     static_cast<std::uint32_t>(complete),
+                     static_cast<std::uint32_t>(batch.work->scenarios.size()),
+                     scenario.tag,
+                     std::string_view(scenario.name.data(), scenario.nameLength));
+    ReleaseSRWLockExclusive(&batch.progressLock);
+}
+
+/** A cache made before the layout catalogue was ready omitted whole placement domains. */
+[[nodiscard]] bool placement_context_ready(const Scenario& scenario,
+                                           const catalog::Snapshot& snapshot) noexcept {
+    state::build_data::scenarios::Definition layout{};
+    const std::string_view name(scenario.name.data(), scenario.nameLength);
+    if (!state::build_data::find_scenario_layout(name, layout) || layout.spawnStemLength == 0) {
+        return true;
+    }
+    return snapshot.containerPlacementDiagnostics.contextResolved
+           && snapshot.staticSpatialContextResolved;
+}
+
+/** Builds one worker's contiguous chunks with private package and analysis caches. */
+void run_scenario_worker(ScenarioBuildBatch& batch) noexcept {
+    std::unique_ptr<package_reader::Scratch> scratch(new (std::nothrow) package_reader::Scratch());
+    if (scratch == nullptr) {
+        return;
+    }
+    if (!package_reader::prepare_blocks(*scratch, batch.blockCacheSlots)) {
+        (void)package_reader::prepare_blocks(*scratch, 0);
+    }
+    if (!package_reader::prepare_tables(*scratch, batch.tableCacheSlots)) {
+        (void)package_reader::prepare_tables(*scratch, 0);
+    }
+    builder::ScenarioAnalysisCache analyses{};
+    while (!cancelled()) {
+        const std::size_t begin = batch.next.fetch_add(kScenarioChunkSize);
+        if (begin >= batch.results->size()) {
+            break;
+        }
+        const std::size_t end = (std::min)(begin + kScenarioChunkSize, batch.results->size());
+        for (std::size_t index = begin; index < end && !cancelled(); ++index) {
+            const Scenario& scenario = batch.work->scenarios[batch.firstScenario + index];
+            ScenarioBuildResult& result = (*batch.results)[index];
+            result.attempted = true;
+            try {
+                const manifest::Record* existing =
+                    batch.priorReady ? find_record(*batch.prior, scenario.tag) : nullptr;
+                bool kept = existing != nullptr
+                            && load_full_record(batch.work->cacheScenarioDirectory,
+                                                batch.work->sourceFingerprint,
+                                                scenario,
+                                                *existing,
+                                                result.snapshot);
+                if (kept && !placement_context_ready(scenario, *result.snapshot)) {
+                    kept = false;
+                }
+                if (kept
+                    && !materialize_cached_record(
+                        *batch.work, scenario, *existing, result.snapshot, result.snapshot)) {
+                    kept = false;
+                }
+                if (kept) {
+                    result.record = *existing;
+                    result.reused = true;
+                    result.ready = true;
+                } else {
+                    const char* detail = "shard_build_failed";
+                    std::array<char, kDetailCapacity> scratchDetail{};
+                    result.ready = build_record(*batch.work,
+                                                *batch.source,
+                                                *batch.containers,
+                                                *scratch,
+                                                analyses,
+                                                scenario,
+                                                result.record,
+                                                result.snapshot,
+                                                scratchDetail,
+                                                detail);
+                    if (!result.ready) {
+                        (void)std::snprintf(
+                            result.detail.data(), result.detail.size(), "%s", detail);
+                    }
+                }
+            } catch (...) {
+                result.ready = false;
+                (void)std::snprintf(result.detail.data(),
+                                    result.detail.size(),
+                                    "%s",
+                                    "unexpected scenario build exception");
+            }
+            publish_parallel_progress(batch, scenario);
+        }
+    }
+    package_reader::close_files(*scratch);
+}
+
+/** Adapts one batch worker to the Windows thread ABI. */
+DWORD WINAPI scenario_thread_main(void* opaque) noexcept {
+    run_scenario_worker(*static_cast<ScenarioBuildBatch*>(opaque));
+    return 0;
+}
+
+/** Builds all scenario snapshots in parallel while retaining scenario-order output. */
+[[nodiscard]] bool build_scenarios(Work& work,
+                                   const package_reader::Source& source,
+                                   const builder::ContainerIndex& containers,
+                                   const manifest::Catalog& prior,
+                                   bool priorReady,
+                                   std::size_t firstScenario,
+                                   std::size_t scenarioCount,
+                                   std::vector<ScenarioBuildResult>& results) {
+    results.clear();
+    results.resize(scenarioCount);
+    ScenarioBuildBatch batch{};
+    batch.work = &work;
+    batch.source = &source;
+    batch.containers = &containers;
+    batch.prior = &prior;
+    batch.results = &results;
+    batch.completed.store(firstScenario);
+    batch.progressPublished = firstScenario;
+    batch.firstScenario = firstScenario;
+    batch.priorReady = priorReady;
+    const std::size_t workers = scenario_worker_count(results.size());
+    if (workers == 0) {
+        return false;
+    }
+    batch.blockCacheSlots = worker_cache_slots(kParallelBlockCacheBudget, workers);
+    batch.tableCacheSlots = worker_cache_slots(kParallelTableCacheBudget, workers);
+    std::vector<HANDLE> threads;
+    threads.reserve(workers - 1U);
+    for (std::size_t index = 1; index < workers; ++index) {
+        const HANDLE thread = CreateThread(nullptr, 0, &scenario_thread_main, &batch, 0, nullptr);
+        if (thread != nullptr) {
+            threads.push_back(thread);
+        }
+    }
+    run_scenario_worker(batch);
+    for (const HANDLE thread : threads) {
+        (void)WaitForSingleObject(thread, INFINITE);
+        (void)CloseHandle(thread);
+    }
+    return !cancelled();
+}
+
 /** Copies one terminal pass result before worker-owned storage is released. */
 /** Copies one terminal pass result before worker-owned storage is released. */
 void finish_offline(Work& work, bool complete, std::size_t built, std::size_t reused) noexcept {
 void finish_offline(Work& work, bool complete, std::size_t built, std::size_t reused) noexcept {
     if (!work.offline || work.offlineStatus == nullptr || work.offlineResult == nullptr) {
     if (!work.offline || work.offlineStatus == nullptr || work.offlineResult == nullptr) {
@@ -251,9 +625,21 @@ DWORD WINAPI thread_main(void* opaque) noexcept {
                              middleware::content::packages::tables::kActivityDefinitionCount),
                              middleware::content::packages::tables::kActivityDefinitionCount),
                          0,
                          0,
                          "building installed activity inventory");
                          "building installed activity inventory");
-        bool inventoried = build_inventory(*work, source);
+        // The game normally publishes these layouts during boot. An isolated generation
+        // process has no boot worker, but container and spatial extraction still need them.
+        bool layoutsReady = state::build_data::scenario_layouts_ready();
+        if (work->offline && !layoutsReady) {
+            auto scratch = std::make_unique<package_reader::Scratch>();
+            constexpr std::size_t kMaximumLayoutSteps = 10'000;
+            for (std::size_t step = 0; step < kMaximumLayoutSteps && !cancelled() && !layoutsReady;
+                 ++step) {
+                layoutsReady = scenarios::build(source, *scratch);
+            }
+            package_reader::close_files(*scratch);
+        }
+        bool inventoried = layoutsReady && build_inventory(*work, source);
         if (!inventoried) {
         if (!inventoried) {
-            failureDetail = "inventory_incomplete";
+            failureDetail = layoutsReady ? "inventory_incomplete" : "scenario_layouts_unavailable";
         }
         }
 
 
         const std::uint32_t total = static_cast<std::uint32_t>(work->scenarios.size());
         const std::uint32_t total = static_cast<std::uint32_t>(work->scenarios.size());
@@ -649,9 +1035,10 @@ void service() noexcept {
         return;
         return;
     }
     }
     core::path::Buffer packageDirectory;
     core::path::Buffer packageDirectory;
-    const bool ready =
-        work->module != nullptr && !work->sdkDirectory.empty() && !work->packPath.empty()
-        && items::packages::package_directory(packageDirectory) && prepare_work(*work);
+    const bool ready = work->module != nullptr && !work->sdkDirectory.empty()
+                       && !work->packPath.empty() && state::build_data::scenario_layouts_ready()
+                       && items::packages::package_directory(packageDirectory)
+                       && prepare_work(*work);
     if (ready) {
     if (ready) {
         try {
         try {
             work->packageDirectory.assign(packageDirectory.chars.data(), packageDirectory.length);
             work->packageDirectory.assign(packageDirectory.chars.data(), packageDirectory.length);

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

@@ -319,7 +319,1015 @@ bool commit_mission_state(RuntimeInstance& instance,
     return false;
     return false;
 }
 }
 
 
-namespace {
+/** Binds and restores the durable record before the opened program enters a callback. */
+[[nodiscard]] bool bind_mission_state(RuntimeInstance& instance, std::uint64_t now) noexcept {
+    mission_state::Snapshot snapshot{};
+    mission_state::Status status =
+        mission_state::bind(instance.view.binding, instance.programKey, snapshot);
+    ReloadAuthorization* const authorization = reload_authorization(instance.view.binding);
+    if (status == mission_state::Status::programMismatch && authorization != nullptr) {
+        status = mission_state::rebind_program(
+            instance.view.binding, authorization->program, instance.programKey, snapshot);
+    }
+    if (authorization != nullptr) {
+        *authorization = {};
+    }
+    note_vm_status(instance, "state", mission_state::status_name(status));
+    if (status != mission_state::Status::ready) {
+        log_line(
+            core::log::Level::warn, &instance, "state_bind", mission_state::status_name(status));
+        lua_vm::fault(instance.vm, "authoritative mission State binding was refused");
+        return false;
+    }
+    instance.missionStateBound = true;
+    accept_mission_state(instance, snapshot);
+    if (snapshot.state.variableCount > snapshot.state.variables.size()
+        || snapshot.state.timerCount > snapshot.state.timers.size()) {
+        fault_instance(instance, "authoritative mission State durable row count is invalid");
+        return false;
+    }
+    std::vector<lua_vm::Intent> restoredIntents{};
+    try {
+        restoredIntents.reserve(snapshot.state.pendingIntents.size());
+        for (const mission_state::PendingIntent& pending : snapshot.state.pendingIntents) {
+            restoredIntents.push_back(pending.value);
+        }
+    } catch (const std::bad_alloc&) {
+        fault_instance(instance, "authoritative mission State restore allocation failed");
+        return false;
+    }
+    if (!lua_vm::restore_state(instance.vm,
+                               snapshot.state.phase,
+                               snapshot.state.revision,
+                               {snapshot.state.variables.data(), snapshot.state.variableCount},
+                               {snapshot.state.timers.data(), snapshot.state.timerCount},
+                               snapshot.state.nextTimerSequence,
+                               snapshot.state.nextIntentKey,
+                               restoredIntents)) {
+        fault_instance(instance, "authoritative mission State restore was refused");
+        log_line(core::log::Level::warn, &instance, "state_restore", "vm_refused");
+        return false;
+    }
+    if (instance.durableHostOutputRevision != mission_state::kAbsentHostOutputRevision) {
+        instance.expectedScriptableRevision = instance.durableHostOutputRevision;
+        instance.deliveryStage = DeliveryStage::awaitingHostCommit;
+        instance.deliveryDeadline = deadline_after(now, kHostCommitTimeoutMs);
+        instance.firstIntentAttempt = now;
+        instance.intentAttempts = 1;
+        host::InstanceSnapshot hostView{};
+        if (host::instance_snapshot(instance.view.binding, hostView) && hostView.outputPending
+            && hostView.outputKind == host::OutputKind::scriptableOverride
+            && hostView.scriptableRevision == instance.expectedScriptableRevision) {
+            instance.deliveryStage = DeliveryStage::awaitingTransport;
+            instance.deliveryDeadline = deadline_after(now, kTransportTimeoutMs);
+        } else if (hostView.scriptableRevision == instance.expectedScriptableRevision
+                   && !hostView.outputPending
+                   && hostView.scriptableTransportRevision != instance.expectedScriptableRevision) {
+            instance.deliveryDeadline = now;
+        }
+    }
+    if (snapshot.state.faulted) {
+        lua_vm::fault(instance.vm, "authoritative mission State is faulted");
+        log_line(core::log::Level::warn, &instance, "state_restore", "faulted");
+        return false;
+    }
+    log_line(core::log::Level::debug,
+             &instance,
+             "state_restore",
+             snapshot.state.started ? "started" : "ready");
+    return true;
+}
+
+/**
+ * Finishes one on_load call after same-session VM reattachment.
+ * A restore is not a state transition, so an unchanged candidate must not spend a revision.
+ * @return True when the program is running.
+ */
+[[nodiscard]] bool apply_load(RuntimeInstance& instance, lua_vm::CallStatus loaded) noexcept {
+    lua_vm::Snapshot diagnostics{};
+    lua_vm::snapshot(instance.vm, diagnostics);
+    if (loaded != lua_vm::CallStatus::committed && loaded != lua_vm::CallStatus::noHandler) {
+        instance.programStatus = ProgramStatus::programError;
+        log_line(core::log::Level::warn,
+                 &instance,
+                 "load",
+                 lua_vm::status_name(loaded),
+                 {},
+                 diagnostics.lastError.data());
+        persist_mission_fault(instance);
+        return false;
+    }
+    if (diagnostics.stateRevision != instance.missionStateRevision
+        && !commit_mission_state(instance, true, instance.lastMissionSequence)) {
+        return false;
+    }
+    instance.lastLoggedRevision = instance.missionStateRevision;
+    return true;
+}
+
+enum class InitialStateGate : std::uint8_t {
+    ready,
+    pending,
+    failed,
+};
+
+/** Selects the program-declared state once, then waits for its exact roster revision to publish. */
+[[nodiscard]] InitialStateGate initial_state_gate(RuntimeInstance& instance) noexcept {
+    if (!instance.initialStateDeclared) {
+        return InitialStateGate::ready;
+    }
+    activity_sdk_mission::Snapshot seed{};
+    const activity_sdk_mission::Status status =
+        instance.initialStateSelected ? activity_sdk_mission::query(instance.view, seed)
+                                      : activity_sdk_mission::select_state(
+                                            instance.view, instance.initialStateRegion, {}, seed);
+    if (status == activity_sdk_mission::Status::outputBusy) {
+        return InitialStateGate::pending;
+    }
+    if (status != activity_sdk_mission::Status::ready || !seed.configured
+        || seed.plan.effectiveRegion != static_cast<std::uint32_t>(instance.initialStateRegion)) {
+        log_line(core::log::Level::warn,
+                 &instance,
+                 "initial_state",
+                 activity_sdk_mission::status_name(status));
+        fault_instance(instance, "program initial_state mission-seed selection was refused");
+        return InitialStateGate::failed;
+    }
+    instance.initialStateSelected = true;
+    // `plan.effectiveRegion` is the authored-state key; several authored states share one client
+    // slice-set region, so the client link cannot report it. The lease revision reaching the
+    // transport is the publication acknowledgement.
+    return seed.publicationPending || seed.revision == 0 || seed.publishedRevision != seed.revision
+               ? InitialStateGate::pending
+               : InitialStateGate::ready;
+}
+
+/** Runs and durably commits a fresh program only after its initial-state gate is open. */
+[[nodiscard]] bool start_program(RuntimeInstance& instance, std::uint64_t now) noexcept {
+    const lua_vm::CallStatus started = lua_vm::start(instance.vm, now);
+    note_vm_status(instance, "start", lua_vm::status_name(started));
+    if (started != lua_vm::CallStatus::committed && started != lua_vm::CallStatus::noHandler) {
+        instance.programStatus = ProgramStatus::programError;
+        lua_vm::Snapshot diagnostics{};
+        lua_vm::snapshot(instance.vm, diagnostics);
+        log_line(core::log::Level::warn,
+                 &instance,
+                 "start",
+                 lua_vm::status_name(started),
+                 {},
+                 diagnostics.lastError.data());
+        persist_mission_fault(instance);
+        return false;
+    }
+    if (!commit_mission_state(instance, true, instance.lastMissionSequence)) {
+        return false;
+    }
+    instance.startPending = false;
+    instance.lastLoggedRevision = instance.missionStateRevision;
+    log_line(core::log::Level::info, &instance, "open", "ready");
+    return true;
+}
+
+/** Opens the program for one slot, binds its durable record, then reattaches or starts it. */
+[[nodiscard]] AttachResult open_program(RuntimeInstance& instance, std::uint64_t now) noexcept {
+    const format::Activity* const activity = sdk::bound_activity(instance.view);
+    if (activity == nullptr
+        || !sdk_bridge::program_identity(instance.view, instance.publicTarget, instance.identity)) {
+        instance.programStatus = ProgramStatus::programError;
+        note_vm_status(instance, "open", "invalid_sdk_view");
+        log_line(core::log::Level::warn, &instance, "open", "invalid_sdk_view");
+        return AttachResult::programError;
+    }
+    instance.identity.sdkLuaSearchPath = g_sdkLuaSearchPath;
+    instance.identity.playerKey = instance.playerKey;
+    std::span<const char> source{};
+    switch (read_source(*instance.view.catalog, *activity, source)) {
+    case SourceStatus::missing:
+        instance.programStatus = ProgramStatus::missing;
+        note_vm_status(instance, "open", "no_script");
+        log_line(core::log::Level::info, &instance, "open", "no_script");
+        return AttachResult::noScript;
+    case SourceStatus::fileError:
+        instance.programStatus = ProgramStatus::fileError;
+        note_vm_status(instance, "open", "file_error");
+        log_line(core::log::Level::warn, &instance, "open", "file_error");
+        return AttachResult::scriptFileError;
+    case SourceStatus::tooLarge:
+        instance.programStatus = ProgramStatus::sourceTooLarge;
+        note_vm_status(instance, "open", "source_too_large");
+        log_line(core::log::Level::warn, &instance, "open", "source_too_large");
+        return AttachResult::sourceTooLarge;
+    case SourceStatus::ready:
+        break;
+    }
+    if (!make_program_key(instance, *activity, source, instance.programKey)) {
+        std::fill(g_source.begin(), g_source.begin() + source.size(), '\0');
+        instance.programStatus = ProgramStatus::programError;
+        note_vm_status(instance, "state", "invalid_program_key");
+        log_line(core::log::Level::warn, &instance, "open", "invalid_program_key");
+        return AttachResult::programError;
+    }
+    const lua_vm::OpenStatus opened =
+        lua_vm::open(instance.vm,
+                     instance.identity,
+                     sdk_bridge::definition_api(instance.view, instance.worldView),
+                     source);
+    std::fill(g_source.begin(), g_source.begin() + source.size(), '\0');
+    note_vm_status(instance, "open", lua_vm::status_name(opened));
+    if (opened != lua_vm::OpenStatus::ready) {
+        instance.programStatus = ProgramStatus::programError;
+        lua_vm::Snapshot diagnostics{};
+        lua_vm::snapshot(instance.vm, diagnostics);
+        log_line(core::log::Level::warn,
+                 &instance,
+                 "open",
+                 lua_vm::status_name(opened),
+                 {},
+                 diagnostics.lastError.data());
+        return AttachResult::programError;
+    }
+    instance.programStatus = ProgramStatus::loaded;
+    instance.initialStateDeclared =
+        lua_vm::initial_state_region(instance.vm, instance.initialStateRegion);
+    if (instance.initialStateDeclared) {
+        instance.activeRegion = instance.initialStateRegion;
+    }
+    if (!bind_mission_state(instance, now)) {
+        instance.programStatus = ProgramStatus::programError;
+        return AttachResult::programError;
+    }
+    if (instance.missionStarted) {
+        instance.missionReattached = true;
+        const lua_vm::CallStatus loaded = lua_vm::load(instance.vm, now);
+        note_vm_status(instance, "load", lua_vm::status_name(loaded));
+        if (!apply_load(instance, loaded)) {
+            return AttachResult::programError;
+        }
+        log_line(core::log::Level::info, &instance, "open", "ready", "reason=state_reattached");
+        return AttachResult::ready;
+    }
+    switch (initial_state_gate(instance)) {
+    case InitialStateGate::failed:
+        return AttachResult::programError;
+    case InitialStateGate::pending:
+        instance.startPending = true;
+        log_line(core::log::Level::info, &instance, "initial_state", "publication_pending");
+        return AttachResult::ready;
+    case InitialStateGate::ready:
+        break;
+    }
+    return start_program(instance, now) ? AttachResult::ready : AttachResult::programError;
+}
+
+/** Binds one host instance to a free slot once its link, SDK view and world view all resolve. */
+void attach_instance(const host::InstanceSnapshot& hostInstance,
+                     sdk::Snapshot catalog,
+                     std::uint64_t now) noexcept {
+    if (find_instance(hostInstance.binding) != nullptr) {
+        return;
+    }
+    if (catalog == nullptr) {
+        report_attach_result(
+            hostInstance.binding, AttachResult::catalogUnavailable, "catalog_unavailable");
+        return;
+    }
+    server::bap::ActivityLinkView link{};
+    if (!server::bap::activity_link_view(hostInstance.binding, link)) {
+        report_attach_result(
+            hostInstance.binding, AttachResult::noActivityLink, "no_activity_link");
+        return;
+    }
+    if (!link.joined) {
+        report_attach_result(
+            hostInstance.binding, AttachResult::noActivityLink, "activity_join_pending");
+        return;
+    }
+    sdk::BoundView view{};
+    const sdk::Selection selection{
+        .binding = hostInstance.binding,
+        .matchingLinks = link.matchingLinks,
+        .activityClientGeneration = link.activityClientGeneration,
+    };
+    const sdk::Status status = sdk::resolve(catalog, selection, view);
+    if (status != sdk::Status::ready) {
+        report_attach_result(hostInstance.binding,
+                             AttachResult::sdkStatus,
+                             sdk::status_name(status),
+                             format::kAbsentIndex,
+                             status);
+        return;
+    }
+    generated::GeneratedWorldView worldView{};
+    const generated::BindStatus worldStatus = generated::resolve(view, worldView);
+    if (worldStatus != generated::BindStatus::ready) {
+        report_attach_result(hostInstance.binding,
+                             AttachResult::generatedWorldStatus,
+                             generated::status_name(worldStatus),
+                             view.activityRow,
+                             sdk::Status::notReady,
+                             worldStatus);
+        return;
+    }
+    RuntimeInstance* const instance = free_instance();
+    if (instance == nullptr) {
+        report_attach_result(
+            hostInstance.binding, AttachResult::capacity, "capacity", view.activityRow);
+        return;
+    }
+    instance->view = std::move(view);
+    instance->worldView = std::move(worldView);
+    instance->publicTarget = link.publicTarget;
+    instance->playerKey = link.playerKey;
+    instance->occupied = true;
+    const AttachResult opened = open_program(*instance, now);
+    report_attach_result(
+        hostInstance.binding, opened, attach_result_name(opened), instance->view.activityRow);
+}
+
+/** Drops slots that no longer match, publishes the roster, and attaches active host instances. */
+void synchronize_instances(std::uint64_t now) noexcept {
+    host::DiagnosticsSnapshot diagnostics{};
+    host::snapshot(diagnostics);
+    retire_attach_diagnostics(diagnostics);
+    retire_unbound_pending_events();
+    for (RuntimeInstance& instance : g_instances) {
+        const bool bindingActive =
+            instance.occupied && is_active(diagnostics, instance.view.binding);
+        const bool bindingRetained =
+            instance.occupied && state::activity::binding_matches(instance.view.binding);
+        // A generation change only stales the view, so rebind and keep the program.
+        if (instance.occupied && bindingActive && bindingRetained && !still_exact(instance)
+            && rebind_instance(instance)) {
+            log_line(core::log::Level::debug, &instance, "rebind", "generation");
+            continue;
+        }
+        if (instance.occupied && (!bindingActive || !still_exact(instance))) {
+            log_line(core::log::Level::info, &instance, "close", "stale_generation");
+            // Accepted mission inputs belong to the exact SessionBinding, not one ActivityClient
+            // generation or one temporary link outage. Clear only after State replaces the exact
+            // session generation; otherwise reattach must finish every already-accepted row.
+            clear_instance(instance, !bindingRetained);
+        }
+    }
+    std::array<state::activity::SessionRosterRow, state::activity::kSessionCapacity> roster{};
+    std::size_t rosterCount = 0;
+    static_cast<void>(state::activity::snapshot_session_roster(roster, rosterCount));
+    for (RuntimeInstance& instance : g_instances) {
+        if (instance.occupied) {
+            push_session_roster_edges(instance, {roster.data(), rosterCount});
+        }
+    }
+    const sdk::Snapshot catalog = sdk::snapshot();
+    for (std::size_t index = 0; index < diagnostics.instanceCount; ++index) {
+        if (diagnostics.instances[index].active) {
+            attach_instance(diagnostics.instances[index], catalog, now);
+        }
+    }
+}
+
+/** Advances fresh programs only when their declared state roster has reached transport output. */
+void service_pending_starts(std::uint64_t now) noexcept {
+    for (RuntimeInstance& instance : g_instances) {
+        if (!instance.occupied || !instance.startPending
+            || instance.programStatus != ProgramStatus::loaded || instance.missionStarted) {
+            continue;
+        }
+        switch (initial_state_gate(instance)) {
+        case InitialStateGate::pending:
+            break;
+        case InitialStateGate::failed:
+            instance.startPending = false;
+            break;
+        case InitialStateGate::ready:
+            static_cast<void>(start_program(instance, now));
+            break;
+        }
+    }
+}
+
+/** @return True for the host events that report an output's progress, not an input. */
+[[nodiscard]] bool delivery_lifecycle_event(host::EventKind kind) noexcept {
+    switch (kind) {
+    case host::EventKind::authStateCommitted:
+    case host::EventKind::authStateTransportStaged:
+    case host::EventKind::authStateCanceled:
+    case host::EventKind::incidentQueued:
+    case host::EventKind::incidentTransportStaged:
+    case host::EventKind::incidentCanceled:
+    case host::EventKind::incidentRefused:
+    case host::EventKind::scriptableOverrideCommitted:
+    case host::EventKind::scriptableOverrideTransportStaged:
+    case host::EventKind::scriptableOverrideCanceled:
+    case host::EventKind::operatorRefused:
+        return true;
+    default:
+        return false;
+    }
+}
+
+/**
+ * @return True for a row that arrives on the ordered mission-input feed and owns a sequence.
+ * A host-state row must never answer true. It would consume a mission-input sequence it does not
+ * own, which faults the binding on the next real input.
+ */
+[[nodiscard]] bool host_feed_row(host::EventKind kind) noexcept {
+    switch (kind) {
+    case host::EventKind::timerElapsed:
+    case host::EventKind::effectResult:
+    case host::EventKind::phaseEntered:
+    case host::EventKind::triggerEntered:
+    case host::EventKind::triggerExited:
+    case host::EventKind::squadState:
+    case host::EventKind::entitySpawned:
+    case host::EventKind::entityDied:
+    case host::EventKind::sceneFinished:
+    case host::EventKind::objectiveProgress:
+    case host::EventKind::sessionJoined:
+    case host::EventKind::sessionLeft:
+    case host::EventKind::playerTrigger:
+    case host::EventKind::cinematicStarted:
+    case host::EventKind::cinematicTerminated:
+        return false;
+    default:
+        return true;
+    }
+}
+
+/** True when the event may reach a callback for this instance's ActivityClient generation. */
+[[nodiscard]] bool eligible_event(const RuntimeInstance& instance,
+                                  const host::Event& event) noexcept {
+    if (event.kind == host::EventKind::timerElapsed) {
+        return true;
+    }
+    if (event.sourceGeneration != instance.view.activityClientGeneration) {
+        return false;
+    }
+    return event.kind == host::EventKind::clientStateChanged
+           || event.kind == host::EventKind::incidentReceived
+           || event.kind == host::EventKind::clientMessageReceived
+           || event.kind == host::EventKind::effectResult
+           || event.kind == host::EventKind::phaseEntered
+           || event.kind == host::EventKind::triggerEntered
+           || event.kind == host::EventKind::triggerExited
+           || event.kind == host::EventKind::squadState
+           || event.kind == host::EventKind::entitySpawned
+           || event.kind == host::EventKind::entityDied
+           || event.kind == host::EventKind::sceneFinished
+           || event.kind == host::EventKind::objectiveProgress
+           || event.kind == host::EventKind::entitySlotsRequested
+           || event.kind == host::EventKind::sessionJoined
+           || event.kind == host::EventKind::sessionLeft
+           || event.kind == host::EventKind::playerTrigger
+           || event.kind == host::EventKind::cinematicStarted
+           || event.kind == host::EventKind::cinematicTerminated
+           || delivery_lifecycle_event(event.kind)
+           || (event.kind == host::EventKind::senseUpdate
+               && event.senseDecodeStatus
+                      == middleware::bap::activity_message::sense_update::DecodeStatus::complete);
+}
+
+/** Faults the instance unless the ordered mission input arrives with no gap, starting at one. */
+[[nodiscard]] bool validate_mission_sequence(RuntimeInstance& instance,
+                                             const host::Event& event) noexcept {
+    if (event.kind != host::EventKind::senseUpdate
+        && event.kind != host::EventKind::incidentReceived
+        && event.kind != host::EventKind::clientStateChanged
+        && event.kind != host::EventKind::entitySlotsRequested
+        && event.kind != host::EventKind::clientMessageReceived) {
+        return true;
+    }
+    if (instance.lastMissionSequence == 0) {
+        if (event.missionSequence == 1) {
+            return true;
+        }
+        fault_instance(instance, "activity mission input did not start at sequence one");
+        log_line(core::log::Level::warn, &instance, "events", "initial_binding_gap");
+        return false;
+    }
+    const std::uint64_t expected =
+        instance.lastMissionSequence == (std::numeric_limits<std::uint64_t>::max)()
+            ? 1
+            : instance.lastMissionSequence + 1;
+    if (event.missionSequence == expected) {
+        return true;
+    }
+    fault_instance(instance, "activity mission input sequence has a gap");
+    log_line(core::log::Level::warn, &instance, "events", "binding_gap");
+    return false;
+}
+
+/** Records the typed Ghost-link fields while its interaction semantics are being verified. */
+void log_ghost_link_sense(const RuntimeInstance& instance,
+                          const host::SenseObservationSnapshot& sense) noexcept {
+    for (std::size_t index = 0; index < sense.observationCount; ++index) {
+        const auto& observation = sense.observations[index];
+        if (observation.key.slotType != 65 || observation.key.senseSchema != 0x80804D3EU
+            || observation.firstValue > sense.valueCount
+            || observation.valueCount > sense.valueCount - observation.firstValue) {
+            continue;
+        }
+        for (std::size_t offset = 0; offset < observation.valueCount; ++offset) {
+            const auto& value = sense.values[observation.firstValue + offset];
+            if (!value.present || value.schemaRow != observation.key.schemaRow) {
+                continue;
+            }
+            std::array<char, 192> fields{};
+            const int written = std::snprintf(
+                fields.data(),
+                fields.size(),
+                "registry=%08x object=%08x slot=%u ordinal=%u bits=%u value=%llu sequence=%llu",
+                observation.key.registryKey,
+                observation.key.objectTag,
+                static_cast<unsigned>(observation.key.slotIndex),
+                static_cast<unsigned>(value.fieldOrdinal),
+                static_cast<unsigned>(value.width),
+                static_cast<unsigned long long>(value.unsignedValue),
+                static_cast<unsigned long long>(observation.sequence));
+            if (written > 0) {
+                log_line(core::log::Level::info,
+                         &instance,
+                         "ghost_link_sense",
+                         "observed",
+                         {fields.data(),
+                          (std::min)(static_cast<std::size_t>(written), fields.size() - 1)});
+            }
+        }
+    }
+}
+
+/** Runs one event through the VM, commits what it changed, and faults on a script failure. */
+[[nodiscard]] lua_vm::CallStatus dispatch_event(RuntimeInstance& instance,
+                                                const host::Event& event,
+                                                const host::SenseObservationSnapshot* sense,
+                                                const host::ClientMessageSnapshot* clientMessage,
+                                                bool firstAttempt,
+                                                std::uint64_t now) noexcept {
+    if (instance.programStatus == ProgramStatus::missing
+        && event.kind == host::EventKind::senseUpdate && sense != nullptr) {
+        push_squad_edges(instance, *sense);
+        return lua_vm::CallStatus::inactive;
+    }
+    if (instance.programStatus != ProgramStatus::loaded || !eligible_event(instance, event)) {
+        return lua_vm::CallStatus::inactive;
+    }
+    if (firstAttempt) {
+        ++instance.eventsSeen;
+        instance.lastEventSequence = event.sequence;
+    }
+    // The three region numbers the script is about to read. A report restates only the leg it
+    // moved, so `pending` and `current` read -1 on most reports and `held` is the one that says
+    // where the client is standing.
+    if (firstAttempt && event.kind == host::EventKind::clientStateChanged) {
+        std::array<char, 64> legs{};
+        const int written =
+            std::snprintf(legs.data(),
+                          legs.size(),
+                          "held=%d pending=%d current=%d",
+                          event.heldRegionIndex,
+                          event.clientStateHasRegion ? event.regionIndex : -1,
+                          event.clientStateHasCurrentRegion ? event.currentRegionIndex : -1);
+        if (written > 0) {
+            log_line(core::log::Level::debug,
+                     &instance,
+                     "client_state",
+                     "legs",
+                     {legs.data(), static_cast<std::size_t>(written)});
+        }
+    }
+    const lua_vm::CallStatus status = lua_vm::dispatch(instance.vm, event, clientMessage, now);
+    if (event.kind == host::EventKind::clientStateChanged && event.clientStateHasRegion) {
+        instance.activeRegion = event.regionIndex;
+    }
+    if (firstAttempt && event.kind == host::EventKind::incidentReceived) {
+        push_player_trigger(instance, event);
+        push_cinematic(instance, event);
+    }
+    if (event.kind == host::EventKind::senseUpdate && sense != nullptr) {
+        if (firstAttempt) {
+            log_ghost_link_sense(instance, *sense);
+        }
+        push_trigger_edges(instance, *sense);
+        push_squad_edges(instance, *sense);
+        push_scene_edges(instance, *sense);
+        push_objective_edges(instance, *sense);
+    }
+    note_vm_status(instance, "event", lua_vm::status_name(status));
+    if (firstAttempt && instance.eventsSeen == 1) {
+        log_line(core::log::Level::info,
+                 &instance,
+                 "dispatch",
+                 lua_vm::status_name(status),
+                 event.kind == host::EventKind::senseUpdate            ? "sense"
+                 : event.kind == host::EventKind::clientStateChanged   ? "client_state"
+                 : event.kind == host::EventKind::incidentReceived     ? "incident"
+                 : event.kind == host::EventKind::entitySlotsRequested ? "entity_slots_requested"
+                 : event.kind == host::EventKind::timerElapsed         ? "timer"
+                 : event.kind == host::EventKind::effectResult         ? "effect_result"
+                                                                       : "client_message");
+    }
+    const std::uint64_t nextInputSequence =
+        host_feed_row(event.kind) ? event.missionSequence : instance.lastMissionSequence;
+    if (status == lua_vm::CallStatus::committed) {
+        if (!commit_mission_state(instance, true, nextInputSequence)) {
+            return lua_vm::CallStatus::scriptError;
+        }
+        ++instance.eventsCommitted;
+        lua_vm::Snapshot diagnostics{};
+        lua_vm::snapshot(instance.vm, diagnostics);
+        if (event.kind == host::EventKind::clientStateChanged
+            || diagnostics.stateRevision != instance.lastLoggedRevision) {
+            instance.lastLoggedRevision = diagnostics.stateRevision;
+            log_line(core::log::Level::debug,
+                     &instance,
+                     "event",
+                     "committed",
+                     event.kind == host::EventKind::senseUpdate          ? "sense"
+                     : event.kind == host::EventKind::clientStateChanged ? "client_state"
+                     : event.kind == host::EventKind::incidentReceived   ? "incident"
+                     : event.kind == host::EventKind::entitySlotsRequested
+                         ? "entity_slots_requested"
+                     : event.kind == host::EventKind::timerElapsed ? "timer"
+                     : event.kind == host::EventKind::effectResult ? "effect_result"
+                                                                   : "client_message");
+        }
+        return status;
+    }
+    if (status == lua_vm::CallStatus::noHandler) {
+        return commit_mission_state(instance, true, nextInputSequence)
+                   ? status
+                   : lua_vm::CallStatus::scriptError;
+    }
+    if (status == lua_vm::CallStatus::inactive) {
+        return status;
+    }
+    lua_vm::Snapshot diagnostics{};
+    lua_vm::snapshot(instance.vm, diagnostics);
+    log_line(core::log::Level::warn,
+             &instance,
+             "event",
+             lua_vm::status_name(status),
+             {},
+             diagnostics.lastError.data());
+    persist_mission_fault(instance);
+    return status;
+}
+
+/** Reads new host events, advances delivery, and queues only the lifecycle rows for scripts. */
+void consume_delivery_events(std::uint64_t now) noexcept {
+    host::EventRead events{};
+    host::read_events_after(g_eventCursor, events);
+    g_eventCursor = events.cursor;
+    if (events.reset) {
+        log_line(core::log::Level::warn, nullptr, "delivery", "event_feed_reset");
+        return;
+    }
+    if (events.gap) {
+        log_line(core::log::Level::warn, nullptr, "delivery", "event_feed_gap");
+    }
+    for (std::size_t index = 0; index < events.count; ++index) {
+        RuntimeInstance* const instance = find_instance(events.events[index].binding);
+        if (instance != nullptr) {
+            observe_delivery_event(*instance, events.events[index], now);
+            // Sense, client-state, incident and client-message rows reach the script through the
+            // ordered mission-input feed. Only the delivery lifecycle rows belong in this queue.
+            if (delivery_lifecycle_event(events.events[index].kind)) {
+                push_script_event(*instance, events.events[index]);
+            }
+        }
+    }
+}
+
+/** One free queue row, growing the queue by one when none is free; null when it cannot grow. */
+[[nodiscard]] PendingMissionEvent* free_pending_event() noexcept {
+    for (PendingMissionEvent& pending : g_pendingMissionEvents) {
+        if (!pending.occupied) {
+            return &pending;
+        }
+    }
+    if (g_pendingMissionEvents.size() == g_pendingMissionEvents.max_size()) {
+        return nullptr;
+    }
+    try {
+        g_pendingMissionEvents.emplace_back();
+    } catch (const std::bad_alloc&) {
+        return nullptr;
+    }
+    return &g_pendingMissionEvents.back();
+}
+
+/** Copies one accepted input and its values into a queue row; faults the instance when full. */
+[[nodiscard]] bool queue_mission_event(RuntimeInstance* instance,
+                                       const host::MissionInputEvent& input) noexcept {
+    PendingMissionEvent* const pending = free_pending_event();
+    if (pending == nullptr) {
+        if (instance != nullptr) {
+            fault_instance(*instance, "mission event queue allocation failed");
+            clear_pending_events(instance->view.binding);
+        }
+        log_line(core::log::Level::warn, instance, "events", "allocation_failed");
+        return false;
+    }
+    clear_pending_event(*pending);
+    if (input.event.kind == host::EventKind::senseUpdate
+        && input.event.senseDecodeStatus
+               == middleware::bap::activity_message::sense_update::DecodeStatus::complete) {
+        pending->senseAvailable =
+            host::mission_input_sense_snapshot(input.sequence, pending->sense);
+    }
+    if (input.event.kind == host::EventKind::clientMessageReceived) {
+        pending->clientMessageAvailable =
+            host::mission_input_client_message_snapshot(input.sequence, pending->clientMessage);
+    }
+    pending->nextAttempt = 0;
+    pending->event = input.event;
+    if (instance != nullptr) {
+        pending->callbackEligible = eligible_event(*instance, input.event);
+        pending->eligibilityResolved = true;
+    }
+    pending->occupied = true;
+    return true;
+}
+
+[[nodiscard]] constexpr bool mission_sequence_precedes(std::uint64_t left,
+                                                       std::uint64_t right) noexcept {
+    constexpr std::uint64_t halfRange = std::uint64_t{1} << 63U;
+    return left != right && right - left < halfRange;
+}
+
+static_assert(mission_sequence_precedes((std::numeric_limits<std::uint64_t>::max)(), 1));
+static_assert(!mission_sequence_precedes(1, (std::numeric_limits<std::uint64_t>::max)()));
+
+/** @return True when the durable cursor already committed this retained input row. */
+[[nodiscard]] bool mission_sequence_committed(std::uint64_t sequence,
+                                              std::uint64_t committed) noexcept {
+    return committed != 0
+           && (sequence == committed || mission_sequence_precedes(sequence, committed));
+}
+
+/** True when the same binding still holds a queued row with an earlier mission sequence. */
+[[nodiscard]] bool has_earlier_pending_event(const PendingMissionEvent& selected) noexcept {
+    for (const PendingMissionEvent& pending : g_pendingMissionEvents) {
+        if (pending.occupied && &pending != &selected
+            && same_binding(pending.event.binding, selected.event.binding)
+            && mission_sequence_precedes(pending.event.missionSequence,
+                                         selected.event.missionSequence)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/** TODO: no caller. Decide whether the drain gate retires on this or on `binding_matches`. */
+[[nodiscard]] bool host_binding_active(const state::activity::SessionBinding& binding) noexcept {
+    host::InstanceSnapshot snapshot{};
+    return host::instance_snapshot(binding, snapshot) && snapshot.active;
+}
+
+/** Dispatches queued rows in mission-sequence order and retires those no callback can take. */
+void drain_pending_mission_events(std::uint64_t now) noexcept {
+    bool progressed = false;
+    do {
+        progressed = false;
+        for (PendingMissionEvent& pending : g_pendingMissionEvents) {
+            if (!pending.occupied || now < pending.nextAttempt
+                || has_earlier_pending_event(pending)) {
+                continue;
+            }
+            RuntimeInstance* const instance = find_instance(pending.event.binding);
+            if (instance == nullptr) {
+                if (!state::activity::binding_matches(pending.event.binding)) {
+                    clear_pending_event(pending);
+                    progressed = true;
+                }
+                continue;
+            }
+            if (instance->programStatus != ProgramStatus::loaded) {
+                clear_pending_event(pending);
+                progressed = true;
+                continue;
+            }
+            if (instance->startPending) {
+                continue;
+            }
+            if (instance->timerPending) {
+                continue;
+            }
+            if (mission_sequence_committed(pending.event.missionSequence,
+                                           instance->lastMissionSequence)) {
+                clear_pending_event(pending);
+                progressed = true;
+                continue;
+            }
+            if (!pending.missionSequenceObserved) {
+                if (!validate_mission_sequence(*instance, pending.event)) {
+                    clear_pending_events(instance->view.binding);
+                    progressed = true;
+                    break;
+                }
+                pending.missionSequenceObserved = true;
+            }
+            if (!pending.eligibilityResolved) {
+                pending.callbackEligible = eligible_event(*instance, pending.event);
+                pending.eligibilityResolved = true;
+            }
+            if (!pending.callbackEligible) {
+                if (!commit_mission_state(
+                        *instance, instance->missionStarted, pending.event.missionSequence)) {
+                    clear_pending_events(instance->view.binding);
+                    progressed = true;
+                    break;
+                }
+                clear_pending_event(pending);
+                progressed = true;
+                continue;
+            }
+            if (pending.event.kind == host::EventKind::senseUpdate && !pending.senseAvailable) {
+                fault_instance(*instance, "accepted mission Sense values were unavailable");
+                clear_pending_events(instance->view.binding);
+                log_line(core::log::Level::warn, instance, "events", "sense_unavailable");
+                progressed = true;
+                break;
+            }
+            if (pending.event.kind == host::EventKind::clientMessageReceived
+                && !pending.clientMessageAvailable) {
+                fault_instance(*instance,
+                               "accepted mission client-message values were unavailable");
+                clear_pending_events(instance->view.binding);
+                log_line(core::log::Level::warn, instance, "events", "client_message_unavailable");
+                progressed = true;
+                break;
+            }
+            lua_vm::Intent intent{};
+            if (instance->deliveryStage != DeliveryStage::idle
+                || lua_vm::pending_intent(instance->vm, intent)) {
+                continue;
+            }
+            const bool firstAttempt = pending.attempts == 0;
+            if (firstAttempt) {
+                pending.firstAttempt = now;
+            }
+            ++pending.attempts;
+            const host::SenseObservationSnapshot* const sense =
+                pending.event.kind == host::EventKind::senseUpdate && pending.senseAvailable
+                    ? &pending.sense
+                    : nullptr;
+            const host::ClientMessageSnapshot* const clientMessage =
+                pending.event.kind == host::EventKind::clientMessageReceived
+                        && pending.clientMessageAvailable
+                    ? &pending.clientMessage
+                    : nullptr;
+            static_cast<void>(
+                dispatch_event(*instance, pending.event, sense, clientMessage, firstAttempt, now));
+            clear_pending_event(pending);
+            progressed = true;
+        }
+    } while (progressed);
+}
+
+/** @return True when one exact accepted sequence is already retained locally or in this read. */
+[[nodiscard]] bool input_sequence_retained(const state::activity::SessionBinding& binding,
+                                           std::uint64_t sequence,
+                                           const host::MissionInputRead& inputs) noexcept {
+    for (const PendingMissionEvent& pending : g_pendingMissionEvents) {
+        if (pending.occupied && same_binding(pending.event.binding, binding)
+            && pending.event.missionSequence == sequence) {
+            return true;
+        }
+    }
+    for (std::size_t index = 0; index < inputs.count; ++index) {
+        if (same_binding(inputs.events[index].event.binding, binding)
+            && inputs.events[index].event.missionSequence == sequence) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/** @return True when every accepted but uncommitted sequence is present in this read or local
+ * queue. */
+[[nodiscard]] bool
+outstanding_input_interval_complete(const state::activity::SessionBinding& binding,
+                                    const mission_state::InputSequenceSnapshot& state,
+                                    const host::MissionInputRead& inputs) noexcept {
+    if (state.issued < state.committed) {
+        return false;
+    }
+    const std::uint64_t outstanding = state.issued - state.committed;
+    if (outstanding > g_pendingMissionEvents.size() + inputs.count) {
+        return false;
+    }
+    std::uint64_t sequence = state.committed;
+    for (std::uint64_t index = 0; index < outstanding; ++index) {
+        ++sequence;
+        if (!input_sequence_retained(binding, sequence, inputs)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Faults only retained bindings whose durable uncommitted interval is provably incomplete. */
+void reconcile_input_feed_loss(const host::MissionInputRead& inputs) noexcept {
+    host::DiagnosticsSnapshot hostState{};
+    host::snapshot(hostState);
+    for (std::size_t index = 0; index < hostState.instanceCount; ++index) {
+        const host::InstanceSnapshot& hostInstance = hostState.instances[index];
+        if (!state::activity::binding_matches(hostInstance.binding)) {
+            continue;
+        }
+        mission_state::InputSequenceSnapshot inputState{};
+        if (!mission_state::input_sequence_snapshot(hostInstance.binding, inputState)
+            || inputState.faulted
+            || outstanding_input_interval_complete(hostInstance.binding, inputState, inputs)) {
+            continue;
+        }
+        mission_state::Snapshot snapshot{};
+        const mission_state::Status status =
+            mission_state::fault_input_feed(hostInstance.binding, snapshot);
+        RuntimeInstance* const instance = find_instance(hostInstance.binding);
+        if (status != mission_state::Status::ready) {
+            log_line(core::log::Level::warn,
+                     instance,
+                     "events",
+                     mission_state::status_name(status),
+                     "reason=feed_gap_fault_refused");
+            continue;
+        }
+        if (instance != nullptr) {
+            accept_mission_state(*instance, snapshot);
+            lua_vm::fault(instance->vm, "accepted mission input feed lost a row");
+            instance->programStatus = ProgramStatus::programError;
+        }
+        log_line(core::log::Level::warn, instance, "events", "feed_gap_faulted");
+        clear_pending_events(hostInstance.binding);
+    }
+}
+
+/** Reads one page of the ordered feed and queues every row not yet committed. */
+[[nodiscard]] bool consume_mission_input_page() noexcept {
+    host::MissionInputRead inputs{};
+    host::read_mission_inputs_after(g_missionInputCursor, inputs);
+    if (inputs.reset) {
+        g_missionInputCursor = {inputs.cursor.generation, 0};
+        reconcile_input_feed_loss(inputs);
+        log_line(core::log::Level::warn, nullptr, "events", "mission_feed_reset");
+    }
+    if (inputs.gap) {
+        log_line(core::log::Level::warn, nullptr, "events", "mission_feed_gap");
+        reconcile_input_feed_loss(inputs);
+    }
+    for (std::size_t index = 0; index < inputs.count; ++index) {
+        const host::MissionInputEvent& input = inputs.events[index];
+        RuntimeInstance* const instance = find_instance(input.event.binding);
+        mission_state::InputSequenceSnapshot inputState{};
+        const bool hasInputState =
+            mission_state::input_sequence_snapshot(input.event.binding, inputState);
+        if ((instance == nullptr && !state::activity::binding_matches(input.event.binding))
+            || (instance != nullptr && instance->programStatus != ProgramStatus::loaded)
+            || (hasInputState
+                && (inputState.faulted
+                    || mission_sequence_committed(input.event.missionSequence,
+                                                  inputState.committed)))) {
+            g_missionInputCursor.generation = inputs.cursor.generation;
+            g_missionInputCursor.sequence = input.sequence;
+            continue;
+        }
+        if (instance != nullptr
+            && mission_sequence_committed(input.event.missionSequence,
+                                          instance->lastMissionSequence)) {
+            g_missionInputCursor.generation = inputs.cursor.generation;
+            g_missionInputCursor.sequence = input.sequence;
+            continue;
+        }
+        if (instance != nullptr) {
+            lua_vm::Snapshot diagnostics{};
+            lua_vm::snapshot(instance->vm, diagnostics);
+            if (diagnostics.faulted) {
+                g_missionInputCursor.generation = inputs.cursor.generation;
+                g_missionInputCursor.sequence = input.sequence;
+                continue;
+            }
+        }
+        if (!queue_mission_event(instance, input)) {
+            return false;
+        }
+        g_missionInputCursor.generation = inputs.cursor.generation;
+        g_missionInputCursor.sequence = input.sequence;
+    }
+    return inputs.count == host::kMissionInputReadPageSize;
+}
+
+/**
+ * Reads the ordered mission-input feed and drains the queue. The feed is unbounded and one read
+ * copies at most a page, so a burst is consumed in the tick it arrives instead of a page a tick.
+ */
+void consume_mission_inputs(std::uint64_t now) noexcept {
+    drain_pending_mission_events(now);
+    while (consume_mission_input_page()) {
+        drain_pending_mission_events(now);
+    }
+    drain_pending_mission_events(now);
+}
 
 
 /**
 /**
  * Settles the inputs of every instance whose activity has no script. No program will consume
  * Settles the inputs of every instance whose activity has no script. No program will consume

+ 27 - 38
scripts/README.md

@@ -1,35 +1,32 @@
 # Ember development scripts
 # Ember development scripts
 
 
-`mission_ember.lua` currently implements only the opening landing combat slice. It is not a
-complete mission controller. The initial six-squad selection needs an in-game playtest. Bridge
-interaction, later encounters, authored checkpoints, dialogue and mission completion remain to
-be implemented.
-
-The original installed SDK inspected on 2026-09-04 lacks the opening squad definitions. This controller
-intentionally reports the missing binding before publishing any gameplay state. Build this branch
-and regenerate the SDK first; the scenario-scoping fix in the squad linker addresses object-key
-collisions between campaign and arcade content. Generation must still verify runnable member
-counts, actor bindings and anchors. The isolated SDK generated in `build/sdk-generated` now passes
-these checks for the initial six opening squads. Do not remove checks or supply replacement enemies.
-
-After regeneration, copy this directory's contents to `Sunrise/scripts` beside the generated
-`Sunrise/sdk/lua` directory in the game artifact tree. The runtime selects `mission_ember.lua`
-by the activity name. Enable the existing `server.activation.mission_scripting` setting.
-
-The controller declares region 64 at startup and waits for the client to hold it before spawning enemies. Enemy clear
-requires evidence that every watched squad existed and now has no live members. Duplicate clear
-events do not advance twice, and removed squads must be observed alive again. Region transit and
-script reload do not reset the encounter. `Encounter:reset` is available for a future confirmed
-checkpoint callback; this version does not infer a wipe from region transit.
-
-Fresh launches declare the opening powerhouse state through `initial_state`. The installed
-`state.activity.arrival_overrides` must also include `mission_ember` with `bubble: 8` and
-`slice_set: 64`; the arrival router otherwise chooses the first live bubble (the reactor area).
-The branch's bundled defaults include this override. Existing settings need the row added
-explicitly. Restart the game after changing arrival settings.
-
-The combat-clear callback displays the authored bridge-control directive. It does not move the
-bridge, complete the activity or silently proceed through unimplemented encounters.
+This controller is a development playtest slice, not complete 1AU. It selects the authored
+arrival-cinematic state (49), activates its bookend when held, and hands off to powerhouse
+state 64 on the matching termination event. The supplied reference video starts in gameplay;
+it cannot establish the arrival movie’s identity.
+
+The landing encounter uses eleven authored Mercury squads with their default counts.
+Bridge devices reset only when the playable region is held. Arrival dialogue waits for spawn
+settlement; combat clear sets the console directive and plays the console-guidance cue.
+Actual Ghost-console activation/bridge extension, later encounters, later dialogue, wipes and
+mission completion still need implementation and in-game verification. See
+[the full squad audit](../MISSION_EMBER_SQUADS.md) and
+[video observations](../MISSION_EMBER_REFERENCE.md).
+
+Build this branch and regenerate the SDK before installing these scripts. The corrected
+native generator initializes the scenario catalogue before offline extraction and rejects
+cached shards missing container/spatial context. `build/sdk-corrected` has 138 statically
+runnable Ember definitions; 13 sensors have no definition and 11 processing sensors have
+refused alternate spawn-rule bindings. Never substitute arbitrary enemies or coordinates.
+
+Copy these scripts to `Sunrise/scripts` beside `Sunrise/sdk/lua`. Enable
+`server.activation.mission_scripting`. For cinematic arrival, the settings override for
+`mission_ember` must use `bubble: 6` and `slice_set: 49`; gameplay is selected by the script
+after the cinematic. Restart after updating arrival settings.
+
+Region-less client deltas preserve encounter ownership. Duplicate events and script reload
+do not repeat the movie, spawn requests, dialogue or bridge reset. Mock-context tests verify
+those conditions; they do not establish visible AI, dialogue or cinematic playback.
 
 
 Run local checks from the repository root:
 Run local checks from the repository root:
 
 
@@ -54,11 +51,3 @@ python tools/inspect_mission_sdk.py /path/to/Sunrise/activity_sdk.pack \
     --shard /path/to/Sunrise/sdk/scenarios/80B3C09E-HASH.pack \
     --shard /path/to/Sunrise/sdk/scenarios/80B3C09E-HASH.pack \
     --output /tmp/mission-ember-inventory.json
     --output /tmp/mission-ember-inventory.json
 ```
 ```
-
-The current landing test places ten SDK squads (the original six and the four available
-bonus-support squads), using their authored default counts. Bonus-wave timing remains unverified;
-the missing bonus-anchor and far-side definitions are not substituted. Startup snaps the six
-bridge devices closed and resets the bridge objective after the initial state is published.
-The region callback no longer redundantly selects that state. Bridge interaction and extension
-still need scripting; this test checks its initial state only. Server/client info logging is
-installed to expose refused spawn and device requests on the next fresh launch.

+ 9 - 10
scripts/mission_ember.lua

@@ -2,21 +2,20 @@
 local missions = require("missions")
 local missions = require("missions")
 local mission = require(assert(missions.MISSION_EMBER, "mission_ember SDK module is absent"))
 local mission = require(assert(missions.MISSION_EMBER, "mission_ember SDK module is absent"))
 local landing = require("mission_ember.landing")(mission)
 local landing = require("mission_ember.landing")(mission)
+local opening = require("mission_ember.opening")(mission, landing)
 
 
 return {
 return {
-    initial_state = landing.initial_state,
-    on_start = function(context, state)
-        landing.initialize(context)
-    end,
+    initial_state = opening.initial_state,
+    on_event_cinematic_terminated = opening.terminated,
     on_event_client_state_changed = function(context, state, event)
     on_event_client_state_changed = function(context, state, event)
-        local region = event.current_region_index
-        if region == nil then
-            context:clear_variable("ember.region")
-            return
-        end
-        context:set_variable("ember.region", region)
+        opening.client_state(context, state, event)
+        local region = event.held_region_index or event.current_region_index
+        -- A settle-only delta omits region fields; it does not mean that the player left.
+        if region ~= nil then context:set_variable("ember.region", region) end
+        region = state:variable("ember.region")
         if region == landing.region then
         if region == landing.region then
             landing.enter(context, state)
             landing.enter(context, state)
+            landing.client_state(context, state, event)
         end
         end
     end,
     end,
     on_event_squad_state = function(context, state, event)
     on_event_squad_state = function(context, state, event)

+ 25 - 10
scripts/mission_ember/landing.lua

@@ -3,6 +3,7 @@ local Encounter = require("mission_ember.encounter")
 
 
 local NAMES = {
 local NAMES = {
     "LANDING_MERCURY_ANCHOR_SQUAD",
     "LANDING_MERCURY_ANCHOR_SQUAD",
+    "LANDING_MERCURY_BONUS_ANCHOR_A_SQUAD",
     "LANDING_MERCURY_SUPPORT_A_SQUAD",
     "LANDING_MERCURY_SUPPORT_A_SQUAD",
     "LANDING_MERCURY_SUPPORT_B_SQUAD",
     "LANDING_MERCURY_SUPPORT_B_SQUAD",
     "LANDING_MERCURY_SUPPORT_C_SQUAD",
     "LANDING_MERCURY_SUPPORT_C_SQUAD",
@@ -52,29 +53,43 @@ return function(mission)
                               "bridge controls directive")
                               "bridge controls directive")
     local encounter = Encounter.new("ember.landing", squads, function(context)
     local encounter = Encounter.new("ember.landing", squads, function(context)
         context:slot(directive):set_directive{directive = controls}
         context:slot(directive):set_directive{directive = controls}
+        context:slot(mission.Slot.M_DIALOG_SENSOR_80B3C90A):play_dialogue_cue{
+            cue = mission.DialogueCue.M_DIALOG_SENSOR_80B3C90A.CUE_3,
+        }
+        context:set_phase(3)
         -- The bridge must wait for its authored interaction. Combat clear does not open it.
         -- The bridge must wait for its authored interaction. Combat clear does not open it.
     end)
     end)
 
 
+    local function initialize(context)
+        for _, device in ipairs(bridge) do
+            context:slot(device):transition{transition = context.sdk.device_transitions.close, snap = true}
+        end
+        context:slot(bridge_objective):reset_objectives{}
+    end
+
     return {
     return {
         initial_state = entry,
         initial_state = entry,
         region = entry.region_index,
         region = entry.region_index,
-        initialize = function(context)
-            -- on_start runs after the initial-state roster has reached the transport.
-            -- Do not repeat this on region transit or on_load: those are not mission resets.
-            for _, device in ipairs(bridge) do
-                context:slot(device):transition{
-                    transition = context.sdk.device_transitions.close, snap = true,
-                }
-            end
-            context:slot(bridge_objective):reset_objectives{}
-        end,
         enter = function(context, state)
         enter = function(context, state)
             if encounter:phase(state) ~= 0 then
             if encounter:phase(state) ~= 0 then
                 return
                 return
             end
             end
+            initialize(context)
+            context:set_phase(2)
             encounter:enter(context, state)
             encounter:enter(context, state)
             context:slot(directive):set_directive{directive = clear}
             context:slot(directive):set_directive{directive = clear}
         end,
         end,
+        client_state = function(context, state, event)
+            if event.teleport_state == 0 then context:set_variable("ember.spawned", true) end
+            if state:variable("ember.spawned") and not state:variable("ember.guidance")
+                and event.region_index == nil and event.current_region_index == nil
+                and event.spawn_state == nil and event.teleport_state == nil then
+                context:set_variable("ember.guidance", true)
+                context:slot(mission.Slot.M_DIALOG_SENSOR_80B3C90A):play_dialogue_cue{
+                    cue = mission.DialogueCue.M_DIALOG_SENSOR_80B3C90A.CUE_0,
+                }
+            end
+        end,
         on_squad_state = function(context, state, event)
         on_squad_state = function(context, state, event)
             encounter:on_squad_state(context, state, event)
             encounter:on_squad_state(context, state, event)
         end,
         end,

+ 32 - 0
scripts/mission_ember/opening.lua

@@ -0,0 +1,32 @@
+-- Arrival cinematic and control handoff. The cinematic owns a separate authored state.
+return function(mission, landing)
+    local entry = assert(mission.states.STATE_80B3C09E_0006_0001_80B3C09A)
+    local cinematic = assert(mission.Slot.PF_CINEMATIC_BOOKEND_CINEMATIC)
+    local function phase(state) return state:variable("ember.cinematic") or 0 end
+    local function finish(context, state)
+        if phase(state) ~= 1 then return end
+        context:set_variable("ember.cinematic", 2)
+        context:slot(cinematic):set_cinematic_active{active = false}
+        context:select_state(landing.initial_state)
+        context:set_phase(2)
+    end
+    return {
+        initial_state = entry,
+        client_state = function(context, state, event)
+            local held = event.held_region_index or event.current_region_index
+            -- Only a held-region report starts the movie; a requested destination is insufficient.
+            if held == entry.region_index and phase(state) == 0 then
+                context:set_variable("ember.cinematic", 1)
+                context:set_phase(1)
+                context:slot(cinematic):set_cinematic_active{active = true}
+            end
+        end,
+        terminated = function(context, state, event)
+            if phase(state) ~= 1 then return end
+            local slot = context:slot(cinematic)
+            if event.registry_key ~= slot.registry_key or event.slot_type ~= slot.slot_type
+                or event.slot_index ~= slot.slot_index then return end
+            finish(context, state)
+        end,
+    }
+end

+ 41 - 15
tests/mission_ember_controller_test.lua

@@ -1,6 +1,7 @@
 package.path = "scripts/?.lua;" .. package.path
 package.path = "scripts/?.lua;" .. package.path
 local names = {
 local names = {
-    "LANDING_MERCURY_ANCHOR_SQUAD", "LANDING_MERCURY_SUPPORT_A_SQUAD",
+    "LANDING_MERCURY_ANCHOR_SQUAD",
+    "LANDING_MERCURY_BONUS_ANCHOR_A_SQUAD", "LANDING_MERCURY_SUPPORT_A_SQUAD",
     "LANDING_MERCURY_SUPPORT_B_SQUAD", "LANDING_MERCURY_SUPPORT_C_SQUAD",
     "LANDING_MERCURY_SUPPORT_B_SQUAD", "LANDING_MERCURY_SUPPORT_C_SQUAD",
     "LANDING_MERCURY_RANGED_A_SQUAD", "LANDING_MERCURY_RANGED_B_SQUAD",
     "LANDING_MERCURY_RANGED_A_SQUAD", "LANDING_MERCURY_RANGED_B_SQUAD",
     "LANDING_MERCURY_BONUS_SUPPORT_A_SQUAD", "LANDING_MERCURY_BONUS_SUPPORT_B_SQUAD",
     "LANDING_MERCURY_BONUS_SUPPORT_A_SQUAD", "LANDING_MERCURY_BONUS_SUPPORT_B_SQUAD",
@@ -8,7 +9,11 @@ local names = {
 }
 }
 local mission = {
 local mission = {
     Squad = {}, Slot = {M_DIRECTIVE_SENSOR_80B3C90A = "directive"},
     Squad = {}, Slot = {M_DIRECTIVE_SENSOR_80B3C90A = "directive"},
-    states = {STATE_80B3C09E_0008_0000_80B3C09C = {region_index = 64}},
+    states = {
+        STATE_80B3C09E_0008_0000_80B3C09C = {region_index = 64},
+        STATE_80B3C09E_0006_0001_80B3C09A = {region_index = 49},
+    },
+    DialogueCue = {M_DIALOG_SENSOR_80B3C90A = {CUE_0 = 0, CUE_3 = 3}},
     Directive = {
     Directive = {
         FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_A70DA4A6 = "clear",
         FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_A70DA4A6 = "clear",
         FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_2700C0C5 = "controls",
         FIND_AND_DISABLE_THE_ALMIGHTY_S_WEAPONS_2700C0C5 = "controls",
@@ -25,15 +30,18 @@ local devices = {
 }
 }
 for _, name in ipairs(devices) do mission.Slot[name] = name end
 for _, name in ipairs(devices) do mission.Slot[name] = name end
 mission.Slot.EMBER_POWERHOUSE_BRIDGE_OBJECTIVE = "bridge_objective"
 mission.Slot.EMBER_POWERHOUSE_BRIDGE_OBJECTIVE = "bridge_objective"
+mission.Slot.PF_CINEMATIC_BOOKEND_CINEMATIC = "cinematic"
+mission.Slot.M_DIALOG_SENSOR_80B3C90A = "dialogue"
 package.preload.missions = function() return {MISSION_EMBER = "test_mission"} end
 package.preload.missions = function() return {MISSION_EMBER = "test_mission"} end
 package.preload.test_mission = function() return mission end
 package.preload.test_mission = function() return mission end
 local program = require("mission_ember")
 local program = require("mission_ember")
-assert(program.initial_state == mission.states.STATE_80B3C09E_0008_0000_80B3C09C,
-       "fresh launches must declare the opening powerhouse state")
+assert(program.initial_state == mission.states.STATE_80B3C09E_0006_0001_80B3C09A,
+       "fresh launches must declare the authored cinematic state")
 local vars, directives, placements, seeds = {}, {}, 0, 0
 local vars, directives, placements, seeds = {}, {}, 0, 0
 local state = {variable = function(_, key) return vars[key] end}
 local state = {variable = function(_, key) return vars[key] end}
-local closed, resets = {}, 0
+local closed, resets, movies, cues = {}, 0, {}, {}
 local context = {sdk = {squad_modes = {replace = "replace"}, device_transitions = {close = "close"}}}
 local context = {sdk = {squad_modes = {replace = "replace"}, device_transitions = {close = "close"}}}
+function context:set_phase(value) vars.phase = value end
 function context:set_variable(key, value) vars[key] = value end
 function context:set_variable(key, value) vars[key] = value end
 function context:clear_variable(key) vars[key] = nil end
 function context:clear_variable(key) vars[key] = nil end
 function context:select_state(entry)
 function context:select_state(entry)
@@ -45,6 +53,13 @@ function context:squad(name)
     return {default_counts = {1}, place = function() placements = placements + 1 end}
     return {default_counts = {1}, place = function() placements = placements + 1 end}
 end
 end
 function context:slot(name)
 function context:slot(name)
+    if name == "cinematic" then
+        return {registry_key = 9, slot_type = 6, slot_index = 0,
+            set_cinematic_active = function(_, args) movies[#movies + 1] = args.active end}
+    end
+    if name == "dialogue" then
+        return {play_dialogue_cue = function(_, args) cues[#cues + 1] = args.cue end}
+    end
     for _, device in ipairs(devices) do
     for _, device in ipairs(devices) do
         if name == device then
         if name == device then
             return {transition = function(_, args)
             return {transition = function(_, args)
@@ -75,21 +90,32 @@ local function cleared()
         })
         })
     end
     end
 end
 end
-program.on_start(context, state)
-assert(#closed == 6 and resets == 1 and placements == 0)
+assert(#closed == 0 and resets == 0 and placements == 0)
+program.on_event_client_state_changed(context, state, {region_index = 49})
+assert(#movies == 0, "a requested region is not a held cinematic")
+region(49); region(49)
+assert(#movies == 1 and movies[1] == true and placements == 0)
+program.on_event_cinematic_terminated(context, state, {registry_key = 8, slot_type = 6, slot_index = 0})
+assert(seeds == 0, "unrelated movie termination cannot advance the mission")
+local termination = {registry_key = 9, slot_type = 6, slot_index = 0}
+program.on_event_cinematic_terminated(context, state, termination)
+program.on_event_cinematic_terminated(context, state, termination)
+assert(seeds == 1 and #movies == 2 and movies[2] == false)
 region(40)
 region(40)
-assert(seeds == 0 and placements == 0)
+assert(placements == 0 and #closed == 0)
 region(64); region(64)
 region(64); region(64)
-assert(seeds == 0 and placements == #names and directives[1] == "clear")
-region(nil); cleared()
-assert(#directives == 1)
-region(64)
-assert(seeds == 0 and placements == #names)
+assert(placements == #names and directives[1] == "clear")
+assert(#closed == 6 and resets == 1, "initialize devices only after playable region is held")
+program.on_event_client_state_changed(context, state, {teleport_state = 0})
+assert(#cues == 0)
+region(nil); region(nil)
+assert(#cues == 1 and cues[1] == 0, "arrival dialogue follows spawn settlement once")
 cleared(); cleared()
 cleared(); cleared()
 assert(#directives == 2 and directives[2] == "controls")
 assert(#directives == 2 and directives[2] == "controls")
+assert(#cues == 2 and cues[2] == 3, "settle deltas must preserve encounter ownership")
 package.loaded.mission_ember = nil
 package.loaded.mission_ember = nil
 program = require("mission_ember")
 program = require("mission_ember")
 region(64); cleared()
 region(64); cleared()
-assert(seeds == 0 and placements == #names and #directives == 2)
+assert(seeds == 1 and placements == #names and #directives == 2 and #cues == 2)
 assert(#closed == 6 and resets == 1, "transit and reload must not reset the bridge")
 assert(#closed == 6 and resets == 1, "transit and reload must not reset the bridge")
-print("controller region ownership, transit, clear and reload checks passed")
+print("cinematic handoff, held-region initialization, dialogue and reload checks passed")