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

Keep the server and SDK runtime; remove hooks, scripts, diagnostics and validators

Removes the client hooks and their server and Lua bridges, the mission scripts, tests, tools and notes, the Ember diagnostics, and the body validators that re-derived the encoders. Names every literal, trims comments to the rule, formats the changed files, merges the four combatant encoders into one field-table header, and drops the unused qualified-teleport machinery.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01DxPz39Hf52rgGEpL6xyy7x
stan 4 дней назад
Родитель
Сommit
4593154f10
100 измененных файлов с 2381 добавлено и 13185 удалено
  1. 0 3
      .gitignore
  2. 7 4
      CMakeLists.txt
  3. 0 217
      MISSION_EMBER_FINAL_ISSUES_PLAN.md
  4. 0 546
      MISSION_EMBER_HANDOVER.md
  5. 0 79
      MISSION_EMBER_PLAN.md
  6. 0 45
      MISSION_EMBER_POST_OPUS.md
  7. 0 73
      MISSION_EMBER_REACTOR.md
  8. 0 111
      MISSION_EMBER_REFERENCE.md
  9. 0 190
      MISSION_EMBER_SQUADS.md
  10. 0 1353
      MISSION_EMBER_STATUS.md
  11. 0 2661
      OpusHandoff.md
  12. 1 14
      Sunrise/Sunrise.vcxproj
  13. 16 0
      Sunrise/src/client/content/activity/activity_sdk_generation_scenario_shards.cpp
  14. 0 372
      Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp
  15. 2 2
      Sunrise/src/client/content/activity/activity_sdk_squad_graph_edges.cpp
  16. 0 59
      Sunrise/src/client/content/activity/scriptable_catalog_type23_placement_links.cpp
  17. 0 1240
      Sunrise/src/client/diagnostics/entity_create_probe.cpp
  18. 0 37
      Sunrise/src/client/diagnostics/entity_create_probe.h
  19. 0 10
      Sunrise/src/client/hooks/bootflow/bootflow_hook_lifecycle.cpp
  20. 0 68
      Sunrise/src/client/hooks/bootflow/ember_movie_hud.cpp
  21. 0 34
      Sunrise/src/client/hooks/bootflow/ember_movie_tick.cpp
  22. 0 48
      Sunrise/src/client/hooks/bootflow/ember_movie_ui.cpp
  23. 0 62
      Sunrise/src/client/hooks/bootflow/ember_sunburn.cpp
  24. 0 18
      Sunrise/src/client/hooks/bootflow/internal.h
  25. 0 89
      Sunrise/src/client/hooks/bootflow/loading_cinematics.cpp
  26. 1 14
      Sunrise/src/client/hooks/bootflow/world_step.cpp
  27. 5 24
      Sunrise/src/client/hooks/cine_auth_probe/cine_auth_probe.cpp
  28. 0 203
      Sunrise/src/client/hooks/ember_movies/ember_movies.cpp
  29. 0 14
      Sunrise/src/client/hooks/ember_movies/ember_movies.h
  30. 0 130
      Sunrise/src/client/hooks/ember_movies/orbit_return.cpp
  31. 0 7
      Sunrise/src/client/hooks/ember_movies/orbit_return.h
  32. 0 44
      Sunrise/src/client/hooks/ember_movies/orbit_rules.h
  33. 0 29
      Sunrise/src/client/hooks/ember_movies/playback_rules.h
  34. 0 29
      Sunrise/src/client/hooks/ember_movies/readiness_rules.h
  35. 0 236
      Sunrise/src/client/hooks/ember_movies/resources.cpp
  36. 0 19
      Sunrise/src/client/hooks/ember_movies/resources.h
  37. 0 8
      Sunrise/src/client/hooks/ember_movies/sunburn_rules.h
  38. 0 60
      Sunrise/src/client/hooks/ember_movies/surface_rules.h
  39. 0 99
      Sunrise/src/client/hooks/harvester_drop/harvester_drop_experiment.cpp
  40. 0 7
      Sunrise/src/client/hooks/harvester_drop/harvester_drop_experiment.h
  41. 0 135
      Sunrise/src/client/hooks/harvester_drop/harvester_exit.cpp
  42. 0 107
      Sunrise/src/client/hooks/mission_retirement/mission_retirement.cpp
  43. 0 31
      Sunrise/src/client/hooks/mission_retirement/mission_retirement.h
  44. 1 67
      Sunrise/src/client/hooks/retail_log/retail_log_enqueue_observer.cpp
  45. 0 3
      Sunrise/src/client/runtime/client_hook_activation.cpp
  46. 0 12
      Sunrise/src/core/settings/client/client_settings_parser.cpp
  47. 0 2
      Sunrise/src/core/settings/client/definition.h
  48. 0 1
      Sunrise/src/middleware/bap/activity_message/activity_incident_parser.cpp
  49. 1 4
      Sunrise/src/middleware/bap/activity_message/activity_replicate_membership_encoder.cpp
  50. 0 616
      Sunrise/src/middleware/bap/activity_message/activity_scriptable_auth_fixed_body_codec.cpp
  51. 39 12
      Sunrise/src/middleware/bap/activity_message/activity_scriptable_auth_mission_body_codec.cpp
  52. 92 58
      Sunrise/src/middleware/bap/activity_message/activity_sense_update_decoder.cpp
  53. 23 14
      Sunrise/src/middleware/bap/activity_message/activity_sensor_auth_bodies.cpp
  54. 9 6
      Sunrise/src/middleware/bap/activity_message/activity_squad_auth_body_codec.cpp
  55. 80 0
      Sunrise/src/middleware/bap/activity_message/auth_fields.h
  56. 0 40
      Sunrise/src/middleware/bap/activity_message/authority_cleanup.h
  57. 1 1
      Sunrise/src/middleware/bap/activity_message/cinematic_incident.h
  58. 0 55
      Sunrise/src/middleware/bap/activity_message/combatant_action_auth.h
  59. 268 0
      Sunrise/src/middleware/bap/activity_message/combatant_auth.h
  60. 0 69
      Sunrise/src/middleware/bap/activity_message/combatant_delivery_auth.h
  61. 0 56
      Sunrise/src/middleware/bap/activity_message/combatant_path_auth.h
  62. 0 33
      Sunrise/src/middleware/bap/activity_message/combatant_retire_auth.h
  63. 47 0
      Sunrise/src/middleware/bap/activity_message/damage_monitor_auth.h
  64. 89 38
      Sunrise/src/middleware/bap/activity_message/darkness_zone_auth.h
  65. 27 20
      Sunrise/src/middleware/bap/activity_message/ghost_link_auth.h
  66. 77 44
      Sunrise/src/middleware/bap/activity_message/interactable_object_auth.h
  67. 251 80
      Sunrise/src/middleware/bap/activity_message/mission_auth_patch.h
  68. 58 48
      Sunrise/src/middleware/bap/activity_message/mission_effect_auth.h
  69. 51 34
      Sunrise/src/middleware/bap/activity_message/music_section_auth.h
  70. 4 4
      Sunrise/src/middleware/bap/activity_message/replicate_membership.h
  71. 31 20
      Sunrise/src/middleware/bap/activity_message/roster_presence.h
  72. 70 42
      Sunrise/src/middleware/bap/activity_message/scene_events_auth.h
  73. 6 1
      Sunrise/src/middleware/bap/activity_message/scriptable_auth_body.h
  74. 29 11
      Sunrise/src/middleware/bap/activity_message/sense_observation_packet.h
  75. 1 1
      Sunrise/src/middleware/bap/activity_message/sensor_auth_update.h
  76. 4 3
      Sunrise/src/middleware/bap/activity_message/squad_auth_body.h
  77. 78 37
      Sunrise/src/middleware/bap/activity_message/squad_objective_auth.h
  78. 73 29
      Sunrise/src/server/activity/activity_sdk_behavior_scope.h
  79. 42 1
      Sunrise/src/server/activity/activity_sdk_device_prepare.cpp
  80. 16 498
      Sunrise/src/server/activity/activity_sdk_device_runtime.cpp
  81. 4 0
      Sunrise/src/server/activity/activity_sdk_mission_internal.h
  82. 36 33
      Sunrise/src/server/activity/activity_sdk_mission_prepare.cpp
  83. 14 617
      Sunrise/src/server/activity/activity_sdk_mission_runtime.cpp
  84. 5 788
      Sunrise/src/server/activity/host_runtime.cpp
  85. 34 11
      Sunrise/src/server/activity/host_runtime.h
  86. 3 3
      Sunrise/src/server/activity/host_runtime_mission_inputs.cpp
  87. 9 6
      Sunrise/src/server/activity/host_runtime_names.cpp
  88. 29 789
      Sunrise/src/server/activity/host_runtime_scriptable.cpp
  89. 1 3
      Sunrise/src/server/activity/host_runtime_scriptable_requests.cpp
  90. 20 33
      Sunrise/src/server/activity/host_runtime_sense.cpp
  91. 80 22
      Sunrise/src/server/activity/mission/mission_script_actor_path_sense.h
  92. 2 3
      Sunrise/src/server/activity/mission/mission_script_arena.cpp
  93. 14 6
      Sunrise/src/server/activity/mission/mission_script_event_batch.h
  94. 47 14
      Sunrise/src/server/activity/mission/mission_script_ghost_sense.h
  95. 33 56
      Sunrise/src/server/activity/mission/mission_script_lua_context_api.cpp
  96. 0 2
      Sunrise/src/server/activity/mission/mission_script_lua_event_delivery.cpp
  97. 7 5
      Sunrise/src/server/activity/mission/mission_script_lua_event_derived.cpp
  98. 2 1
      Sunrise/src/server/activity/mission/mission_script_lua_names.h
  99. 464 275
      Sunrise/src/server/activity/mission/mission_script_lua_slot_api.cpp
  100. 77 27
      Sunrise/src/server/activity/mission/mission_script_object_sense.h

+ 0 - 3
.gitignore

@@ -15,6 +15,3 @@
 # CMake build files
 /build/
 /out/
-
-# Local Python inspection tools and tests
-__pycache__/

+ 7 - 4
CMakeLists.txt

@@ -18,10 +18,11 @@ file(GLOB_RECURSE VENDOR_SOURCES CONFIGURE_DEPENDS
     "Sunrise/vendor/lua/*.c"
     "Sunrise/vendor/lua/*.h"
 )
-file(GLOB LUA_SOURCES CONFIGURE_DEPENDS "Sunrise/vendor/lua/*.c")
-# Match the Visual Studio project: the embedded Lua runtime uses C++ linkage.
+# Lua builds as C++ here, matching the MSVC project's CompileAsCpp setting.
+file(GLOB_RECURSE LUA_SOURCES CONFIGURE_DEPENDS
+    "Sunrise/vendor/lua/*.c"
+)
 set_source_files_properties(${LUA_SOURCES} PROPERTIES LANGUAGE CXX)
-list(APPEND VENDOR_SOURCES ${LUA_SOURCES})
 file(GLOB_RECURSE RESOURCE_FILES CONFIGURE_DEPENDS
     "Sunrise/resources/*.rc"
     "Sunrise/resources/*.h"
@@ -83,7 +84,9 @@ target_compile_definitions(steam_api64 PRIVATE
 )
 
 if(MSVC OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
-    target_compile_options(steam_api64 PRIVATE /EHsc)
+    # The vendored Lua interpreter throws, so its error path needs C++ unwinding.
+    target_compile_options(steam_api64 PRIVATE $<$<COMPILE_LANGUAGE:CXX>:/EHsc>)
+
     # Clang warnings are more strict the MSVC I guess, this only works on real clang
     if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
         list(APPEND STRICT_WARNING_FLAGS

+ 0 - 217
MISSION_EMBER_FINAL_ISSUES_PLAN.md

@@ -1,217 +0,0 @@
-# 1AU final issues: handoff and implementation plan
-
-Updated 2026-09-06. User: Millie. Mission: Destiny 2 **1AU**, internal **mission_ember**. Scripting: Lua, using Sunrise's generated SDK and native authority support.
-
-## Start here
-
-The user has completed nearly the whole mission. Preserve the working opening, dropship doors/exits, bridge, enemy spawning, carry interactions, reactor target progression and darkness-zone mechanics. The latest implementation did **not** fix the remaining visual/native problems. Do not interpret passing unit tests or earlier status notes as gameplay confirmation.
-
-Repository: `/home/millie/Documents/Sunrise-builds/mission-ember`, branch `mission-ember`.
-Latest implementation commit: `50e68b7` — `Implement 1AU mission scripting and native encounter support`. It contains all prior mission/runtime work (130 changed files), without a co-author tag. Working tree was clean before creating this document. No implementation changes were made after that commit during the subsequent planning conversation.
-
-Installed game: `/home/millie/Games/Sunrise/bin/x64`.
-Installed scripts: `Sunrise/scripts/mission_ember.lua` and `Sunrise/scripts/mission_ember/` below that directory.
-Log: `/home/millie/Games/Sunrise/bin/x64/Sunrise/logs/sunrise.log`.
-Latest install manifest: `build/finale-followup-installation.json`.
-Backup: `build/finale-followup-backup-20260906-112405`.
-Installed DLL SHA-256: `1ca1f37ea2735aeb0dab6d4a7522937e3cc9c66a5a09b297448253081887f97c`.
-
-### User constraints
-
-- **Never launch or stop the game.** Millie launches manually. Read-only captures are permitted. Check the game is closed before installing.
-- Use Proton for standalone package extraction if necessary; do not launch through Wine. The extractor is not the game.
-- Preserve save, settings and SDK. Back up and verify installation. Installation outside the workspace requires tool escalation, but user authorization to install persists.
-- Give concrete progress updates at least every minute. The user has repeatedly objected to silent investigation/stalls.
-- Completed areas must remain completed on backtracking. Only a confirmed checkpoint wipe resets its encounter.
-- Do not revisit tube-entry deaths: the user identified those as a noclip issue and explicitly withdrew that request.
-- Commit without a co-author tag when committing further work. Do not push unless requested.
-- The current request is to prepare this handoff plan, not to implement another speculative patch.
-
-## Latest user findings — authoritative acceptance criteria
-
-1. Electron Controllers still warp out of the Interceptor room and become inaccessible behind the door. Both must remain reachable, and killing both must open the gate.
-2. Escape currently applies the wrong hazard: **shock**. It must apply **fire/scorch**.
-3. **The pipes on the climb toward the fusion-cell deposit must apply that same fire/scorch effect**, before the deposit/escape phase. Restrict it to actual hazard contact and clear it on leaving.
-4. The beam must disappear after the final fusion cell is deposited.
-5. During the reactor fight, the beam currently stays visually consistent. It must surge and trigger the corresponding screen effect before clamshell exposure.
-6. Explosions must trigger progressively as the player performs the escape, not all at deposit.
-7. Escape still freezes before the ending cutscenes render. Both authored ending movies must play and complete, including native skipping.
-8. Mission objectives now repeatedly flash/re-update without actually changing. Main title and sub-objective must progress once at their intended milestones, independently of navpoint changes.
-
-## Reference and evidence
-
-Gameplay reference: https://www.youtube.com/watch?v=PqurUhqC2CE
-Intro cinematic: https://www.youtube.com/watch?v=IUmUYHELJAY
-Reference repository: https://github.com/stanuwu/SunriseMissions
-
-Actual gameplay video is cached at `/tmp/ember-reference/1au.mp4` (640x360, ~36:42). Prefer reviewing the cached footage to relying on recollection. Relevant reactor/finale segment: **21:20–28:30**, plus escape completion around **28:42** and the following movies.
-
-Existing contact sheets under `build/reactor-audit/`: `reference-01..08.jpg` (5-second samples), `vent-01..05.jpg` (23:28 onward, 1-second samples), `core-01..06.jpg` (25:32 onward), `carry-01..05.jpg` (27:15 onward).
-
-Observed reference timing:
-
-- 23:38: white/blue screen pulse.
-- 23:40–42: blue beam intensification; 23:43 bright orange; 23:44 shutters begin opening; ~23:45 target exposed.
-- Another pulse around 24:09. Current 14s closed / 6s warning / 10s open timings were reconstructed from video, not recovered original mission code.
-- 26:30: central target destroyed.
-- 27:37: final cell held; 27:45: glowing hot pipes burn the player during the climb.
-- 27:58: deposit; 28:01: overload; 28:05 onward: escape; ~28:42: ending transition.
-
-Local documentation: `/home/millie/Documents/Sunrise-docs/MissionDocs/`.
-Relevant files: `OMEGA-FOREST-ENEMIES.md`, `OMEGA-LAIR-CINEMATIC.md`, `OMEGA-NAVIGATION.md`, `1AU-SCRIPTING-AND-AI.md`. The local Omega cinematic doc may not contain the newest recovery described below; the user supplied it externally.
-
-### New Omega evidence from the user
-
-> The received cinematic authority at record `33F26015` had revision 11 / play 1 pending, while the native controller remained at revision 0. The captured native readiness gate correctly blocked application because 19 allocated global records had never been seeded.
-
-The user reported that a bounded live recovery allowed playback, followed by native completion at `t=1408781`, about 171.8 seconds after starting: `phase=5 revision=14 play=0 complete=1 failed=0`; then encounter `ending_finished phase=10 crown_stage=14`.
-
-**Interpretation:** authority receipt and native application are different stages. This proves playback after that recovery in Omega, not an automatic installed-build fix and not the cause of 1AU's freeze. Do not copy Omega's record ID, revision or count into 1AU. Derive the actual missing records and the publication path that should seed them.
-
-Previous 1AU capture: `build/reactor-playtest/20260906-104913.log` and `20260906-104913-events.log`. It reached the final deposit and escape endpoint. A teleport was armed, but no first-ending cinematic-start event followed. Native diagnostics repeated `network_send` stalls. The newer game log also ends in stall snapshots after testing the staging workaround. Preserve that log before it is overwritten.
-
-Repeated scanned stack addresses included `exe+17354B7`, `exe+173A58B` and native worker waits. These are scanned stack words, not a reliable unwound call chain. Decompilation of 1735460/173A560 showed communication/wait machinery; it did not establish the freeze's root cause.
-
-## Implementation order
-
-### 1. Diagnose and fix ending readiness/publication
-
-Files:
-
-- `scripts/mission_ember/ending.lua`, `routes.lua`, `apex.lua`
-- `Sunrise/src/server/activity/mission/mission_script_runtime_dispatch.cpp`
-- `Sunrise/src/server/activity/activity_sdk_mission_runtime.cpp`
-- `Sunrise/src/server/bap/bap_route.cpp`
-- `Sunrise/src/server/bap/encrypted/push/activity/activity_mission_seed_roster.cpp`
-- Cinematic codec, roster publication, authority cleanup and diagnostics modules in the current commit.
-
-Current ending flow selects an unplayed intro cinematic state (region 49), waits for held-region acknowledgement, then selects movie region 1; repeats staging before region 2. This detour was based on a same-bubble teardown hypothesis. **It still freezes. Do not keep adding staging states or arbitrary sleeps.**
-
-Target states/slots:
-
-- First: `STATE_80B3C09E_0000_0001_80B3C091`, region 1, `PF_CINEMATIC_BOOKEND_STM_CINEMATIC`.
-- Second: `STATE_80B3C09E_0000_0002_80B3C093`, region 2, `PF_CINEMATIC_BOOKEND_CNN_CINEMATIC`.
-- Current staging: `STATE_80B3C09E_0006_0001_80B3C09A`, region 49.
-
-Plan:
-
-1. Capture requested/current/held region, seed-lease plan/revision, publication state, cinematic received authority and applied controller state at the failure.
-2. Inspect the native cinematic readiness predicate. Enumerate allocated global records, initialization/seed state and owning schema/registry. Establish whether authority reaches the movie at all before pursuing the Omega mechanism.
-3. Map any missing seeds back to canonical/global/selected-state roster construction. Fix the actual producer or ordering. Never fabricate arbitrary zero bodies or bypass readiness globally.
-4. Ensure every required record is seeded with its schema-valid body and old-area records can retire without deadlocking publication.
-5. Replace the failed staging workaround with the correct authored transition once the evidence supports it. Keep exact completion/skip identity checks and one-shot progression.
-
-Acceptance: automatic first movie start, native finish/skip, second movie start, native finish/skip, final lifetime completion; no freeze, Bird, permanently pending readiness or resource growth. A successful manual memory recovery alone is insufficient.
-
-### 2. Stabilize HUD objectives and advance their actual text
-
-Files: `route_support.lua`, `landing.lua`, `mission_script_lua_slot_api.cpp`, `activity_scriptable_auth_fixed_body_codec.cpp`, `scriptable_auth_body.h`.
-
-Current API sends type-68 directives, always using lane 0, with generated name hash/element and optional type-47 navpoint. Latest patch added an `audience` reference to type-70 engagement sensors and `set_engagement_state{flags=0, revision=1}` per region. Region bindings: landing `M_ENGAGEMENT_SENSOR_80B3CB5E`; Processing `...80B3C8F7`; Cinder `...80B3C22D`; Apex `...80B3C21C`.
-
-Native evidence: `1009740 -> A76E90 -> 9F1290` checks engagement membership; `9F2A00` updates active-player membership. **This receiver is type 70, not a type-34 object filter.** The latest binding caused flashing without correct progression, so the prior inference is incomplete.
-
-Plan:
-
-1. Log actual outgoing directive hash/element/state/lane and audience, alongside native applied HUD entry and engagement membership changes.
-2. Distinguish repeated script publication from repeated native banner activation with unchanged authority.
-3. Confirm generated title/description mapping and native lifecycle/active-lane semantics. Trace `1009C00`, `10098C0`, `1009ED0`, `1008B40`, `1008570` as needed.
-4. Separate objective enter/complete transitions from navpoint visibility changes. Current signature is `hash .. ':' .. shown`, so combat/navpoint changes republish the entire directive.
-5. Fix the actual banner lifecycle and stable audience; do not hide flashing with an arbitrary debounce while leaving text incorrect.
-
-Acceptance: correct main title and sub-objective at Processing, Cinder, reactor targets, final cell and escape; idle/combat/navpoint changes do not flash/re-enter the objective; backtracking does not restore an earlier objective.
-
-### 3. Resolve Controller combat/teleport areas
-
-Files: `route_roster.lua`, `route_support.lua`, `encounter.lua`, `combat_ai.lua`, `apex.lua`.
-
-`electron_controllers` = `DISPENSER_SUPPORT_A_SQUAD` and `DISPENSER_SUPPORT_B_SQUAD`. Assigned `EMBER_APEX_ACCESS_OBJECTIVE`, 14 task groups. Latest patch pins the two squads to `fixed_tasks={9,10}` and skips later cost-based changes. **That mapping was inferred from ordering and failed in game. Remove the assumption; do not freeze all AI.**
-
-Cached descriptors:
-
-- Access objective config `80B3C361`, descriptor offset 2216, 14 groups.
-- Security objective config `80B3D606`, offset 1752, 7 groups.
-- Registry `A36972D4`, root `80B3C220`.
-- Access groups 9/10 refer to type-45 task slots 40/41, named `__influencer_firing_area_set_12/13` in `80B3C21D`. Names/order do not prove correct geometry.
-- Controller actor class `80C1A8E4`, behavior config `8162C450`. A squad anchor is approximately (-363, 2643, 181.5).
-
-Capture actor position, selected objective/task, warp/custom behavior and candidate areas before/after escape from the room. Resolve authored firing/teleport areas and constrain the responsible behavior to reachable room positions. Determine whether the warp is objective-driven or a separate actor behavior. Both deaths, not vehicle boarding or unrelated supports, must open `SECURITY_DOOR_DEVICE`.
-
-### 4. Recover the actual beam surge and shutdown controls
-
-`apex.lua:beam()` currently drives position and power for `SPECOPS_APEX_RING_LASER_DEVICE` / `SPECOPS_APEX_RING_RING_DEVICE`; neither trial produced the intended surge. Do not assume another polarity or more power toggles will trigger the missing screen event.
-
-Cached root `80B3C21C`, registry `A3B76C64`:
-
-| Object/device | Slot | Config | Model |
-| --- | ---: | --- | --- |
-| Laser object | 45/type4 | 80B3D4E2 | 80B7117C |
-| Core object | 46/type4 | 80B3D4E5 | 80B711C6 |
-| Ring object | 47/type4 | 80B3D4E8 | 80B71218 |
-| Laser device | 48/type23 | 80B3D4EB | — |
-| Ring device | 49/type23 | 80B3D4EE | — |
-
-Useful cached components: laser `80B71179`, `80B7117A`, `80C70C0E`, `80C77D05`; core `80B711A6`, `80BFCF84`; ring `80B711EB`, `80B71216`, `80C77DA4`. These are leads, not confirmed effect triggers.
-
-Trace the placed-object effect graph inputs, relevant native events/sequences, and the separate player screen effect. Match video surge -> screen pulse -> shutter exposure. Verify resource references actually resolve and authority is applied to the intended placed instance.
-
-On successful final deposit, explicitly stop the cycle and shut down the actual beam object/effect. Current deposit code erroneously sends both devices to true/open. Preserve the required escape geometry; do not delete an entire shared model indiscriminately. Restoring the escape checkpoint must keep the beam off.
-
-### 5. Apply fire/scorch and progressive escape explosions
-
-Current `hazards()` attaches `REACTOR_MOTHER_BRAIN_HOT_PIPES_THERMAL_HOP_ON` and `AOD_REACTOR_RAIL_TOP_HOP_ON`; the user sees shock. Identify which resource causes it. Verify the correct fire/scorch resource and native damage/status behavior, not just a suggestive slot name.
-
-**Timing:** climb-pipe scorch must already work while carrying the cell toward the deposit (phase 5). Current setup runs only after deposit (phase 6), which is too late. Escape fire is enabled at its authored timing. Leaving contact, checkpoint resets and ending transit must remove effects correctly.
-
-Global root `80B3C09F` hot-pipe candidate volumes: slots 3 (`REACTOR_MOTHER_BRAIN_HOT_PIPES_02_TRIGGER_VOLUME`), 4 (`...03...`), 5, 6 and 8 (unnamed SDK slots). Inspect actual transforms/contact surfaces. Slot 7 is a broad kill volume and must not be treated as a pipe. Current Apex rail filter uses slot 414/type60 (`SLOT_019E`, authored `reactor_rail_top_all_player_trigger` volume); applying damage across that whole space may itself be incorrect.
-
-Native filter support: `slot_set_object_filter` has `inside` and newly added `inside_any`; `mission_effect_auth.h` validates bodies. The current union/intersection encoding passes shape tests but has not proven the intended spatial/native behavior. Inspect it before extending it further. Effects attach through type26 revision/filter authority. Keep player-only selection and remove on leaving contact.
-
-Escape explosions have explicit authored triggers:
-
-- `EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_EXPLOSION_SET_A_PLAYER_TRIGGER`: type31 slot105.
-- B/C/D: type31 slots106/107/108.
-- Corresponding type60 volumes242/243/244/245 in root80B3C21C.
-- Existing scene: `EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_TRIGGERED_EXPLOSIONS_SCENE`.
-- Deposit scene: `MOTHER_BRAIN_HOLE_EXPLOSION_SCENE`.
-
-Current deposit immediately activates both scenes and arms dialogue/end triggers, not the four explosion-set triggers. Inspect whether the explosion scene owns trigger handling internally or requires explicit script-selected tracks. Arm and drive the correct authored sequence as each section is reached. Use one-shot flags; do not replay everything on backtracking. Reset only the appropriate escape state on a checkpoint retry.
-
-Acceptance: scorch on climb pipes before deposit and correct escape fire afterward; no shock or damage outside hazard surfaces; A–D explosions follow player progress at reference timing; beam stays off after deposit.
-
-## Existing tools and data — avoid repeating extraction
-
-- `build/full-mission-audit/sdk.json`: generated slots, scenes, squads/anchors, directive text, trigger volumes.
-- `build/full-mission-audit/descriptors.json`: slot -> config/descriptor/component mapping.
-- `build/full-mission-audit/package-graph.json`: cached package graph.
-- `build/sdk-mission-complete/sdk/lua/missions/mission_ember_80b3c09e.lua`: complete generated Lua mission binding.
-- `build/first-encounter-audit/tags/TAG.bin` and `.meta`: extracted resources and class/size.
-- `build/first-encounter-audit/game_image.bin`: native image, base `0x140000000`.
-- `build/first-encounter-audit/live-*.c`: existing native decompilations. Offline IDA previously ran in an interactive tool session; do not assume its session ID is still usable.
-- `/tmp/ember-xrefs.py HEX_RVA`: call/pointer references in cached native image.
-- `/tmp/ember-schema.py HASH`: reflected wire schema inspection.
-- `/tmp/ember-read-reactor-assets.py`, `/tmp/ember-read-final-assets.py`: standalone Proton package-reader examples. Extract only needed tags.
-- Read-only capture examples: `/tmp/ember-capture-reactor.py`, `/tmp/ember-capture-mission-variables.py`, `/tmp/ember-capture-mission-observations.py`, `/tmp/ember-capture-deletion-transport.py`. Review offsets and process selection before reuse.
-- `/tmp/install-ember-finale-followup.py`: latest backed-up, game-closed-checked installation example. Use a fresh backup/manifest name; preserve old evidence.
-
-## Validation and delivery
-
-The committed baseline built and passed 21 portable C++ tests plus five Lua suites. Current full-route test peaks: 233 durable variables / 61 intents per callback / 2 timers / 13,000 mock-instrumented instructions. Native limits: 512 variables, 64 intents, 32 timers, 100,000 Lua instructions. Do not crowd more initialization into existing callbacks.
-
-The production sandbox removes string pattern methods (`find`, `match`, `gmatch`, `gsub`) and `dump`; a previous foundry fault resulted from using `string.find`. Tests now remove those methods during route callbacks. Use supported literal comparisons and SDK bindings.
-
-```bash
-cmake --build build -j4
-cmake --build build/portable-tests -j4
-ctest --test-dir build/portable-tests --output-on-failure
-lua tests/mission_ember_controller_test.lua
-lua tests/mission_ember_routes_test.lua build/sdk-mission-complete/sdk/lua/missions/mission_ember_80b3c09e.lua
-lua tests/mission_ember_combat_ai_test.lua
-lua tests/mission_ember_encounter_test.lua
-lua tests/mission_ember_wipe_test.lua
-git diff --check
-```
-
-Some newest tests only establish that the failed candidate emits its chosen assignments/filters; change those assertions when evidence changes the implementation. Passing tests must not be described as visual/native success.
-
-Deliver fixes with concrete native evidence and a short manual test sequence. Install into the closed game with backup and hash checks. Ask Millie to launch; do not launch for them. Observe the Controller fight, beam cycle, climb scorch, deposit shutdown, progressive escape, stable objectives and both movies. Preserve logs and explicitly distinguish confirmed behavior from remaining hypotheses. Commit the completed work without a co-author tag.

+ 0 - 546
MISSION_EMBER_HANDOVER.md

@@ -1,546 +0,0 @@
-# 1AU / mission_ember — implementation and engineering handover
-
-Prepared 6 September 2026 for Millie and the next agent working on Sunrise. Read this document before the older plans or transcripts. The delivered ZIPs preserve **`b9880dc`**, whose gameplay implementation was `88a9632`. This working handover now also documents the subsequent gameplay-HUD correction; the existing delivery archives have not been replaced. Later historical notes often describe approaches that have already been replaced.
-
-## 1. Current result and first action
-
-1AU is scripted from the opening cinematic through the escape and two ending movies. The user has repeatedly played the route and described it as almost complete. The latest investigation isolated the ending presentation problem after resolving native playback, texture residency, movie sequencing and return to orbit.
-
-**The user confirmed the installed HUD correction (`3fea388`) works perfectly.** Preserve this tested baseline. The following describes the preceding failure and its correction. The user tested `88a9632` and reported reticle, ammunition, abilities, radar and mission objectives still rendering over the movie. The log confirmed `requested=43 selected=33 applied=33` at t=766752 and STM playing at t=767072. Native HUD ownership is separate from the cinematic overlay window; see section 17. Do not start by rewriting mission progression or the movie loader.
-
-The earlier build, `41e2728`, played both movies through the native decoder. Movie one was skipped with Escape; movie two reached its natural end. The game accepted completion, selected orbit and reached native `orbit_setup`. However, a loading window covered the video. `88a9632` changed the selected UI state from `0x22` (`loading`) to `0x21` (`cinematic_overlay`). That mapping is verified through the native state-to-window tables, rather than inferred from the broad cinematic category. The subsequent HUD filter keeps that correction and excludes only the native role-18 HUD window's drawing during owned movie presentation. It is built, ABI-verified and installed. The first installation attempt stopped before any game writes because Destiny was running; installation succeeded after a subsequent process check confirmed the game had closed.
-
-Installed HUD-correction DLL SHA-256: `c2e84a350012ffd11eaeb39ef39bcb385b986c42dac5c0386ac0d0325f7fcd5b`. The user confirmed the HUD fix works in game; no separate menu/skip test matrix was reported. The guarded installer verified the DLL and all 17 mission scripts against their source hashes.
-
-Latest installed DLL SHA-256:
-
-```text
-c2e84a350012ffd11eaeb39ef39bcb385b986c42dac5c0386ac0d0325f7fcd5b
-```
-
-Installation manifest: `build/ember-installation.json`. Last backup: `build/ember-install-backup-20260906-212949-077349`. The old `build/apex-cooling-installation.json` still records the pre-filter delivery baseline. The install replaced the DLL and 17 mission Lua files, verified their hashes, and preserved settings, save and generated SDK. The agent did not launch or stop the game.
-
-### What the evidence establishes
-
-| Area | Evidence and current confidence |
-| --- | --- |
-| Original opening, landing spawn, first encounter, bridge, lever, Harvester doors/exit | User confirmed these across earlier tests. Preserve the regression baseline. |
-| Full route through refinery, Sunside, Foundry and reactor | Repeated user playthroughs reached the escape. Automated route tests exercise all phases; this is not a claim of every authored detail matching retail. |
-| Movie picture | User explicitly confirmed visible first-movie picture on `15e507e`. Subsequent captures show valid video surfaces for both movies. |
-| Movie sequencing | `41e2728` produced first completion, second request, second playing and second natural completion. |
-| Orbit handoff | `41e2728` accepted completion, queued orbit, requested native cleanup and reached orbit setup. |
-| Gameplay HUD, subtitles and menus | `88a9632` selects state 33, but the user reports gameplay HUD over the movie. The user confirmed the installed role-18 HUD fix (`3fea388`) works perfectly. Individual subtitle/menu/skip combinations were not separately reported. |
-| Remaining finale polish | Beam sound/screen effect, escape scorch, explosions, extraction-ship path and menu/skip behavior need targeted confirmation. Earlier source changes are not proof of visual or audio success. |
-| Fireteam wipe and respawn presentation | Implemented and covered by tests; a multiplayer acceptance run is still valuable. Do not imply solo observations prove co-op correctness. |
-
-## 2. User instructions that must survive the handover
-
-- The user launches and closes the game manually. **Do not launch or stop it.** Do not launch through Wine. If a standalone package reader needs a Windows runtime, use the Proton script; that is a separate process from the game.
-- Installation is authorized, but first verify the game is closed, back up the files and verify the installed bytes. Keep the existing save, settings and SDK unless a specific change is needed and explained.
-- Commit work without a co-author tag. Do not push to a remote without a request.
-- Provide concise, meaningful progress updates at least once per minute during active work. Silent reverse engineering caused significant frustration.
-- Do not describe passing tests, an accepted request, or audible movie playback as proof of rendered presentation. Report the specific receipt or observation.
-- Completed areas are one-shot on backtracking. Only a confirmed checkpoint wipe resets the relevant encounter.
-- Darkness starts after the bridge scan/activation and clears after the relevant area enemies die. Outside darkness, respawn is 3 seconds. In darkness, a dead player waits 30 seconds while someone is alive; when all known fireteam members are dead, show a 3-second wipe countdown and restart the checkpoint.
-- The user explicitly withdrew the tube-entry death issue after identifying a noclip interaction. Do not reopen that work without a new report.
-- The user asked for subtitles and normal inventory/settings access during ending movies. The earlier broad HUD draw suppression was removed. Investigate native UI behavior before reintroducing a mask.
-
-## 3. Workspace and artifact map
-
-| Item | Current location |
-| --- | --- |
-| Repository / branch | `/home/millie/Documents/Sunrise-builds/mission-ember`, branch `mission-ember` |
-| Origin | `https://github.com/Nyxaraa/Sunrise-Nyxara.git` |
-| Installed game directory | `/home/millie/Games/Sunrise/bin/x64` |
-| Game packages | `/home/millie/Games/Sunrise/packages` |
-| Runtime DLL | `build/x64/Release/steam_api64.dll` → game `steam_api64.dll` |
-| Mission entry script | `scripts/mission_ember.lua` → game `Sunrise/scripts/mission_ember.lua` |
-| Mission modules | `scripts/mission_ember/*.lua` → game `Sunrise/scripts/mission_ember/` |
-| Installed SDK pack | game `Sunrise/activity_sdk.pack` |
-| Installed generated SDK | game `Sunrise/sdk/` |
-| Log | game `Sunrise/logs/sunrise.log` |
-| Current native-image reference | `build/first-encounter-audit/game_image.bin` |
-| Latest playback capture | `build/first-encounter-audit/41e2728-live-20260906-205738/` |
-| Latest UI capture | `build/first-encounter-audit/41e2728-ui-windows/` |
-| External contributor docs | `/home/millie/Documents/Sunrise-docs/MissionDocs/` |
-
-The delivery contains two ZIPs. The branch ZIP has the complete committed source tree and a Git bundle of this branch's reachable history. It also carries the current DLL, installed generated SDK, selected diagnostic evidence and a manifest. The scripts ZIP contains the 17 mission Lua files in their `Sunrise/scripts/` deployment layout. The branch ZIP's `_snapshot/` directory describes included runtime assets and checksums. It is supplemental archive content, not part of the source commit.
-
-The 59 GB ignored `build/` cache is not copied wholesale. Selected evidence is included under its original relative `build/first-encounter-audit/` location so the native verifier can run. Compiler caches, old DLL backups, full gameplay recordings and the installed game packages are not needed to represent the Git branch and are not in the delivery. The manifests record what is actually included.
-
-### References
-
-- Gameplay: <https://www.youtube.com/watch?v=PqurUhqC2CE>. Earlier work focused on the finale at 21:20–28:30 and the beam surge around 23:38–23:44.
-- Opening movie: <https://www.youtube.com/watch?v=IUmUYHELJAY>.
-- Mission scripting reference: <https://github.com/stanuwu/SunriseMissions>.
-- Contributor guide: `IKORA-ANIMATION-AND-ENDING.md` in MissionDocs.
-- Enemy reference: `omega-forest-enemies.md` in the user's documentation.
-
-These links are the user-supplied references, not newly reviewed video evidence during preparation of this document. Existing reference notes and captures should be consulted before changing encounter cadence.
-
-## 4. Read order and historical traps
-
-Read these first:
-
-1. This handover.
-2. `docs/mission-ember-prerendered-ending.md`.
-3. The newest sections of `docs/mission-ember-final-corrections.md`.
-4. The relevant current Lua module and native implementation.
-5. The test covering that behavior.
-
-The following are useful history, **not current implementation authority**:
-
-- `MISSION_EMBER_STATUS.md`: many chronological entries with obsolete installed hashes and failed approaches.
-- `MISSION_EMBER_FINAL_ISSUES_PLAN.md`: earlier plan based on `50e68b7`; its “do not implement” instruction applied to that planning turn, not the current task.
-- `OpusHandoff.md`: earlier contributor transcript, preserved unchanged.
-- `MISSION_EMBER_POST_OPUS.md`, `MISSION_EMBER_REACTOR.md`, `MISSION_EMBER_SQUADS.md`, `MISSION_EMBER_PLAN.md`, `MISSION_EMBER_REFERENCE.md`.
-- `docs/mission-ember-apex-cooling-and-scorch.md`: mixes useful authored mappings with superseded world-transition, hazard and timing experiments.
-
-Examples of obsolete claims to avoid carrying forward: ending movies must select regions 1/2; broad HUD suppression is still active; movie completion waits for texture eviction; UI `0x22` is ready cinematic presentation; the beam still has an independent pre-roll timer; escape uses only the placed damage object; Electron Controllers still use fixed access-objective tasks. Read current source for each.
-
-## 5. Mission identity and generated SDK
-
-- Mission scenario tag: `80B3C09E`.
-- Activity definition: `38F926B2`.
-- Generated module: `sdk/lua/missions/mission_ember_80b3c09e.lua`.
-- Root script resolves it through `require('missions').MISSION_EMBER`.
-- Installed Lua manifest content key: `sha256:dbd228327d14e7b42ea920e7e3da55b3314b9d5483b0f31bb6ac73afc92dcc35`.
-
-The SDK pack, generated scenario shards, catalog and Lua declarations form a matching set. The source tree alone does not contain the full generated SDK. The archive supplies the installed set; game packages still come from the user's installation. Do not mix a stale pack with a newly generated Lua tree.
-
-The authored mission is split into packed region indices:
-
-| Region | Role |
-| --- | --- |
-| 49 | Opening cinematic alternate; initial state `STATE_80B3C09E_0006_0001_80B3C09A` |
-| 64 | Powerhouse / landing / bridge |
-| 56 | Link / Processing / refinery |
-| 40 | Cinder / Sunside / Foundry |
-| 0 | Apex / access / reactor / escape |
-| 1 and 2 | Authored ending alternatives; current direct movie path deliberately does not travel there |
-| 48 | Shared/global state; do not confuse it with the held opening alternate 49 |
-
-Binding must be exact: registry key, slot type, slot index and, where relevant, generation/revision. A requested destination is not an arrival receipt. A slot name existing in Lua does not prove its native object is present or its authority has been applied.
-
-## 6. Lua architecture and runtime contracts
-
-`mission_ember.lua` is the entry point. It constructs opening, landing and wipe controllers, then lazily constructs the later route. Events are delivered through explicit callbacks for client state, cinematic incidents, player triggers, object interaction/state, squad state, actor path state, damage state, timer completion and fireteam state.
-
-| Module | Responsibility |
-| --- | --- |
-| `opening.lua` | Start intro only on held region 49; exact completion/skip source; transition to landing once |
-| `landing.lua` | Landing encounters, lever/Crimson Shadow, bridge console, darkness and navigation |
-| `roster.lua` | Opening encounter squad lists |
-| `encounter.lua` | Reusable durable encounter state and one-shot spawn/clear/reset |
-| `combat_ai.lua` | Native authored objective assignment and cost-based task-group selection |
-| `transport.lua` | Four Harvesters, reserved cargo, authored paths, delivery and exit action |
-| `routes.lua` | Region dispatch, forward progression ownership, ending event gate and checkpoint reset routing |
-| `route_support.lua` | Exact slot matching, later encounters, devices, objects, effects, directives, cues, checkpoints |
-| `route_roster.lua` | Later encounter squad/objective mappings |
-| `carry.lua` | Native cell pickup/deposit, ownership tracking, consumption and expiry recovery |
-| `processing.lua` | Refinery machinery, fusion-cell objective, waves and exit hatch |
-| `cinder.lua` | Sunside, retreats, indoor checkpoints, grinder/ascent and Foundry |
-| `apex.lua` | Controllers, reactor cooling, clamshell/core targets, final cell, hazards, explosions and escape |
-| `music.lua` | Authored music section selection at mission milestones |
-| `ending.lua` | Direct movie 1 → movie 2 → lifetime completion, polled at 250 ms |
-| `wipe.lua` | All-dead countdown and native checkpoint-restart handshake |
-
-Progress is stored in the transactional state exposed by `state:variable` and `context:set_variable`. Module-local tables are declarations and controller objects, not durable mission progress. Event callbacks stage typed intents; accepted Lua intent construction is not the same as native application.
-
-Current bounds are important:
-
-| Bound | Value / source |
-| --- | --- |
-| Durable variables | 512, `state/activity/mission/definition.h` |
-| Timers | 32 |
-| Intent burst | 63 |
-| State/timer key storage | 64 bytes including terminator |
-| Durable string storage | 128 bytes including terminator |
-| Lua arena | 64 MiB, `mission_script_vm.h` |
-| VM wrapper storage | 512 KiB |
-| Latest full-route mock peak | 244 variables, 61 intents/event, 3 timers, 13,000 instrumented instructions/event |
-
-The script sandbox excludes some standard string functions. A prior production fault before Foundry was caused by `string.find`; the route harness now removes those functions during callbacks. Prefer exact names and explicit tables to pattern parsing in mission callbacks.
-
-A callback budget is not an invitation to schedule everything in one event: several setup/reset actions already approach the 63-intent ceiling. Split independent setup stages with short generation-qualified timers when needed.
-
-## 7. Enemy spawning and native AI
-
-The decisive AI discovery was that spawning enemies is not enough. Squads must belong to the correct authored combat objective. That objective supplies combat areas and task behavior. This resolved the “shooting without moving/no idle behavior” class of failure.
-
-The common pattern is:
-
-```lua
-context:squad(mission.Squad.SOME_SQUAD):place{
-    mode = context.sdk.squad_modes.replace
-}
-context:slot(mission.Slot.SOME_SQUAD):assign_combat_objective{
-    objective = context:slot(mission.Slot.AUTHORED_OBJECTIVE),
-    revision = 1,
-    task_group = -1,
-    reserved = false,
-}
-```
-
-Use the actual generated declarations. Do not invent a squad or objective from a nearby name. The SDK resolver had to distinguish campaign and arcade occurrences; reusing a slot with the wrong occurrence can publish successfully while controlling the wrong native content.
-
-`combat_ai.lua` consumes objective revision 1 and native task-cost arrays. Costs below 2040 are candidates; 2040 is the native saturated/unreachable bound. It selects the lowest cost and retains the current group on ties to avoid repeated relinking. Twelve five-bit group selections are packed into each durable integer. Fixed-task declarations, when present, are treated separately by the encounter.
-
-`Encounter:enter` validates authored counts before staging placements. Its phase moves from 0 to 1 only once. Population tracking uses `seen` and `alive` masks so an initial zero report does not count as a kill. Phase 2 marks completion. A later visit does not call placement again. Reset is explicit and retires old members with native removal before new generation work.
-
-Current Electron Controllers use `EMBER_APEX_SECURITY_OBJECTIVE` with seven task groups, together with the security/dispenser area. The earlier access-objective fixed-task experiment is superseded. `electron_controllers` is exactly `DISPENSER_SUPPORT_A_SQUAD` and `DISPENSER_SUPPORT_B_SQUAD`; support C is a separate encounter. Killing both controllers gates the Interceptor-area door. Their accessibility and tendency to move through that door were repeated user reports, so keep this on the final acceptance checklist.
-
-## 8. Lever, Ghost console, bridge and ordinary devices
-
-The landing door lever is an authored type-4 interactable object paired with a type-23 device. Rendering/interaction and animation are separate operations. Initialize the native interactable, unlock/power and establish the device baseline, then animate it on the exact accepted interaction receipt.
-
-`landing.lua` checks the lever's registry key/type/index, sets `ember.lever.used`, transitions the lever device to `open`, enters `mercury_bonus`, and opens the paired door. Proximity must not spawn Crimson Shadow or open that door. Crimson Shadow is optional to the mandatory pre-bridge clear and must not hold the Ghost console disabled.
-
-For ordinary devices:
-
-```lua
-slot:transition{transition = context.sdk.device_transitions.unlock}
-slot:transition{transition = context.sdk.device_transitions.power_on}
-slot:transition{transition = context.sdk.device_transitions.close, snap = true}
--- Later, on the proper event:
-slot:transition{transition = context.sdk.device_transitions.open}
-```
-
-These are examples of API use, not universal endpoint polarity. The bridge specifically uses position 1/open for retracted and position 0/close for extended. The opening beam also has its own verified mapping. Always check the authored device instead of assuming “open” means physically open for every model.
-
-The Ghost console is enabled only after the required opening squads have been observed and cleared. The bridge advances on the exact Ghost-link generation with inactive state and progress at least 1, not merely on an object interaction or combat clear. That receipt extends bridge devices, activates darkness, starts the far-side `sun` and `helipad` groups, and starts transport. Completed encounter state prevents a later bridge crossing from respawning them.
-
-The close-side bridge squad is part of pre-bridge combat, not a surprise trigger after walking onto the bridge. Far-side hangar squads preload at bridge activation. Darkness completion considers the appropriate bridge/passenger/sun population; distant helipad population must not leave this area permanently restricted.
-
-## 9. Harvester transport: exact current sequence
-
-The ship and its passengers have separate native commands and receipts. Do not return to spawning passengers at world transforms inside a moving ship; that caused falling troops and premature deaths.
-
-The four ships are A/B/C/D. A and B carry the four bridge passenger squads with manifest `{1,3}` and `{2,4}`. C and D are fly-through ships with no bridge passengers.
-
-1. Before Ghost use becomes available, reserve passenger squads. Create empty ship parent squads with zero ordinary member counts. Assign the ships to the bridge combat objective. This prepares native ownership without an extra stationary spawn.
-2. `play_actor_path{generation=..., revision=1, path=entry_sequence_slot}` creates/enters each authored ship member along its entry path.
-3. Wait for that member's native revision-1 path-complete receipt. Current delay is `ARRIVAL_SETTLE_MS` 3000 plus `EXTRA_DROP_DELAY_MS` 3000. Preserve the actual six-second code value unless a new timed comparison justifies changing it; earlier conversation “3-second delay” referred to an added delay.
-4. Call `deliver_squads{generation=..., revision=1, squads=reserved_sensor_slots}` for A/B. Delivery uses the native transport mechanism that opens the doors and detaches cargo. It is not a lever-device animation substitute.
-5. Wait for delivery completion and actual passenger population receipts. Hold for a further 4000 ms at the authored drop position, then request revision-2 exit path.
-6. On revision-2 path completion, invoke `play_actor_action` with revision 3, empty group hash `811C9DC5`, and action `7B0D3643` = FNV-1(`exit_25`). This is the Harvester's authored exit action from controller `80FE21CE`.
-7. Retire the actor with a newer generation after the exit-action completion receipt, or when its dead receipt arrives. Do not delete it before the effect or leave it parked at the path end.
-
-`ember.transport.flags` packs five flags per ship. Timers include generation so a wipe cannot let old departures affect new ships. Existing flights are allowed to finish and retire after the player leaves the region. The native actor transport/action hooks and schemas under `Sunrise/src/` are part of this implementation; copying Lua onto an older upstream DLL will not reproduce it.
-
-## 10. Refinery cell and later route
-
-`processing.lua` initializes the drill open with the obstruction present. Accepted fusion-cell deposit drives the next phase, closes the drill/machinery as scripted, sets the Processing checkpoint and starts defense. The end-of-drill hatch opens when reaching its area, because its enemies are beyond that hatch; it must not wait for those inaccessible squads to die.
-
-`carry.lua` is shared by refinery and final reactor cell:
-
-- Enable the authored pickup and receptacle with `set_interactable_object`.
-- Track pickup owner and exact generation through native Sense.
-- Accept deposit only from the receptacle's current generation after a pickup receipt.
-- Set done before consumption/recovery side effects.
-- Consume the carried cell with a newer inactive object generation. Interaction alone did not remove it in earlier builds.
-- If an uncompleted cell disappears or becomes unusable, arm a two-second recovery timer. Advance pickup generation by two, leaving the receptacle generation unchanged.
-- Do not recover while its region is unloaded; defer and resume after return.
-- Ignore stale receipts. Once deposited, recovery remains disabled.
-
-Ownership fields require their actual native wire representation. An earlier optional/raw64 decoding mistake prevented valid owner state, and an inverted use flag removed the prompt. The successful current API should be preserved when debugging later cell behavior.
-
-`cinder.lua` advances through explicit authored triggers and durable phases. The indoor Sunside darkness/dialogue boundary is beyond the far door, not while still outside. Checkpoints include ready room 2 and Foundry. The foundry callback uses exact group comparisons compatible with the sandbox.
-
-## 11. Reactor mechanics, effects and remaining presentation questions
-
-`apex.lua` owns access/security, reactor sides, core, mother-brain cell and escape. Both Electron Controllers gate the Interceptor-area door. Reactor bridges start retracted and extend after both side targets are destroyed. The central doors join the cooling cadence after their unlock condition. The central target must be actually placed, damage-watched and killed before the carry route unlocks.
-
-Destruction requires native evidence. Missing health, an initial zero or a stale generation cannot count as destroying a target. The object/damage decoder fixes in the DLL are essential to the Lua progression.
-
-### Beam and cooling
-
-The current cycle is:
-
-- closed/resting: 14,000 ms;
-- warning/surge: 6,000 ms;
-- open/cooling exposure: 10,000 ms.
-
-Surge ends **before** the clamshell doors open. This is a cooling mechanism. The user's failed inversion request was undone: current `beam_pose` uses `open` for normal and `close` for surge, applied to both laser and ring devices. `beam_surge` owns the rising edge and queues the authored alarm at that same transition; independent audio pre-roll experiments are superseded.
-
-Beam objects and their device power must both be enabled. Presence handling seeks the driven endpoint once and animates back to the normal endpoint so authored events can run. It is generation/idempotence guarded and must not restart the beam after final deposit. Deposit shuts the beam down.
-
-The user repeatedly reported that sound lagged the visual by about four seconds. Current code ties their requests together, but internal animation/audio offsets can still differ. Do not equate simultaneous Lua calls with audible synchronization. The exact full-screen surge effect is another outstanding verification item; no unrelated Omega event should be substituted just because its identifier is available.
-
-### Scorch
-
-The latest source uses one native player attachment, borrowing the authored Sunside sunburn asset `80B82489` through `bootflow/ember_sunburn.cpp`. It narrowly recognizes Ember type-26 slot 43's captured component/template identity, replaces the per-component spawn-request asset for the native attach call, then restores it. It does not patch a shared package or apply an arbitrary damage multiplier.
-
-The placed `SUNBURN_DAMAGE_OBJECT` remains disabled to avoid a second source. Before deposit, the same attachment is filtered to the five authored narrow hot-pipe volumes. During escape it uses the rail-top player filter. The mode is idempotent and the effect is disabled at the escape trigger/reset boundary. Earlier experiments with a rail effect plus pipe effect, a delayed reattach, or only the damage object are superseded.
-
-The user reported both doubled damage and absent escape scorch across prior candidates. Verify actual damage rate and scope in the current build. A successful `ember_sunburn result=attached` receipt proves an attachment, not its contact geometry or correct damage amount.
-
-### Explosions and extraction ship
-
-Escape explosions use a single live Scene generation. As the authored A/B/C/D trigger volumes are crossed, `set_scene_events` republishes the accumulated matching event keys (`explosion_set_a_trigger` through `_d_trigger`). Scene activation without those inputs did not fire the authored effects. Duplicate trigger receipts must not restart an explosion.
-
-The extraction ship uses its own placed object/device path, distinct from the combat Harvesters. Its actual model is `80B71228`, with device component `80C70C0E` and animation component `80B71226`; the shared fallback `80BFDDC2` is not the placed ship. Presence is followed by unlock/power/baseline/animated drive once. Its visual authored flight still belongs in the final checklist.
-
-## 12. Objectives, navigation, dialogue and music
-
-Each generated directive element contains the main title and sub-objective. Main wording can legitimately remain “Find and disable…” through much of the mission, then change at reactor and escape milestones. Repeating the same element is not a main-objective update.
-
-Type-68 state 0 enters the directive; repeated publication replays the banner animation. `route_support.lua` deduplicates on the directive hash plus its stable marker. It tracks furthest region rank so backtracking does not regress the banner. Navpoints are authored type-47 references; combat-aware guidance is handled by the relevant controller.
-
-`ember.hud.audience.<region>` is only a once-only flag for initializing the type-70 engagement sensor used as the directive audience. It is **not** a HUD visibility switch, cinematic state or video mask. Native engagement flag bit 0 clear includes active players.
-
-Dialogue cues are authored `M_DIALOG_SENSOR_80B3C90A` cue declarations, with durable deduplication keys. Music uses the native type-11 sensor and sections from bank `80B3C904`, with 29 ordered sections. Section-to-encounter pacing was reconstructed from package order and mission milestones. The music bank owns the transitions, but a complete audible retail-equivalence check has not been established.
-
-## 13. Darkness, respawn and checkpoint reset
-
-`wipe.lua` treats “all dead” as positive dead count, zero alive count **and zero unknown count**. Unknown membership must not wipe an apparently incomplete fireteam.
-
-Stage 1 publishes a three-second type-35 darkness countdown and ticks it once per second. If someone becomes alive or darkness clears, cancel it. At zero, `restart_checkpoint` begins the native hard-wipe handshake. Once native spawn state reaches teardown/fade state 2, reset the relevant encounter and issue the release request. Native state 0 after release completes the reset.
-
-Authored checkpoint spawn sets used by the route:
-
-| Checkpoint | Region | Spawn-set hash |
-| --- | ---: | --- |
-| Landing / bridge | 64 | `9C58857A` |
-| Processing | 56 | `4B27745D` |
-| Ready room 2 | 40 | `82328D63` |
-| Foundry | 40 | `782CAF4C` |
-| Reactor entrance | 0 | `DF59C25C` |
-| Escape | 0 | `45920385` |
-
-Resetting encounter state on a routine region enter would violate the one-shot requirement. Keep wipe handshake, encounter reset and ordinary traversal separate. Do not simulate the requested 30-second respawn by merely withholding a spawn location; the user's requirement is a visible native timer.
-
-## 14. Why the ending is a direct pre-rendered movie path
-
-The opening is an in-engine type-6 cinematic. Its controller owns an authored region and emits exact started/terminated/skip incidents. The two ending bookends use pre-rendered movie components:
-
-| Movie | Placement | Entity | Component | Wrapper | Stream |
-| --- | --- | --- | --- | --- | --- |
-| STM, first | `80B3C222` | `80B38179` | `80BDDC62` | `80BCA001` | `80BCA034` |
-| CNN, second | `80B3C226` | `80B3817B` | `80BDDC67` | `80BCA003` | `80C7C000` |
-
-Both entities contain class `808065EB`, configured by `808065EC`; config+`4C` carries the movie asset. The package/native evidence is more reliable than guessing from cinematic slot names.
-
-Earlier attempts selected bookend regions 1/2, tried region 49 as staging, or waited for seeded type-6 authority. Those encountered native world teardown/record-chain failures before a movie controller existed. The Omega guide describes an in-engine cinematic whose received authority can wait behind a global readiness gate. That finding is useful, but is not proof that a pre-rendered Ember movie needs the same actor/controller workaround.
-
-Current escape handling keeps Apex loaded and calls the native movie manager on the game frame. It does not fabricate a type-6 actor, bypass general readiness, drain global I/O, or complete the mission before playing the videos.
-
-Public mission API:
-
-```lua
-context:play_prerendered_movie{index = 1} -- or 2; optional stop request
-local status = context:prerendered_movie_status(1)
-```
-
-Requests carry session, ActivityClient generation and request identity and are restricted to the appropriate private Ember/Apex context. The native bridge lives in `Sunrise/src/client/hooks/ember_movies/`; frame observation is installed by `bootflow/ember_movie_tick.cpp` and also uses player-camera frame observation.
-
-`ending.lua` polls every 250 ms. Queued/preparing do not count as playback. First native completion queues the second movie. Second native completion sets `ember.complete`, phase 100 and lifetime 6. Other gameplay route dispatch stops once `ember.ending` owns the route.
-
-## 15. Native movie playback and resource ordering
-
-All addresses below are RVAs in the captured mapped build, with image base `140000000`. Production calls resolve unique signatures and verified relative calls; do not blindly hardcode these addresses into another version.
-
-| Operation | RVA |
-| --- | --- |
-| Native pre-rendered component start | `DDB0F0` |
-| Movie manager accessor / acquire | `41B040` / `41A3C0` |
-| Play(manager, asset, 0) | `41CD20` |
-| Stop / release | `41D0C0` / `41A980` |
-| Native busy predicate / decoder accessor | `41B420` / `41AB70` |
-| CPU movie frame processing | `41D140` |
-| Surface selection publication | `1202B00` |
-| UI movie command submission | `132B890` → `1278FF0`, command `1B` |
-| Resource manager/create/add/submit | `4294D0` / `423EF0` / `4312D0` / `435AA0` |
-| Root status / destroy | `42C650` / `425310` |
-
-The decoder's asset is at `+1B4` and state at `+1B0`. The bridge requires state 5 for the exact asset to recognize playing. Completion requires that playback was previously seen, native busy is false, and decoder state is stopped/end (0 or 6). State 7 is failure. A changed decoder/asset/world fails instead of awarding completion. Prepare/busy/resource waits are bounded at 30 seconds; playback at 600 seconds.
-
-### All movie resources use kind 1
-
-Kind 2 means shared/type-16 resources. Copying it from a startup loader caused a native crash. The metadata, compact stream mapping, surface definitions, buffers and containers here all use package-verified kind 1.
-
-The stream request does **not** read an entire video into RAM. Native `3591B0` recognizes the stream type and publishes encoded file offset/patch and length. Omitting it left valid wrappers with an unopened stream and decoder failure state 7.
-
-### The six authored Y/U/V surfaces
-
-| Slot | Plane | Definition | Buffer | Container |
-| ---: | --- | --- | --- | --- |
-| 1 | Y0 | `80BCA021` | `80BCA020` | `80BCA022` |
-| 2 | Y1 | `80BCA024` | `80BCA023` | `80BCA025` |
-| 3 | U0 | `80BCA026` | `80BCA027` | `80BCA028` |
-| 4 | U1 | `80BCA029` | `80BCA02A` | `80BCA02B` |
-| 5 | V0 | `80BCA02C` | `80BCA02D` | `80BCA02E` |
-| 6 | V1 | `80BCA02F` | `80BCA030` | `80BCA031` |
-
-Catalog `80BCA032` names those containers. A container is class `80806B91` and contains the definition reference. Definitions are package type `44FB`, buffers `254FB`, both subtype 19. The definition file has eight bytes, but native allocation expands it to 16; the backing pointer lives at +8. Native buffer callback `1204581` fills that pointer. The renderer reads it via `4A6340`.
-
-Load order is essential. A container registration callback at `1204163` immediately dereferences its definition's slot byte. A single asynchronous batch with “definitions listed first” does not order the callbacks. The `a47adbd` run faulted there with RAX 0 and RBX `80BCA021`.
-
-Current `MovieResource` therefore uses two roots:
-
-1. Load only six definitions and validate their native resident size/type, slot and format.
-2. Load both movies' metadata/compact stream mappings, shared catalog, six backing buffers and six containers.
-3. Wait for ready roots, valid wrappers/media, non-null backing pointers and registered surfaces.
-4. Publish the native surface-selection stacks after the movie player is idle, then verify all six selected definitions before acquire/play.
-
-The registration table is at captured global `142E800B0`, eight rows of 20 bytes: count, three candidate handles, selected handle. Nonzero stale candidates with count zero are not registrations. A CPU frame and sound do not prove those GPU surfaces exist.
-
-## 16. Shared resources, EOF and orbit
-
-Both movies share the same textures. `15e507e` withheld first completion until those textures disappeared after root release; the loader retained them, so Lua never queued movie two. The captured first movie reached EOF and then stayed at `surfaces_retiring`.
-
-Current code exposes native EOF immediately, balances that movie's manager acquire, and keeps the shared resource set for CNN. A held set can only chain movie 1 → movie 2 for the same session/generation with a recorded first completion. Another owner, reversed/repeated sequence or preparing/failed state cannot borrow it. Native cleanup runs independently after the final movie. It must never gate the mission's completion on global cache eviction.
-
-`orbit_return.cpp` arms only after both movies completed for the same owner. It waits for native lifetime 6, confirms the same world arrival and selected Ember destination, constructs the native default orbit selection, clears/selects/commits it, and waits for the destination to read back as orbit. Then it requests deferred native cleanup, step 28, and recognizes orbit setup at step 29. There is no completion-banner hold.
-
-The last tested timeline:
-
-| Time in log | Receipt |
-| ---: | --- |
-| 281530 | STM playing, state 5 |
-| 443845 | Foreground Escape requested native stop |
-| 443950 | STM complete; CNN queued |
-| 444277 | CNN playing, state 5 |
-| 649483 | CNN natural completion; orbit handoff armed |
-| 649544 | Lifetime completion accepted; orbit selection queued |
-| 652554 | Deferred native cleanup requested |
-| 653295 | `orbit_setup` |
-
-This validates sequencing and native orbit setup. It does not retrospectively validate the obscured video, HUD/menu behavior, or final resource eviction. In particular, the captured log did not establish a final `resource_released` receipt before shutdown; retain that distinction if investigating repeat-run resource ownership later.
-
-## 17. The loading overlay mistake and the exact correction
-
-The removed early workaround detoured UI layer drawing at `132BD80` and returned without drawing anything during movies. It hid HUD, but also hid subtitles and normal UI. The user asked to use native cinematic state instead.
-
-`41e2728` then mapped normal gameplay requests (`0x2B`, `0x2D`, `0x2F`) to `0x22` through the original native UI state transition at `E1CD60`. Both `0x21` and `0x22` belong to category 4. Checking only that category was insufficient.
-
-The complete mapping now verified is:
-
-```text
-UI state 0x21 -> 1312540 -> window enum 26 -> 13126E0 -> 17A73819 = cinematic_overlay
-UI state 0x22 -> 1312540 -> window enum 29 -> 13126E0 -> D505DEBB = loading
-```
-
-Names use FNV-1, not FNV-1a. Native window manager `1316FC0` reads the UI state, calls those mappings and explicitly excludes `0x22` from its cinematic-overlay branch at `1317094`.
-
-The live UI capture showed `loading` asset `80B46D88` alongside `subtitle_overlay` asset `80BC6681`, hash `7737E414`. Six valid video surfaces were present. Thus this black screen was a real UI window covering video, not a return of the texture problem.
-
-**`88a9632` changes the substitution to `0x21`.** It leaves other requested UI states alone, retains presentation across the movie handoff, and clears it on final completion or failure. Native drawing, window updates and input remain intact. The tests now follow state → enum → actual name hash and check that true loading/menu requests are preserved.
-
-The user briefly opened menus for a read-only capture on the old build, but that only caught a short-lived additional window while the loading state was active. It is not a validated inventory/settings allowlist.
-
-The next `88a9632` test left the gameplay HUD over the video. Native `1316FC0` independently creates `hud` (name hash `268AB804`) or an equipment-specific override, then assigns semantic role 18 through `13165C0`. That setter writes window offset `310`; it is distinct from the state enum at `410`. The current `bootflow/ember_movie_hud.cpp` filter checks this role only at the verified root-window draw caller `132C1BD` → `13D9060`, returning before the entire HUD subtree, including cached child commands, is submitted. Recursive child widgets use the same callee but do not have a full window allocation, so the exact return-address guard must precede the role read. Do not remove that guard.
-
-Both `132BD80` layers and every other window role continue drawing. Native updates/input/visibility and user preferences remain unchanged. The filter is active only while the ending bridge owns presentation, spanning the movie handoff and ending on final completion/failure. It reports `hud_filter_attached` at startup and `hud_draw_suppressed` with the actual window identity on its first suppression. The native verifier checks the root construction, caller/callee and role setter; the user confirmed the HUD fix works in game. This is narrower than the old blanket layer suppression. Latest log and disassembly evidence: `build/first-encounter-audit/b9880dc-hud/`.
-
-A remaining interaction concern to test: the direct player uses a foreground Escape edge to call native stop. Native in-engine cutscenes have a type-6 skip incident; direct movies do not. Verify that closing inventory/settings does not accidentally skip the movie. If a change is needed, base it on native menu/input ownership rather than simply consuming all Escape presses.
-
-## 18. Launch suppression and Lime
-
-`bootflow/loading_cinematics.cpp` detours `LoadingCinematics_Suppressed`, captured RVA `C24490`, using a verified caller signature. It honors `client.suppress_loading_cinematics` only when the current native selected activity resolves to definition `38F926B2`. It reads the selection each time; a retained host session or saved character activity is not proof that Ember is being launched now.
-
-This is travel fly-in suppression, not movie playback authority or the cinematic presentation state. The mission intro still starts through its own controller.
-
-A previous Lime investigation also involved incorrect native alternate-world assumptions. Base and alternate worlds have separate registrations (`4C8E60` / `4C8E40`); mutating the message-1 per-bubble state byte to select a bookend was wrong. Do not restore that wire change or globally suppress loading cinematics.
-
-## 19. Bird, simulation records and cleanup
-
-Bird was not established as a single error with one universal cause. Captures included exhausted simulation records, detached authority records, blocked transmission, wrong purge epochs and later teardown stalls. Do not “fix Bird” by blindly increasing a pool or enabling darkness everywhere.
-
-Important retained fixes include:
-
-- Zero-count squad replacement uses native destruction with a newer generation, not merely a kill-only operation.
-- Membership publishes the remote-host transmission bit used by native entity views. Acknowledgements alone could advance while entity transmission remained disabled.
-- Abandoned-slot/purge requests receive exact-mask native purge responses with the entity authority manager's own epoch. This is distinct from another replication epoch field.
-- Valid transport acknowledgements release outgoing contributions independently of unsupported incoming gameplay lanes.
-- Retired roster keys keep wire ordinals while their presence bits clear; bodies for retired keys are omitted.
-- Encounter removal/reset stays bounded and generation-qualified.
-
-Useful logs: `ev=simulation_records stage=pressure`, `ev=entity_replication stage=census`, mission intent refusals, incorrect epoch assertions and native watchdog exceptions. The simulation-record capacity observed in the current run is 1024. The portable tests cover the wire and ownership rules; live counters and actual reclamation must still be inspected when changing them.
-
-## 20. Build, tests and installation
-
-### Windows DLL from this Linux workspace
-
-The working setup uses CMake/Ninja, clang-cl, lld-link, llvm-rc and an xwin SDK. `linux-to-win-toolchain.cmake` refers to `.xwin-cache`; that directory is local toolchain state and is not included in the ZIP. Supply a compatible xwin tree with CRT and Windows SDK layout before configuring a fresh build.
-
-```sh
-cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=linux-to-win-toolchain.cmake -DCMAKE_BUILD_TYPE=Release
-cmake --build build --target steam_api64 -j 6
-```
-
-The existing cache uses Windows SDK `10.0.26100`. The target is C++20, including the embedded Lua sources built with the project's C++ exception configuration. The tested build path is CMake; do not assume Visual Studio project item lists are automatically synchronized with the globbed CMake source list. Do not start a second linker while the first is still running.
-
-### Portable C++ regressions
-
-```sh
-cmake -S tests -B build/portable-tests
-cmake --build build/portable-tests -j 4
-ctest --test-dir build/portable-tests --output-on-failure
-```
-
-There are currently 24 tests. Relevant coverage includes actor paths, objective/AI support, object interaction, directives/navpoints, darkness/life, effect/damage/music schemas, cleanup/ACK handling, scene events, and `ending_retirement_test` for movie resources, sequencing, native UI selection and orbit guards.
-
-### Lua route regressions
-
-Use the SDK provided in the branch archive or the installed SDK. The route test accepts an explicit module path:
-
-```sh
-lua tests/mission_ember_routes_test.lua /path/to/Sunrise/sdk/lua/missions/mission_ember_80b3c09e.lua
-```
-
-Other suites are `mission_ember_encounter_test.lua`, `mission_ember_controller_test.lua`, `mission_ember_combat_ai_test.lua`, and `mission_ember_wipe_test.lua`. Check each harness's SDK argument/default when running outside the original workspace. No game launch is involved.
-
-### Native ABI/package verification
-
-```sh
-python3 tests/verify_ember_movie_native.py build/first-encounter-audit/game_image.bin /path/to/game/packages
-```
-
-The image must be the mapped/decrypted image, not the encrypted on-disk executable. The verifier checks unique signatures, relative call targets, resource kinds, stream mappings, surface definitions, backing callbacks, movie and orbit operations, sunburn attachment, and the actual cinematic/loading window mapping. Small extracted tag files in `build/first-encounter-audit/tags/` support the package checks. Image-specific checks must be re-established after a game executable update.
-
-### Install
-
-A durable installer is now committed at `tools/install_mission_ember.py`; it replaces the old dependency on `/tmp/install-ember-apex-cooling.py`.
-
-```sh
-python3 tools/install_mission_ember.py --game /home/millie/Games/Sunrise/bin/x64 --dry-run
-python3 tools/install_mission_ember.py --game /home/millie/Games/Sunrise/bin/x64
-```
-
-For the DLL carried in the archive, add `--dll _snapshot/runtime/steam_api64.dll`. It expects Linux `/proc` to enforce the closed-game guard, backs up the 18 destinations, verifies hashes, uses atomic replacement per file and never launches/stops the game. It does not install SDK/settings/save; the snapshot's matching SDK is separately supplied for a new environment. In this managed workspace, writing to the game directory requires tool escalation, but installation authorization already exists.
-
-## 21. Regression checks and debugging decision tree
-
-1. Have the user manually launch the installed HUD-filter DLL; its SHA-256 is in section 1. Confirm `frame_attached`, `presentation_attached` and `hud_filter_attached` in the new log.
-2. Reach the final escape trigger normally. Confirm one movie-1 request. Preserve the current trigger and progression if it already fires.
-3. Expect requested gameplay state 43 to select/applied state **33**, not 34, and require `hud_draw_suppressed role=18` during playback. Check that the picture is visible, reticle/ammunition/abilities/radar/objective HUD is absent and subtitles appear when enabled.
-4. Open inventory and settings; verify normal input and rendering, including closing them without unintended skip. The native state is preferred to a new mask, but this behavior has not yet been observed on the new build.
-5. Test first-movie natural EOF in one run and explicit skip in another. The last run established first skip and second natural EOF, not every combination.
-6. Verify second-movie picture and subtitles; require native `playing movie=2`, not just its queued/submitted log.
-7. After second EOF/skip, require completion and orbit setup. The return request should not wait through a completion-banner hold.
-8. Check final cleanup receipts and a second mission launch in the same process if investigating retained resources. Never force-free native definitions while containers can still call them.
-
-If black returns, classify it before changing code:
-
-| Observation | Investigate |
-| --- | --- |
-| Decoder never reaches 5 | Resource requests, correct stream mapping, player ownership and decoder error |
-| Audio/CPU frames but zero selected/backed surfaces | Definition/buffer/container residency and publication |
-| Valid surfaces plus named `loading` window | UI state/window selection; do not rewrite the decoder |
-| First EOF but no second queued | Completion exposure and same-owner resource handoff |
-| Both movies complete but no lifetime 6 | Lua ending poll/intent delivery and owner generation |
-| Lifetime 6 accepted but no orbit | Native selection readback, deferred cleanup and orbit state receipts |
-| Frozen game before any movie request | Escape dispatch/world teardown; capture thread/watchdog state read-only |
-
-Keep read-only evidence scoped to the relevant process/module and never recover by blindly writing memory. Existing historical capture helpers under `/tmp` are not part of the interface and may contain a stale PID. Inspect them before reuse. The archive includes the important captured evidence so analysis need not depend on those processes or helper scripts still existing.
-
-## 22. Handover completion checklist
-
-- The delivered archives remain source `b9880dc` with installed gameplay `88a9632`. This working tree additionally carries the HUD correction described in sections 1 and 17; do not assume it is in the old ZIPs.
-- This document, the reusable installer and the two pre-existing untracked handovers are included in the final documentation commit, with no co-author trailer.
-- Branch ZIP includes all committed files and branch history; scripts ZIP includes all 17 Lua files.
-- Runtime DLL, matching generated SDK and selected diagnostic data are supplemental branch-archive assets with checksums.
-- Archive verification and the exact final commit are recorded in the delivery manifest.
-- No game launch, stop, new gameplay change or settings/save replacement is part of packaging.
-- The user confirmed the role-18 HUD correction works perfectly on `3fea388`. Preserve it; use section 21 for regression checks, not as a claim that every menu/skip combination was exercised.

+ 0 - 79
MISSION_EMBER_PLAN.md

@@ -1,79 +0,0 @@
-# 1AU remaining mission implementation plan
-
-Authorized on 2026-09-06: finish navigation/wipes, then plan and implement the remaining
-mission without further approval. The user launches the game manually. Install completed
-candidates only while it is closed; preserve settings/save and make rollback backups.
-
-## Evidence and implementation rules
-
-Reference video: https://www.youtube.com/watch?v=PqurUhqC2CE ; saved copy
-`/tmp/ember-reference/1au.mp4`. Opening cutscene reference:
-https://www.youtube.com/watch?v=IUmUYHELJAY . Existing timestamp observations are in
-`MISSION_EMBER_REFERENCE.md`. SDK topology, compact inventories and subsequent frame
-inspection are recorded in `build/full-mission-audit/`.
-
-Use authored squad identities, spawn rules/anchors, combat objectives, devices, objects,
-navpoints, scenes, triggers and dialogue. Video timestamps describe event ordering;
-progression must follow gameplay receipts rather than the recording's elapsed time.
-Keep completed encounters complete across backtracking and streaming. A deliberate wipe
-resets only its checkpoint, with new object/ship generations and stale-event protection.
-A copied SDK declaration is not evidence that its native interaction already works.
-
-## Execution result
-
-All seven work packages reached an installed full-route candidate on2026-09-06.
-The SDK audit and controller/wire regressions passed; the complete route, checkpoint
-reset policies and ending are implemented. The native thermal/exposure and timed
-escape-failure policies remain incomplete, and actual gameplay has deliberately not been
-launched. See `MISSION_EMBER_STATUS.md` for exact limits and installation evidence.
-The work order below records the plan used to build the candidate.
-
-## Work order
-
-1. **Landing navigation and wipe foundation — candidate installed.** Native directive
-   target references, visible normal/darkness respawn policy, death evidence, three-second
-   all-dead countdown and native membership wipe. Reset at Mercury before rescanning;
-   retain completed approach and lever. Offline regressions passed. Native HUD/fade and
-   co-op presentation await the user's live test.
-2. **Complete the package audit before wiring later populations.** Resolve the SDK's
-   ambiguous Processing spawn-rule edges and missing access/ready-room definitions.
-   Extract each combat objective's task-group bounds. Map region ownership and objects
-   to their native entry/model, interaction/destruction/carried state, and paired devices.
-   Verify progression gates against closer video samples.
-3. **Mineral Processing / link, region56 (~05:30–11:00).** Entry squads and shutters;
-   obstructed grinder discovery; fusion-cell pickup, carry and receptacle; activation and
-   defend waves; clearing debris and opening the exit. Include machinery/klaxon/lighting,
-   dialogue and forward guidance. Give the defense its own retry checkpoint.
-4. **Sunside / cinder, region40 (~11:00–20:50).** Exterior solar-exposure behavior and
-   authored cover/route; Sunside squads and retreat scenes; ready rooms, tumbler,
-   chamber/ascent/meat-grinder route; Foundry/Bruiser and exit machinery. Load interactable
-   and environmental objects when their area becomes active. Preserve staged waves and
-   their objectives; checkpoint each contained encounter. Finish at the energy-stream exit.
-5. **Light's End / apex, region0 (~21:00–27:45).** Access and security squads, Interceptor
-   availability, reactor east/west/core encounters. Cycle authored vent devices and expose
-   the corresponding destructible targets; count actual target destruction. Progress from
-   two side exchangers to the core, then fusion-cell pickup and reservoir deposit. Add
-   enemy reinforcements, dialogue, objective counters, navigation and checkpoint cleanup.
-6. **Escape and ending (~27:45–35:27).** Overload scene, escape route and authored ship
-   handoff; identify and sequence the two final cinematic states, including native skip
-   incidents. Complete Ember exactly once. Chosen gameplay after~35:54 is a separate
-   mission and is outside this script.
-7. **End-to-end verification and installation.** Run the mission controller against an
-   SDK-backed simulated event stream: normal route, skipped narrow triggers, deaths and
-   retries, duplicate receipts, carried-object recovery, streaming/backtracking, and final
-   completion. Validate every referenced slot/squad/state and objective task bound. Build
-   native code, run relevant wire/decoder regressions, install and SHA-verify the complete
-   candidate with a backup. Record remaining live-only checks honestly.
-
-## Initial audit findings
-
-The generated SDK has 2,051 slot declarations, 162 squad sensors and160 squad definitions;
-138 definitions pass its existing exactness/profile checks. Processing's11 defense
-sensors each have two rule candidates with unresolved association;13 sensors in later
-access/ready-room content have no generated squad definition. These require package
-investigation before claiming every later squad is supported. One shared valid directive
-and dialogue sensor carries the mission's authored text/cues across its gameplay states.
-
-The existing landing behavior and user-confirmed Harvester doors/exit are the regression
-baseline. New code must preserve the verified initial spawn, optional lever gate, pre-bridge
-Ghost requirements, troop delivery delay, and first-section darkness completion.

+ 0 - 45
MISSION_EMBER_POST_OPUS.md

@@ -1,45 +0,0 @@
-# Post-Opus follow-up — 6 September 2026
-
-Read `OpusHandoff.md` and retained Claude's latest beam polarity (normal open, surge close), thermal effect, objective and ending changes. This is an installed test candidate, not a claim that the remaining visual issues have passed gameplay validation.
-
-## Loading cinematic suppression
-
-The Omega contributor supplied `LoadingCinematics_Suppressed`, then RVA `0xC24490`. In the captured native image this is a no-argument boolean wrapper. Its travel/spaceflight callers skip cinematic setup and waiting when it returns true (D46900, D474D0, EE50A0 and EF44C0 inspected).
-
-`client.suppress_loading_cinematics` now forces that predicate true only when the native loading flow names 1AU (definition 38F926B2). It defaults false in source and is enabled in the installed settings. Setting it false restores the original predicate. The scope is re-evaluated on every call through the same native activity reader the loading flow uses (C294B0). The SDK maps its index to the definition hash. Orbit, another activity, an absent SDK or an unresolved activity uses the original predicate; retained host sessions do not control suppression. The scripted type-6 movie controller is not detoured. Verify that the erroneous Earth fly-in disappears while the authored 1AU introduction and ending movies still play.
-
-The hook resolves the unique EF44C0 caller signature, follows its call at +30 and checks the target's boolean-wrapper bytes. Offline signature validation found exactly one caller and resolved C24490. The short predicate signature alone matches two functions and is deliberately not used for lookup.
-
-## Ending publication
-
-Claude's pending fix cleared an arrival window when the old and new selected plans share a slice set. Ordinary traversal can leave the selected plan at landing (64) while the client already holds Apex (0). Selecting bookend 1 then incorrectly reopened an arrival window by comparing plans alone.
-
-Selection now reads the instantiated client region. A target world already held does not open an arrival wait, and a repeated selection repairs a stale pending flag. Tests cover landing-plan-to-bookend selection with Apex held, a sibling bookend, a real world change, and unknown client placement. A live ending test must verify publication advances and both STM/CNN movies complete; this fix addresses the observed lease deadlock and does not prove there are no subsequent native readiness gates.
-
-## Escape ship
-
-Preserved and tested Claude's uncommitted change: instantiate `REACTOR_GETAWAY_SHIP_OBJECT`, unlock and power on its paired device, then open it without snapping. A regression verifies this order after accepted cell delivery. The authored ship model is 80BFDDC2, with components 80BFDDC0, 815B8D64 and 80C70CAE. Actual flight/animation still needs visual confirmation.
-
-## Remaining surge screen effect
-
-Beam pose is user-confirmed and its corrected polarity is preserved. No additional screen-effect trigger has been established or installed. The alarm sequence resource 80BD1525 is a model with components 80C70B84 and 80F1F165; those are extracted in `build/first-encounter-audit/tags`. Inspect their effect graph before inventing another hop-on or treating a label search as proof that the effect does not exist. The existing alarm sequences remain triggered during the warning phase.
-
-## Validation and install
-
-Release DLL build passed, all 22 portable tests passed, all five Lua suites passed, and `git diff --check` passed. Full-route simulator peak: 234 variables, 61 intents/event, three timers, 13,000 instructions/event. These tests do not validate rendered effects.
-
-Installed 19 files, including the settings change, with complete prior-file backups and SHA-256 verification. No game launch, save edits or SDK changes.
-
-Backup: `/home/millie/Documents/Sunrise-builds/mission-ember/build/post-opus-install-backup-20260906-150504`.
-
-Installed DLL SHA-256: `be4ea67c9399c4f08c41eac25f9ed2f27152dff9876b449e8d0d60e7fc9a0139`.
-
-## Scope correction
-
-Loading suppression is now limited to 1AU using the native loading activity reader above. The corrected Release DLL built successfully, and offline signature/call validation passed. Installation was deferred because the game was running; the installed DLL still has the earlier global override until this build is copied after the game closes. The scope-only installer is `/tmp/install-ember-loading-scope.py` and preserves settings, saves and scripts.
-
-## Lime regression and rollback — 15:42
-
-The attempted Message 1 state-ordinal change in a312bdc is INVALID for this client. Live logs explicitly reject world creation with `slice-set state > 1 chosen`, then `Failed to instantiate world and start game` / `Failed to change world to mission_ember`. Do not reinstate the assumption that cinematic ordinals 1/2 belong in the per-bubble Message 1 field. The byte-bias unit tests checked our invented mapping, not the client's accepted semantics.
-
-Reverted a312bdc in 7440f06, rebuilt successfully and passed all 22 portable tests. Installed the rollback while the game was closed, with backup and hash verification. The 1AU-only C24490 suppression guard remains; no game launch, settings or save changes. Ending playback and the beam screen pulse remain unresolved. Log evidence is archived in `build/lime-20260906/sunrise.log`.

+ 0 - 73
MISSION_EMBER_REACTOR.md

@@ -1,73 +0,0 @@
-> Follow-up: the latest live run reached the escape endpoint but reported missing beam effects/hazards, a stale mission banner, inaccessible Controllers, and a freeze before the ending movie. See the newest entry in MISSION_EMBER_STATUS.md for the fixes and their remaining gameplay checks. The earlier candidate below was not visually validated for those behaviors.
-
-# 1AU reactor sequence — 2026-09-06
-
-Reference: [1AU playthrough, 21:20–28:30](https://www.youtube.com/watch?v=PqurUhqC2CE&t=1280).
-Reviewed the cached video across that interval at five-second spacing, then at one-second
-spacing around 23:28–24:27, 25:32–26:39 and 27:15–28:04. Subtitles informed dialogue placement;
-this was visual inspection, not an audible validation of the music mix. Contact sheets live
-in `build/reactor-audit/`.
-
-## Reference sequence
-
-| Time | Observed behavior | Implementation |
-| --- | --- | --- |
-| 21:25–22:55 | Interceptor/security fight, then access to Light's End | Two authored single-Psion squads `DISPENSER_SUPPORT_A/B` form the Electron Controller gate; boarding and support deaths do not open the security door. |
-| 23:00–23:10 | Ghost reacts to the weapon, then identifies thermal exchangers | Cue 40 at entry, cue 41 nine seconds later; duplicate triggers do not interrupt/replay either. |
-| 23:38–23:44 | Beam/screen surge and transition, then opening clamshell shutters | Native placed laser/ring objects are active. Their device transitions and alarm sequences precede the exposure window. |
-| 23:44–23:54 | Purple target exposed for roughly ten seconds | Six-second warning, ten-second exposure, fourteen-second recovery. This thirty-second clock is reconstructed pacing, not a recovered original script. |
-| 23:51 | First target destroyed; “That's one!” | Valid health/death receipt sets one durable completion bit and activates its authored destruction scene. |
-| ~24:55–25:00 | Second target destroyed; access across bridges to central island | Both deaths extend both bridges, unlock/power central doors, instantiate the central target and activate coffin squads. |
-| 26:00–26:04, 26:25–26:30 | Central target visible, shutters close, reopen, then target destroyed | Central doors and shield inherit the same clock, including an exposure already in progress. They continue cycling until central target death. |
-| 26:30 | Ghost says temperature increase is insufficient | Cue 48; leave central housing open, unlock the fusion-cell route. |
-| 27:28–27:38 | Ghost identifies fusion cell; player picks it up | Cue 50 via approach or first ownership receipt. Native `MOTHER_BRAIN_CARRY_OBJECT`; recovery handles expiration. |
-| 27:45–28:00 | Cell carried to reservoir and inserted | Carry navpoint switches to `EMBER_DIRECTIVE_REACTOR_MOTHER_BRAIN_DELIVERY_NAV_POINT`; accepted deposit consumes the cell and begins escape once. |
-| 28:01–28:30 | Overload/explosions, escape objective, Ghost calls Zavala | Existing authored explosion scenes/escape devices, cues 51–54, escape checkpoint and two ending cinematics retained. |
-
-## Mechanisms fixed
-
-`80809562` damage Sense fields health and shield have reflected flags `0x21`, meaning
-mandatory raw float fields. Optional presence is bit `0x02`. The previous decoder consumed
-nonexistent presence bits, misaligning both health and the Auth revision. Native `A774B0`
-publishes the fixed twelve-byte health/shield/revision state. Independent complete packet
-fixtures now cover healthy, zero-health and absent-target (-1) states, plus truncation.
-Lua accepts only the current encounter generation and a previously observed living target.
-Unloading an object, initial zero, old generations and duplicate deaths cannot advance it.
-Redundant lethal-effect requests after an already acknowledged target death were removed.
-
-Sequences previously searched only the opening seed state. Scene lease validation likewise
-rejected a later local state even with a current connection/region. The resolver now prefers
-the exact live-region occurrence; the existing seed occurrence remains a fallback for shared
-mission objects. It retains scenario, object, publication, connection generation and ambiguity
-checks. Scene validation also accepts the exact live state. This addresses the recorded
-`target_unavailable` alarms and the later-area scene `wrong_state` failures without changing
-the selected mission state or republishing the opening area.
-
-Device position, power and lock are independent lanes. The central doors are initially
-closed/locked, then explicitly unlocked/powered at the second clamshell death. The reactor
-bridges use position 1 initially and position 0 to extend, consistent with the reported inverse
-pose and the landing bridge. This final-area polarity still needs visual confirmation.
-Beam VFX use the authored `SPECOPS_APEX_RING_LASER_DEVICE`/`RING_DEVICE` and their placed
-models (`80B7117C`, `80B711C6`, `80B71218`); no fabricated overlay or model replacement.
-The exact beam/screen response and animation latency require an in-game check.
-
-Native music sections now advance through access, controller defeat, weapon reveal,
-thermal-exchanger combat, first destruction, central access/exposure, cell and escape
-(19–28). Section identities beyond the recovered `tumbler_exit` are inferred from bank order;
-finale section names and audible transitions have not been independently verified.
-
-## Validation and installation
-
-- Release DLL builds; 21 portable native tests pass, including mandatory damage packets and
-  live-region behavior selection (missing area, cinematic state, foreign scenario, ambiguity).
-- Five Lua suites pass. Full route exercises both-controller gating, warning before exposure,
-  both-clamshell bridge gate, central object creation and repeating doors, stale/missing health,
-  wipe generation, cell expiration/recovery, consumption, duplicate events and ending cinematics.
-- Peak full-route usage: 228/512 variables, 61/63 recorded intents per callback, 2/32 timers,
-  13,000/100,000 instructions per mocked callback. Reset publishes cleanup before repopulating.
-- Core initialization is deferred while outside region zero and idempotent on duplicate timers.
-- Installation manifest: `build/reactor-finale-installation.json`. Existing save/settings/SDK
-  and prior replication cleanup remain intact. Game launch is manual.
-
-This is an implemented and regression-tested candidate. Native sound, screen surge, actual
-bridge pose and shutter timing remain live-test observations, not established results.

+ 0 - 111
MISSION_EMBER_REFERENCE.md

@@ -1,111 +0,0 @@
-# 1AU reference and implementation evidence
-
-Finale review and current implementation: [reactor evidence](MISSION_EMBER_REACTOR.md).
-The detailed September 6 review supersedes the earlier coarse finale rows below.
-
-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 | catwalk entry/mid and pipe squad families | individual actor timing remains 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.
-
-## Arrival cutscene reference
-
-Additional reference supplied by Millie: https://www.youtube.com/watch?v=IUmUYHELJAY
-(21 seconds, inspected locally at two-second intervals on 2026-09-05).
-The clip shows the player's ship approaching the Almighty, a view along its exterior weapon
-structure, and a fade to black. Millie specifies the sequence as ordinary fly-in, then this
-cinematic, then player spawn. The clip itself ends before the player spawn.
-
-The controller selects authored cinematic state 49 and its type-6 bookend slot, then selects
-playable state 64 only on that slot's termination event. Early region-64 reports cannot start
-landing combat or guidance before that handoff. No 21-second timer is used: completion/skip
-comes from the cinematic event. The selected package asset and the absence of a premature
-player spawn still need visual confirmation in the running game.
-
-## Opening-route correction
-
-The start of the video precedes the catwalk fight; it is not the Mercury console platform.
-Native build-data lists the `default` spawn cluster at approximately (-400, -192, -1.9).
-The generated SDK's arrival-dialogue volume surrounds that cluster, followed by catwalk
-15/35/50/80/100-percent volumes and then pipe/Mercury volumes. The previously selected
-`powerhouse_landing_mercury` set around (-444, 74, -36.5) skips that opening route.
-This spatial evidence corrects the earlier hash choice; a manual playtest must still confirm
-which registered point the client actually selects and its facing/grounding.
-
-Frame inspection was repeated for the opening contact sheet. It shows catwalk combat before
-the pipe view and weapon-active exchange around 01:35. The Lua policy now stages the early
-squad families and cue 2 on authored route events, with no video timestamps used as timers.
-
-## Bridge video recheck — 2026-09-05 14:43
-
-Half-second frame sequences3:10–3:34.5 saved in
-`build/first-encounter-audit/reference-arrival-01.jpg`, `reference-arrival-02.jpg`,
-`reference-drop-01.jpg`, and `reference-drop-02.jpg`.
-Ghost deployment is visible around3:15; machinery moves3:16–22; ships sweep into view3:24–29
-while the player crosses, followed by troop combat. The camera does not follow every ship
-or show a complete passenger manifest, so these frames cannot establish all four cargo pairings.
-
-Native curve captures prove that the earlier path packet selected the start marker. Entry/exit
-now select marker1. See `transport-curve-markers-20260905.json` for all eight endpoints and
-native parameter arrays. Values4/5 are spline coordinates, not elapsed flight seconds.
-
-
-## Full route review — 2026-09-06
-
-Inspected the saved reference video again using closer frame sequences in
-`build/full-mission-audit/processing-*.jpg`, `processing-cell-*.jpg`, `cinder-*.jpg`
-and `apex-*.jpg`. Offsets printed on those contact sheets are relative to the start
-shown after the plus sign, rather than absolute mission time.
-
-| Reference time | Observed action | Script gate |
-|---|---|---|
-| 05:30–06:40 | Processing entry fight and blocked grinder | Authored entry squads, obstruction/control volumes |
-| 06:44–07:02 | Hologram explanation and fusion-cell pickup | Control trigger, native pickup/ownership receipt |
-| 07:08–07:14 | Cell deposited; control becomes active | Accepted receptacle interaction |
-| 07:15–09:45 | Defend machinery, clear rock obstruction | Three authored defense groups, accounted squad deaths |
-| 10:18–11:18 | First Sunside ready room and window/exit | Room squads and exit devices |
-| 11:18–13:30 | Exterior bridge and shade route | Authored deck triggers and native squads |
-| 13:32–15:08 | Second ready room fight | Two groups; room checkpoint |
-| 15:20–16:44 | Red passage, machinery rooms and ascent | Chamber/meat-grinder/ascent trigger sequence |
-| 17:08–20:08 | Foundry/Bruiser, then exit opens | Three Foundry groups, hatch and ramrod devices |
-| 20:32–22:00 | Energy stream, security area and Interceptor | Chute-volume restrictions and authored vehicle object |
-| 22:50–27:20 | Light's End, exchanger/core destruction, cell | Native damage monitors followed by cell delivery |
-| 27:30–28:40 | Cell overload and upper-rail escape | Native receptacle, scenes, escape-end volume |
-| 28:42 onward | Escape exterior movie, then character cinematics | Authored STM/CNN states and matching termination incidents |
-
-The recording establishes action order, not exact native condition logic. Vent cadence
-is an explicit reconstructed18-second-open/8-second-closed policy pending live tuning.
-The recording does not prove every reinforcement trigger or optional squad timing.
-Native slot positions and combat objectives supply placement/AI. Do not describe these
-Lua files as recovered retail mission scripts.

+ 0 - 190
MISSION_EMBER_SQUADS.md

@@ -1,190 +0,0 @@
-# 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 |

+ 0 - 1353
MISSION_EMBER_STATUS.md

@@ -1,1353 +0,0 @@
-## Finale follow-up — 2026-09-06
-
-Changes for the latest full-run reports:
-
-- Beam warning/exposure now drives laser/ring power as well as position. The previous script only moved the devices after leaving their power continuously enabled.
-- Escape enables the authored thermal hop-on over the five narrow hot-pipe volumes and the authored rail-top effect over its authored volume. Effects are removed before ending transit and re-armed on an escape checkpoint restart. Hazard setup has its own callback.
-- HUD directives now name their type-70 engagement audience. Native `1009740 -> A76E90 -> 9F1290` checks membership; `9F2A00` includes active players with flag bit 0 clear. Earlier messages left this reference absent. The public Lua API validates this as type 70, independently of type-34 damage filters.
-- Both ending movies transit through the existing, unplayed region-49 cinematic staging state before selecting their own region. The observed region-0 -> region-1 same-bubble teleport stalled before cinematic start; staging makes both bookend transitions cross-bubble. Movie playback still waits for held-region acknowledgement and exact completion/skip incidents.
-- Electron Controllers retain individual access-objective task assignments (groups 9/10, task slots 40/41), rather than following later cost changes into other access tasks. The group-to-controller association is inferred from the authored dispenser task ordering and needs a spatial gameplay check; native combat logic remains enabled.
-- The squad observation pool reuses empty records when full, preserving records that still have population or accounted slots.
-
-Installed 18 files into the closed game at 11:24, with backup `build/finale-followup-backup-20260906-112405`. DLL SHA-256: `1ca1f37ea2735aeb0dab6d4a7522937e3cc9c66a5a09b297448253081887f97c`. Save, settings and SDK files were preserved. Manifest: `build/finale-followup-installation.json`.
-
-Validation: Release DLL built; 21 portable C++ tests and all five Lua suites pass. The full route uses 233 durable variables, at most 61 intents and 13,000 mock-instrumented Lua instructions per callback. Added coverage for engagement reference encoding, multi-volume effect filters, fixed controller assignments, and staging arrival before ending playback. No game was launched or stopped.
-
-**Gameplay verification remains required:** beam/screen visuals, exact Controller task geometry, escape damage amounts, visible banner refresh, and completion of both staged bookends. The automated checks validate script progression and wire shapes; they do not prove these visual/native behaviors. The tube/noclip issue was left alone as requested.
-
-# Latest script fix: foundry callback sandbox compatibility
-
-Installed 2026-09-06T10:46:06.446791; backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/foundry-script-backup-20260906-104606`.
-Only `mission_ember/cinder.lua` changed in the game. DLL/settings/save unchanged.
-
-Live playtest fault at t617130: `cinder.lua:122: attempt to call a nil value (method 'find')`.
-The production sandbox removes string pattern functions. Replaced the prefix search with
-exact checks for the three foundry groups. The full-route harness now removes the same
-string functions during callbacks: it reproduced the original fault, then passed after the fix.
-The full route still peaks at 228 variables, 61 intents/callback, two timers and 13,000 mocked
-instructions/callback. No tube behavior changes; user identified tube deaths as a noclip issue.
-
-This run confirmed refinery cell consumption and healthy entity reclamation through the
-foundry. The script fault prevented validating the reactor finale. No game launch performed.
-
-# Latest installed: reactor finale progression and native behavior routing
-
-Installed 2026-09-06T10:29:48.150688 — DLL `bed8bd2ac98f4f39e0229c0535540c2360b6697c609360bb3aab792a0efab8c4`.
-Backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/reactor-finale-backup-20260906-102948`; all 18 files hash-verified. Game closed; settings/save/SDK preserved.
-
-[Reference review, changes and validation](MISSION_EMBER_REACTOR.md).
-
-- Interceptor exit requires both authored Electron Controllers; supports/vehicle boarding do not gate it.
-- Correct mandatory-float damage Sense decoding unblocks clamshell/central target death events.
-- Retracted initial bridges, both-target extension gate, central target creation, explicit door unlocks,
-  shared warning/exposure/recovery cycle, fusion-cell route and consumed deposit to escape.
-- Authored later-area sequences/scenes resolve the live state while retaining the original seed lease.
-- Spaced finale dialogue, carry delivery navpoint, intermediate music milestones; native beam/ring controls.
-- Release DLL, 21 native tests, five Lua suites and diff checks pass. Full route peak: 228 variables,
-  61 intents/callback, two timers, 13,000 mocked instructions/callback. No game launch performed.
-
-Live confirmation still required for final-area bridge polarity, beam/screen VFX, alarm/shutter cadence,
-and audible music selection. Music section mapping and pacing are reconstructed, not original recovered Lua.
-
-# Previous installed: native entity transmission and purge sequencing
-
-Installed 2026-09-06T09:55:13.047971 — DLL `d6bc68793c009a883a420095a7c908a2a60a71336aea204aca2e6cc51559fbd4`.
-Backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/replication-gate-backup-20260906-095512`; 18 files hash-verified. Settings/save/SDK preserved.
-Game was closed throughout installation; launch remains manual.
-
-09:40/09:41 read-only captures found 57 then 93 detached authority records, all waiting
-for the remote host. Native entity view +8/+9/+10 was 1/1/0, pending packet list empty,
-and server packet/ACK counters advancing. Membership field6 (mask_6) was omitted.
-Native 4DF650 reads that mask; 1702580 passes its member bit to 1703910, which calls
-16E5F30 to set parent view +178 (entity subview +10). 1712E70 refuses all transmission
-while that byte is zero. The new membership encoder explicitly publishes the remote
-host bit, or zero when no host is advertised. Sizes increased by 32 bits.
-
-Also corrected msg25 sequencing: the earlier area purges freed records but logged
-`incorrect epoch sequence` (first at t79981, then every area purge). Native 170B030
-requires the entity manager's own epoch +1. This is separate from msg44/common
-replicationEpoch. New binding-owned authorityEpoch starts at zero, increments only
-after the authenticated response is published, and refuses overflow at 255. Exact
-client-requested masks are retained; msg33 remains excluded.
-
-New 15-second `ev=entity_replication stage=census` diagnostic reports authority record
-count (capacity512), detached records, enabled views, pending deletions, packet-bearing
-views, and the native authority epoch. Reads only; native deletion/ACK handlers own frees.
-
-Validation: 20 portable native tests, five Lua suites, full cross-compiled DLL and
-`git diff --check` passed. No live validation of this candidate yet. Next check: kill
-opening enemies and verify enabled=1 and detached/pending deletions fall, then cross
-into Refinery and confirm successive purge epochs without assertions. Full mission
-Bird resolution remains unconfirmed. The prior run reached reason15 at612/1024
-simulation records despite successful area reclamation and fusion-cell consumption.
-
-# Previous installed: native authority cleanup and cell consumption
-
-Installed 2026-09-06T09:23:06.986888 — DLL `8d8772ec2c61bd60421f4f8b0600dc30697160fcf1c31040304b277a6bf8824c`.
-Backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/authority-cleanup-backup-20260906-092306`; 18 files hash-verified. Settings/save/SDK preserved.
-
-Client abandoned-slot and purge requests (26/27) now receive exact-mask native purge (25)
-responses with connection/session/epoch checks. Msg33 live authority transfers are excluded.
-Replication epoch state advances when msg44 publishes. Valid transport ACKs release outgoing
-contributions independently of unsupported incoming gameplay lanes, preventing an exhausted
-packet ring from blocking further acknowledgements. Native record census logs changes every
-15 seconds, including below high pressure. Accepted cell deposit explicitly consumes the pickup
-with a newer inactive Auth generation; recovery stays disabled after completion.
-
-19 portable tests, five Lua suites and cross-compiled DLL passed. Full route:221 variables,
-60 intents/event,1 timer,12000 mocked instructions/event. Native reclamation/Bird resolution
-and carried-cell removal **still require live confirmation**. Latest failure was1024 records,
-with742 detached records in the1019-record capture. See `build/full-mission-audit/PLAYTEST-20260906.md`.
-
-# Previous installed: native squad cleanup
-
-Installed 2026-09-06T08:55:26.045462 — DLL `54c842f6d41c86e2ae675d8176ae0196c15c809bddb8e9b6059106c7fc05358b`.
-Backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/native-cleanup-backup-20260906-085525`;18 files hash-verified. Settings/save preserved. No game launch/stop.
-All-zero squad requests now select native destruction (.18=0), with a fresh generation,
-instead of the previous kill-only path (.18=1). Covers existing encounter retirements and
-checkpoint resets; positive spawn requests are unchanged. Native simulation-record usage
-is logged at high pressure and on recovery.17 portable tests/build passed; Bird resolution
-requires live test. Pool remains1024. Refinery/Sunside fixes below remain installed.
-
-Main objective wording remains authored: Find and disable... until reactor; then Sabotage...,
-Deliver the final blow..., Escape... . User was informed; no speculative HUD change applied.
-
-# Latest installed correction: Refinery ownership/prompt and Sunside timing
-
-Installed 2026-09-06T08:48:06.799329 — DLL `e628a3bf733b40546b459f5c944e6589f7a7f87df756292a66d12a750e0686fd`.
-18 files hash-verified; backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/refinery-fix-backup-20260906-084806`. Settings/save preserved; no game launch/stop.
-Cell ownership decoding now uses required raw64 (80809ACC flags0x21 are custom, not optional).
-Removed inverted enable_use override: F32CD0 confirms native+704=false permits unfiltered use.
-Sunside indoor checkpoint/darkness/dialogue moved beyond the far door; outdoor spawn preload remains.
-17 portable tests and5 Lua suites passed. Needs live verification. Bird remains reason14 native
-simulation records1024/1024; user asks about increasing pool allocation, investigation active.
-
-# Ember implementation status
-
-## Opening regression corrected
-
-Installed 2026-09-06T08:15:31.534338;18 files hash-verified. Backup `/home/millie/Documents/Sunrise-builds/mission-ember/build/opening-regression-backup-20260906-081531`. Settings/save unchanged; game was closed and was not launched.
-
-Fixed missing-Sense-payload eligibility that faulted the mission before later opening triggers.
-Restored the working generic lever interaction; explicit use override is now receptacle-only.
-17 native tests and opening/full-route Lua regressions pass; fresh live test required.
-
-## Playtest corrections — installed 2026-09-06
-
-Installed 2026-09-06T07:37:29.613768: DLL and17 Lua files, all18 SHA-256 verified.
-Backup `build/playtest-fixes-backup-20260906-073729`. Manifest `build/playtest-fixes-installation.json`.
-DLL `8d28a8a686cc97c32dc08c510e5b640098f7f8dba729755af37de83e8623ce45`. SDK/settings/save preserved. No game launch or stop.
-
-Refinery/drill/cell recovery, end-of-drill hatch, interceptor-use door, objective mapping,
-clamshell/core progression, and native music support are implemented and built. See
-[playtest audit](build/full-mission-audit/PLAYTEST-20260906.md) for exact mechanisms and evidence.
-Valid partial Sense packets now preserve complete object/damage/life receipts. Reactor targets
-also accept guarded native present/alive-to-dead receipts. Clamshell power/lighting and alarm
-cues are wired; music uses authored section bits with inferred milestone timing.
-
-17 portable tests and all five Lua suites pass. The current candidate still requires live
-verification of insertion, boarding, shield visuals, core progression and audible music transitions.
-The earlier Bird pool-exhaustion cause remains unresolved. This is not a claim of a finished mission.
-
-## Full-mission candidate — installed 2026-09-06T01:32:12
-
-DLL SHA256 `1d33c97e9baec406afe12c614f11f2e81b87bfc1b24eef99b6bd01f540afb78e`. Backup `build/full-mission-backup-20260906-013148`.
-All 2130 DLL, SDK and script files were staged and SHA-256 verified.
-Settings/save were preserved; the game was not launched or stopped.
-Installer `/tmp/install-ember-full-mission.py`; durable manifest
-`build/full-mission-installation.json`.
-
-The implemented route now continues from the verified Powerhouse through Processing,
-Sunside, Foundry, Light's End, both reactor sides, core, cell overload, escape and the two
-ending cinematic states. The root controller dispatches native object-use/ownership and
-damage receipts. Ending completion/skip incidents advance only once; lifetime6 is selected
-after the second movie, based on the native completion/reward branch.
-
-The regenerated SDK is `build/sdk-mission-complete`. Audit:
-162 squad sensors,173 runnable definitions, zero sensors without definitions. Eleven
-Processing sensors legitimately point to two authored rule descriptors. Scoped source
-resolution now separates campaign and arcade reuse; Lua addresses every sensor once.
-The new120 later squads all retain authored combat objectives and task-group bounds.
-
-Processing includes native cell pickup/deposit, staged defense, machinery, debris, hatch
-objects and exit. Cinder includes ready rooms, exterior/retreat squads, chamber/meat-grinder/
-ascent, Foundry and beam restrictions. Apex includes access/security, Interceptor, vent
-and core objects, native damage monitoring, reinforcements, overload and escape scenes.
-Cell recovery is generation-aware and waits while its region is unloaded. Missing target
-health or an initial zero cannot advance destruction. Completed areas preserve the current
-forward objective on backtracking.
-
-Checkpoint sets: Mercury `9C58857A`/64; Processing `4B27745D`/56; ready room2
-`82328D63`/40; Foundry `782CAF4C`/40; reactor entrance `DF59C25C`/0; escape
-`45920385`/0. These are package spawn sets, not invented coordinates. Reactor reset
-publishes removals first and repopulates on the first playable post-wipe client update.
-The native countdown/3-versus30-second respawn policy is unchanged by the later route.
-
-Validation: Windows DLL built;16 portable CTest tests passed; five Lua suites passed,
-including the full route using the final generated SDK, every later checkpoint, skipped
-narrow volumes, backtracking, lost cells, duplicate cinematic receipts and stale damage.
-Full-route simulation:213 later variables (Powerhouse suite63),59/63 intents per busiest
-event,12,000/100,000 Lua instructions per event including the mock,1/32 timers peak.
-All162 authored squad sensors are mapped exactly once. Simulator population reports
-exercise native member lanes; a simulation is not evidence of actual actors appearing.
-
-### Remaining live validation and reconstruction limits
-
-This is an installed full-route implementation candidate, **not a completed live playthrough**.
-The user-confirmed landing baseline is preserved. Later placements, AI task choices,
-pickup consumption/expiry/co-op ownership, object animations and authored spawn-set
-suitability must be checked in the game. Native navpoint display, visible respawn timer,
-Darkness Consumes You presentation, total wipe timing and multi-client membership also
-await manual validation.
-
-Vent timing is reconstructed18s open/8s closed, not recovered retail timing. The native
-lethal target effect is applied only after a confirmed health-zero receipt; it is not an
-invulnerability switch. The thermal/exposure system is incomplete: deck heat shimmer and
-scoped grinder damage are wired, but sunlight-versus-cover damage, Foundry/core thermal
-hazards and the timed escape-failure damage policy are not fully reconstructed. Beam
-restrictions are wired; propulsion relies on the map's native behavior and remains unverified.
-The optional catwalk secret lever and three skybox flyby objects retain the existing baseline.
-Final STM/CNN movie ordering and lifetime6's post-movie behavior need a live check.
-Do not call the whole mission retail-equivalent or fully playable until those checks pass.
-
-Earlier status entries below are historical; the installed build above supersedes them.
-
-
-## Navpoints and checkpoint wipe — installed 2026-09-06 00:04
-
-Candidate DLL SHA256 `7e58c4d61dfb4e6996f29a59a0c340ff3bacf3ded99b55e4b5dea0c6fbb338f0`.
-Backup `build/nav-wipe-backup-20260906-000436`; installer `/tmp/install-ember-nav-wipe.py`.
-Nine runtime/script files installed and SHA-verified. Settings/save unchanged; no game launch/stop.
-
-Type68 now accepts `set_directive{directive=..., navpoint=context:slot(authored_type47)}`.
-Native100A6E0 consumes Auth80804F6B .11 /80804F70 .0 as the target ClientRef.
-Mode2 uses native route/direct destination selection; empty markers carry the proper
-811C9DC5 fallback-name sentinel. Landing Lua publishes Mercury approach, Ghost-console,
-and helipad approach targets outside active encounters. Targets retire on combat/arrival,
-streaming away and wipe cleanup; completed areas do not re-arm them by backtracking.
-This is encounter-based combat gating, not a recovered player threat-meter edge.
-
-The type13 Sense decoder now reports native death-with-Ghost evidence. Joined private
-sessions sharing the exact destination descriptor (including its selection nonce) supply
-alive/dead/unknown counts; public residents are excluded. Loading/unknown peers block
-all-dead detection. This uses Sunrise's current private cohort representation; multi-client
-fireteam membership and simultaneous wiping still require a live co-op test.
-
-All dead during darkness publishes type35's native three-second countdown, then arms
-the E51D80 membership hard-wipe machine. Host state1 starts it; client state2 triggers
-encounter reset; client4 is held until reset outputs are staged, then host4 releases it.
-The authored Mercury spawn set9C58857A replaces mission arrival for the checkpoint.
-Bridge devices retract, Ghost is re-enabled at a new generation, bridge combat resets;
-completed approach encounters and the lever remain complete. Rescanning starts a fresh
-ship lifecycle. Per-attempt timers reject old unload/departure/countdown events.
-
-Validation: Windows DLL build; native wire tests for navpoint encoding, player life,
-visible3/30-second respawn policy and wipe3/2/1/0 timer; Lua tests for revival cancellation,
-unknown-peer refusal, ordered reset/release, repeated checkpoints, old receipts and ship
-replay, plus existing encounter/AI/controller regressions. The countdown timer uses
-host-stepped native clamped values rather than an invented client-clock epoch.
-**Native HUD marker appearance, countdown presentation, Darkness Consumes You fade,
-checkpoint spawn and co-op behavior await manual gameplay validation.** Earlier entries
-below are historical build notes, not the current implementation state.
-
-## Visible respawn countdown — installed 2026-09-05 23:26
-
-User confirmed the old delay appeared as `Finding Spawn Location`, with no visible
-countdown. DC3770/DC1A20 enforce a late spawn-location hold; they do not own the HUD
-respawn timer. Removed that detour and restored participation +736's bypass.
-
-The native death-timer initializer12E9330 reads BF2480(player datum)+4, converts
-IEEE half through F32350, and stores a duration at player+64. HUD1677E60 reads
-that timer through12EE500 (rounded seconds) and12EE590 (fractional seconds).
-BF2480 -> BF4380 resolves type13 Auth+736, as established by FA00D0's binding.
-Therefore Auth80804F30 .3 ->80804F31 .3 (native+740) now explicitly carries
-half30 (`4F80`) during darkness, half3 (`4200`) outside it. This adds16 body bits
-only for a scripted darkness policy. The adjacent optional timer and independent
-awaiting-client-sync flags are preserved. Unscripted participation bodies are unchanged.
-
-Validation: Windows DLL build, darkness-zone wire regression covering both durations,
-loading suppression, release/re-entry, unscripted behavior and following-field alignment;
-Lua controller regression; diff check. Installed eight runtime/script files with SHA
-verification and a backup, preserving settings/save. No game launched or stopped.
-**The visible countdown needs the user's manual test. Checkpoint wipe is still unfinished.**
-
-DLL SHA256 `30e0b6738d9a59d467857511d95a02dbe6652ae28c9b7a4470022f3021d2d0b6`.
-Backup `build/visible-respawn-backup-20260905-232608`.
-Installer `/tmp/install-ember-visible-respawn.py`; manifest `/tmp/ember-visible-respawn-manifest.json`.
-
-Wipe investigation: F9CC60 publishes type13 Sense80804F2F .0 ->808094E4 .5
-(nativeSense+11) from player+2112==-1 AND a valid handle from352310(player+80).
-This is stronger death evidence than missing actors during loading. No player_died4053
-incident was present in the latest archived run. Existing VM peers are destination peers,
-not proven fireteam members; do not blindly use their count for a fireteam wipe.
-Hard-wipe globals still publish mode1=-1 and an inactive timer. An actual checkpoint
-reset must also rearm the relevant encounter, ship generations and timers exactly once;
-ordinary region return remains one-and-done.
-
-## Native Harvester exit action — installed 2026-09-05 22:57
-
-User confirmed ship exits work in the subsequent manual test. Archived log:
-`build/first-encounter-audit/ship-exit-respawn-test-20260905.log`.
-All four ships logged native action7B0D3643/index4 starting; live metadata uniquely
-resolved group07EBF354. The countdown correction does not change ship behavior.
-
-Recovered exact FNV-1 names in resource `80FE21CE`:
-`enter_90` 4182A2AF / index0, `enter_25` 4A82B0FF / index1,
-`enter_45` 4482A76D / index2; `exit_90` 820D414B / index3,
-`exit_25` 7B0D3643 / index4, `exit_45` 7D0D39A9 / index5.
-This replaces the earlier unmapped-action hypothesis. The shallow `exit_25` is selected
-and the user has now confirmed the departure works.
-
-Lua now sends one `play_actor_action` at second-path completion. The actor generation
-stays1; action revision advances from2 to3. It retires at revision3 completion or death,
-not revision2 completion. A separate four-bit variable prevents repeats after duplicate
-receipts or script reload. The existing6-second unload delay and4-second departure hold,
-A/B manifests, empty C/D ships, working doors and encounter progression are retained.
-
-Native wire: root80807DA1, .6 one kind9 command,254 bits. It carries group, action,
-empty additional identity, absent spatial target and mode/marker -1. Composition keeps
-the completed delivery request and actor generation. New API is documented in the Lua
-contract and accepted by the server's bounded Auth validators.
-
-The native consumer is A97800 -> AB4030 -> A054C0 -> A889E0. The group name is
-not guessed: for an empty-group exit request on actor80FE22FC only, the A054C0 hook
-searches that live actor's native metadata for exactly one group containing the authored
-variant. It passes the recovered group back through the original native lookup, retaining
-both native output indices. Missing/ambiguous tables fall back to original behavior.
-The latest live run confirmed unique group07EBF354 resolution. All unrelated
-actors/requests pass through.
-
-Hooks1023F00 and10245C0 observe native sequence start/stop. They do not force playback,
-completion, deletion, or change controller state. Logs: `harvester_exit stage=resolve`
-(unique/result), `harvester_action stage=start` (active index), and `stage=stop`.
-The stop log can also represent cancellation; compare it with revision3 Sense completion
-and the visible effect. Exact executable prefixes guard all new detours.
-
-Validation: Windows DLL build; mission Auth composition/action validation test including
-preserved cargo/generation and malformed padding; existing path packet regression;
-Lua controller (61 durable variables), encounter and AI tests; diff check. Controller
-covers duplicate/reloaded path receipts, action-running receipts and one retirement per
-completed action. No game launched or stopped. Eight installed files SHA-verified;
-settings/save preserved.
-
-DLL SHA256 `96601b6c1a71dd7f930499f4af39265ca08da0f9d8b69d4c53dc5f1aef1ab3d3`.
-Backup `build/native-exit-backup-20260905-225739`.
-Installer `/tmp/install-ember-native-exit.py`; manifest `/tmp/ember-native-exit-manifest.json`.
-All-dead checkpoint wipe remains unfinished from the earlier scope; this update focuses
-on the ship departure request. Earlier skip and3/30-second respawn changes remain installed.
-
-## Respawn timing correction — installed 2026-09-05 22:34
-
-User explicitly requires30 seconds while darkness is active and3 seconds otherwise.
-DC1A20 hook now selects30/3 via native E4C110 instead of falling back to content's
-possibly-negative delay. Type13 Auth retains the native timer in BOTH states when
-hasDarknessPolicy is set; the previous unrestricted bypass would skip the3 seconds.
-Missions without a scripted darkness policy retain their existing participation body.
-Initial spawn and loading suppression remain native. All-dead checkpoint resets and
-ship exit effects are still unfinished; native timing/HUD awaits the manual test.
-
-Build, darkness-zone wire test (activation/release/re-entry/loading/unscripted cases),
-mission controller regression and diff check passed. Eight installed files hash-verified;
-settings/save unchanged; no game launch or stop.
-DLL SHA256 `8a962d5061a3f4dd00535fe3af634ed86e360ad94efd9a32fc24919fd8314ee6`.
-Backup `build/respawn-3-30-backup-20260905-223418`.
-Installer `/tmp/install-ember-respawn-3-30.py`; manifest `/tmp/ember-respawn-3-30-manifest.json`.
-
-## Escape skip and respawn-delay test build — installed 2026-09-05 22:31
-
-Harvester doors are user-confirmed working. Ship exit visual remains unresolved;
-no entrance-effect replay has been implemented. Working ship timing/cargo and
-opening combat progression were retained.
-
-`cinematic_skip`: SObject3338, FNV-1 `7352DAFE`, type17/schema808087BF.
-Native1069BE0 checks the active/skippable cinematic and emits it through106A530,
-the same payload builder as cinematic_started5239 and cinematic_finished1685.
-Existing game logs already contained two ignored3338 requests. The host now
-resolves this exact cinematic ClientRef and emits cinematicSkipRequested. Lua
-uses the same idempotent stop/select-landing-state handoff as natural completion.
-A later finished notification or repeated Escape cannot repeat that handoff.
-
-The native DC3770 respawn gate compares elapsed time since player+1928 against
-DC1A20's delay. New guarded darkness_respawn hook returns30 seconds while the
-native E4C110 darkness flag is active, otherwise preserving the authored delay.
-It keeps the native gate/timer and initial-spawn bypass. **Neither visible HUD
-countdown nor multiplayer behavior has been verified. All-dead checkpoint reset
-is NOT implemented in this test build; the delay alone is not a team-wipe policy.**
-
-To verify player identity before writing a wipe reducer, bounded read-only
-player_lifecycle logs retain incident payload chunks, sending session, primary
-row, and lifecycle target mask. The death can be an EXTRA target of a combat
-incident; do not equate sender with victim. Known targets:1121 player_spawned,
-439 player_respawned,7117 player_resurrected (schema80806437),4053 player_died
-(schema8080645A),5896 hard_wipe (schema808087B7). No reset is triggered from an
-unverified death field. Required policy:30-second respawn while a teammate lives;
-all members dead => wipe to beginning of latest objective; retain earlier cleared
-sections except when deliberately resetting the selected checkpoint.
-
-Next manual test: Escape during intro, then a death after Ghost extends the
-bridge/activates darkness. Capture the lifecycle payload and actual respawn delay.
-No game launch or stop by the agent.
-
-DLL SHA256 `ccc0ed14ad0beec131a81056c7a70d3753f6f7e56b498ba4ac71249c7e5c049b`.
-Full DLL build, incident payload test and three Lua suites passed; diff check clean.
-Eight DLL/Lua files installed and SHA256 verified. Settings/save unchanged.
-Backup `build/skip-respawn-test-backup-20260905-223142`.
-Installer `/tmp/install-ember-skip-respawn.py`, manifest `/tmp/ember-skip-respawn-manifest.json`.
-
-## Combat and Harvester doors confirmed — installed 2026-09-05 20:06
-
-User reports all opening enemy spawns correct and darkness clears correctly. Harvester doors are now user-confirmed working. The
-remaining opening blocker is the visible exit effect. Keep the successful
-Lua spawn/timing/progression behavior stable. The user explicitly suggests replaying the
-entrance effect before deletion as a reversible exit experiment; this is authorized, but
-the actual entrance-effect trigger is NOT yet identified or implemented.
-
-The 19:32 hook installed but emitted no per-release reports. Its filter had two incorrect
-assumptions: source-root+4 is a resource-header field, not the owning actor tag; native
-resolution from the live delivery instance points to class8080670A, not8080670B. The
-new filter matches all four captured ships against the actual extracted resource.
-
-Read-only manual bridge capture: `ship-runtime-20260905-195257` through `195317`.
-Summary: `build/first-encounter-audit/animation-live-summary-20260905.json` (150 component
-samples). Delivery advances0 ->3/published1 ->5 ->0. Its four bindings+80/+160/+240/+320
-are all invalid references in live memory AND the authored80FE22FC overrides. Forcing
-these actions cannot open this model's doors. Separate80FE21CE controller has13 populated
-sequence bindings but active-action stayed-1 in our 1Hz samples. This does not rule out
-an action shorter than the sampling interval. Mapping saved in `harvester-action-map.json`.
-
-Found actual door names in model controller80FE22CB: source starts0x198, `dropship_doors`
-at file0x368 is FNV-1 D784D6F6, and `doors` at0x388 is FNV-1 80296344. Export80FE2309
-binds the former to class80803D06/feature10;80FE230A binds the latter to feature9.
-These are authored names, not invented native action IDs. Exact target semantics still
-require a visual test.
-
-Replaced the empty-action experiment with a102A5B0 delivery-stage hook. Before native
-stage1, request named scalar `doors=1`; before stage0 after delivery, request `doors=0`.
-Use native576420(entity, &nameHash, alignedFloat4), the same route used by A9BFA0's actor
-set-channel command. Source/tag checks match only80C0E5D2/class8080670A. Native stage
-arguments/result, manifest, flight timing, progression and actor retirement are retained.
-Log `harvester_doors` reports installation and requested values, NOT proof of visible motion.
-
-Installed DLL SHA256 `0667a285b63e64ded6b752c1d0c9736fdfbe976fa73e04470973b9c618cbc31a`.
-Build and three Lua suites passed; exact hook/helper prefixes and corrected source filter
-verified using extracted binary plus all four live delivery captures; diff check clean.
-Installer `/tmp/install-ember-model-doors.py`, manifest `/tmp/ember-model-doors-manifest.json`.
-All eight installed files verified. Backup `build/model-doors-experiment-backup-20260905-200649`.
-Game was confirmed closed; no launch or stop. Settings/save unchanged. The user has now
-confirmed visible door motion. Read-only capture watcher stopped after the four ships retired.
-
-## Forced Harvester pre-drop action — experimental build installed 2026-09-05 19:32
-
-User explicitly authorized trying the embedded component at troop release and reverting
-if it breaks. Added `client/hooks/harvester_drop/harvester_drop_experiment.cpp`, activated
-only with mission scripting enabled. Exact native prologues guard the hook/helpers;
-the release callback additionally checks source-root `80FE22FC` and delivery source
-`80C0E5D2`/class `8080670B`. This is a temporary native experiment, not a verified door API.
-
-Hook `102A920` immediately before native passenger release. Resolve the authored stage2
-pre-drop binding at delivery runtime+240 and invoke `C7AFA0(binding, 3D30F0)` even when
-zero native wait skipped that action. If previous published stage is2, native already
-started it and the hook does not restart it. Invalid binding logs `unbound` and is skipped.
-Keep native release arguments/result, manifest, timing and cleanup. Generic component
-activation through `44E340` already occurs in native `1029330`; this experiment forces
-the separate action rather than merely repeating that existing activation call.
-
-Log event `harvester_drop_experiment` reports installation and each matched release:
-`invoke`, `already_started`, `unbound`, or `exception`. Actual door motion is unverified.
-Build passed; all three Lua suites passed; exact guard bytes checked against extracted
-game image; diff check clean. Game was closed and was neither launched nor stopped.
-
-Installed SHA256 `14f196a2202dfc80219dfed80308201879607df621e6e7d385fa4e99750a7dfe`.
-All eight DLL/script files verified with `/tmp/ember-drop-action-manifest.json` by
-`/tmp/install-ember-drop-action.py`. Backup of previous DLL/scripts/settings:
-`build/drop-action-experiment-backup-20260905-193242`. Settings/save unchanged.
-This installation ALSO includes the previously pending near-side squad changes,
-six-second total unload timer (three additional seconds), and Bird diagnostic below.
-Revert the experiment at source by removing its activation call; the backup restores
-the previous installed runtime and scripts if the entire test build must be rolled back.
-
-## Embedded Harvester controls — investigation, not a door fix
-
-The absence of a named type23 ship-door slot does not rule out an embedded control.
-The user's model-component suggestion is valid. Offline inspection found two separate
-mechanisms in the native delivery component; neither is yet verified as the door animation:
-
-- Delivery start `1029330` calls `44E340(component, source+96, true)`; state0 clears it.
-  Its package binding runs through `80C0E5D2`, the `80FE22C9` instance override, and
-  `815B963A` (component class `80803C23`, feature6). `44E340` resolves that binding and
-  calls `590B10` with category3. The lever animation setter `DF6C70` uses sibling
-  `44E3C0`, which reaches the same helper with category0 and a different binding type.
-  `594CF0` changes component bit masks; `56FD30` updates entity category bitmaps.
-  This is evidence of shared component-activity machinery, not evidence that category3
-  or feature6 means "open doors". Do not expose a guessed door switch from these numbers.
-- Delivery stages invoke bindings at runtime+80/+160/+240/+320 through `C7AFA0`.
-  That function checks the resolved handle at binding+56, returns if it is invalid,
-  otherwise resolves the component and invokes the supplied callback. Start callback
-  `3D30F0` supplies float0 to `58E420`, which forwards to `58E260`; reset/stop callback
-  `587970` clears sequence state and releases its tracked child references via `58E630`.
-  Native stage replication `1028FB0` also switches these actions. These stage bindings
-  are stronger animation candidates, but their actual Harvester targets still need mapping.
-
-Evidence is in `build/first-encounter-audit/live-44e340.c`, `live-44e3c0.c`,
-`live-590b10.c`, `live-594d70.c`, `live-56fd30.c`, `live-1028fb0.c`, and the raw
-disassembly at the listed RVAs in `native-readable.exe`. Old slab captures contain
-source/component references; do not mistake those for the separately allocated delivery
-runtime or treat guessed "door" capture offsets as verified animation components.
-No gameplay patch or installation was made for this investigation.
-
-## Near-side pre-bridge combat, later unload and Bird diagnostic — included in 19:32 installation
-
-Candidate DLL SHA256 `d7d296ef67e8defe3d0a30c65e73a68446fe414b4480d0096d6cd078addfef0b`.
-Release linked; three Lua suites passed; diff check clean. Peak durable variables60.
-Installer `/tmp/install-ember-prebridge-bird.py`, frozen file manifest
-`/tmp/ember-prebridge-bird-manifest.json`. Game PID368720 still running at pre-install check;
-user asked to close it. This was the earlier blocked installation check; these changes
-are now included in the 19:32 experiment installation above.
-
-- Move BOTH `BRIDGE_VIGNETTE_SUPPORT_A/B_SQUAD` into required Mercury combat. Their
-  authored anchors (-420.5,79.5,-36.5) and (-420.5,73.5,-36.5) sit at the near end, not the far
-  monster closets. The eight required squads now gate Ghost. Keep their original bridge
-  AI objective14/task-group cost selection. Optional Crimson Shadow remains lever-only.
-- Actual previous log showed entry completion261710/261845 and native delivery264787/
-  264933: the existing three-second timer was running. Asked user whether to use bridge
-  completion or add three seconds; no answer received before proceeding with the stated
-  assumption of THREE ADDITIONAL seconds. Lua unload now waits3000+3000ms after entry.
-  Four-second post-unload hover and immediate second-path retirement remain intact.
-  If user clarifies the reference point, adjust this assumption; do not claim the old delay
-  was absent or describe the new total as only3000ms.
-- Bird at355562 is a native sobject creation failure: 16EE180 -> 170F190 -> 170B0F0.
-  There were116 free network indices/571 allocations. The last bridge support-A receipt
-  still had three alive at354175; no evidence that completed darkness release caused Bird.
-  Snapshot archived at `build/first-encounter-audit/bridge-clear-bird-test-20260905.log`.
-  The new diagnostic reads native failure reason at the log site before generic reason12
-  replaces it, and counts the separate1024-record simulation bitmap. Original code maps
-  reason11 to state heap,14 to simulation records,15 to authority records. Getter16CC5D0
-  is called read-only behind an exact-byte build guard and SEH; bitmapRVA030B0340 verified
-  against170B2B0 disassembly. No allocation/init hooks or speculative capacity fixes added.
-  Bird is NOT fixed yet; next reproduction must capture this specific reason.
-
-Ship-door answer: existing `transition` API accepts exact type23/device class80804F45.
-The named Harvesters are type2 actors with embedded delivery/animation components and
-have no named type23 door binding in this SDK. The lever call cannot be applied directly;
-we still need a verified native actor/component animation command. No ship-door/warp
-animation claim or guessed hash in this update.
-
-## Lever animation, Harvester hover and bridge-clear scope — installed 2026-09-05 18:55
-
-Lua changes tested against installed DLL
-`13513b002ab38633560f16e59e6dcd2984165540830816c9bba5640cf8d4161b`.
-Three Lua suites passed, diff check clean. Peak durable variables 60. No native code change.
-Installed with `/tmp/install-ember-hover-and-lever.py` after confirming Destiny closed.
-All seven installed script hashes verified against `/tmp/ember-hover-script-hashes.json`.
-Backup: `build/hover-lever-darkness-backup-20260905-185535`. Runtime, settings and save
-unchanged. No game launch or process stop. Manual validation of these changes is pending.
-
-- Use the separate authored type-23 lever device (f6ffb59e slot7) with the existing accepted
-  type-4 lever interaction. Initialize position0 with snap; move to position1 without snap
-  on use. The object's interaction and the visual animation are distinct native components.
-- Add a four-second departure hold after native unload completion and both cargo squads
-  are observed. Empty C/D also hold. Preserve the three-second unload delay and immediate
-  retirement at exit completion. Timers have durable scheduled/elapsed bits; duplicate
-  receipts and module reload cannot restart the pause or repeat departure.
-- Exclude helipad squads from bridge darkness completion. Their authored anchors are around
-  (-305,-294,-24), ~450m from the bridge fight at y140..190. Bridge/passengers/Landing Sun
-  remain required; a living passenger cannot be bypassed. Tests demonstrate last passenger
-  death clears restrictions inside region64 while helipad is still active and populated.
-
-User confirmed the previous build's lever interaction and improved despawn timing live.
-Archived test log `build/first-encounter-audit/lever-hold-darkness-test-20260905.log` shows
-bridge and Landing Sun cleared, but helipad retained 1+2+2 living actors. Passenger member
-squad f6ffb59e/type1/slot26 also last reported alive1/consumed0 (t263045); its lingering
-population is not explained by the helipad scope error. If the next test looks empty but
-remains restricted, capture that actor's actual position/state before claiming all native
-squads are clear. Do not silently remove it from completion requirements.
-
-Harvester door/warp animations remain unresolved from the prior task. This change adds
-hover time and the Mercury lever's authored animation; it does not invent ship animations.
-
-## Lever progression and packet batching — installed 2026-09-05 18:30
-
-DLL SHA256 `13513b002ab38633560f16e59e6dcd2984165540830816c9bba5640cf8d4161b`.
-Backup `build/lever-and-packet-batch-backup-20260905-183010`. Installer checked the real
-process namespace, confirmed Destiny closed and verified all eight installed file hashes.
-Save retained; SDK, declarations and mission scripting enabled. User launches manually.
-
-- Split the six required Mercury squads from five optional bonus squads (Crimson Shadow
-  anchor and four bonus war-beast groups). Optional squads have their own durable encounter
-  state. Route catch-up excludes them. Main combat alone enables Ghost; optional deaths
-  cannot replay the controls directive or block the scan.
-- The authored `LANDING_MERCURY_DOOR_BUTTON_OBJECT` now spawns at its package transform.
-  New typed `set_interactable_object` Auth carries entry zero plus registered interaction
-  child 80804FB8. Native F32220 requires that child before publishing reply 80804FB7;
-  F33A90 -> F36640 sets runtime+720 after accepted use. Auth revision zero leaves control
-  of that latch with native use, rather than forcing a false latch on every publication.
-  Exact-object `on_event_object_interacted` starts optional squads and opens the door once.
-  Removed the proximity-triggered door transition and bonus spawn.
-- Sun/helipad requests run immediately in the completed Ghost callback, ahead of the ship
-  start requests. Later bridge crossings do not respawn those waves.
-- Transport now drains up to 16 complete received frames per service pass. Previously only
-  one was consumed. Each response is flushed before another frame is consumed; partial input
-  and blocked output stop the batch. This addresses a possible packet backlog, but a live
-  timing improvement is NOT yet demonstrated. Lua still retires ships on second-path
-  completion; no extra retirement timer exists. The accepted 3000ms unload delay is retained.
-
-Evidence: archived latest log `build/first-encounter-audit/lever-delay-test-20260905.log`.
-Actor callbacks had queued_ms=0, so prior derived-event batching alone did not explain the
-remaining delays. Exit-start/retirement acknowledgements still lagged staged commands by
-roughly 12–34 seconds. These logs do not isolate client application versus reporting latency.
-
-Validation: release linked; all 12 portable CTest checks and three Lua suites passed; diff
-check clean. New fixtures cover registered object Sense, duplicate use, stale generation,
-partial packets, blocked response ordering, main combat completion without bonus combat,
-wrong lever identity, proximity rejection, and no replay after reload/return.
-
-Remaining: manual validation of the native lever and latency change. Harvester doors and
-animated warp-out are NOT fixed in this build. Current retirement is native deletion. Actor
-command kind 6 only edits a six-element behavior-tag set; kind 8 edits a transform; neither
-has been established as a door/warp action. Harvester behavior imports map to 80B3B399 and
-its component templates, extracted under the audit tags directory. No speculative parameter
-hash or damage-based retirement mode has been installed. Capture native delivery/door
-components during the user's next manually launched bridge encounter.
-
-## Event dispatch and encounter accounting — installed 2026-09-05 17:46
-
-DLL SHA256 `ee769dd010e9ab6a65b4f144e3f283ae88c823496520eba41d05919e7659b56c`.
-Backup `build/encounter-accounting-backup-20260905-174651`; installer checked Destiny closed
-in the real process namespace and verified eight installed hashes. Save retained; SDK,
-declarations and mission scripting enabled. No game launch or stop.
-
-Reviewed recording `/home/millie/2026-09-05 17-04-12.mp4`, including bridge and refinery
-transition. Respawning Restricted banner appears after the console. Archived corresponding
-log in `build/first-encounter-audit/bridge-darkness-test-20260905-1704.log`. All four
-retire-generation receipts eventually arrive. Delay from staged retirement to logged receipt:
-C/D about 33–34 seconds, A/B about 21–22 seconds. These timestamps alone do not distinguish
-client application latency from delayed host observation. Do not describe them as proven
-network latency. Native deletion still has no verified warp animation.
-
-Changes:
-- Derived script event dispatch now drains up to 64 ordered callbacks per service pass,
-  stopping on any pending output or fault. Previously one callback per pass could accumulate
-  a backlog under continuous squad-cost reports. Native input ordering and output commits
-  remain serialized. Actor callback diagnostics now include queue age and remaining count;
-  actor Sense diagnostics include the original receipt tick.
-- Pending squad Auth is installed after the retained estate, even if the squad lease already
-  installed it. Previously an older SDK objective body in the estate could overwrite the new
-  pending body in that same packet, silently deferring its effective publication.
-- Encounter zero-population completion also requires consumed counts for every authored
-  member slot. Native squad Sense .11 (+612 count, +616 values) accounts for consumed
-  requests: 4E8620 -> 4E8660 increments for unowned actor removal/death; 4E8200 increments
-  failed delivery requests. These counts are not births. A temporary zero while requests
-  remain must keep the wave outstanding instead of requesting zero counts and phase 2.
-  Accounting-only Sense deltas now reach the script. Decoder selects exact nested schemas
-  80807ECF/80809491, refusing partial or invalid lists rather than matching arbitrary widths.
-- Existing darkness trigger/release policy, three-second unload delay, manifests and paths
-  retained. No speculative door or warp opcode installed.
-
-Latest test helipad slots 12/13/14 reached positive population at 171762–172019, zero at
-174141–174701, and some repopulated afterward. Sun slots 56/57 similarly reached zero,
-then slot 56 repopulated. Old Lua immediately retired each group at the first combined zero.
-The guard fixes that premature cancellation when requests remain; the cause of initial
-actor losses is still unproven. Logs now include consumed totals for diagnosis.
-
-Validation: release linked; ten portable CTest checks and three Lua suites passed; diff check
-clean. New regression checks cover a 1000-event burst, strict output barriers and ordering,
-partial accounting rejection, temporary disappearance/repopulation and late accounting deltas.
-Peak durable variables remains 54. Manual test of this exact installed build is outstanding.
-
-Remaining: verify ship callbacks no longer lag, observe actual hangar deaths versus accounting,
-confirm darkness clears only after combat; capture live delivery/door components during the
-next user-launched run. Door animation and native animated warp-out remain unresolved.
-Additional offline RE maps command kinds 0–9 through A98E40; kind 6 (A9C060) manipulates
-actor behavior tags, kind 7 (A9BFA0) writes named scalar parameters via 576420. Neither is
-verified to be a Harvester door/warp command. Do not guess parameter hashes.
-
-## Bridge darkness policy and native ship cleanup — 2026-09-05
-
-Installed at17:02; DLL SHA256 `cbdc7a6eec5edcc41f382dc76635931c34db99966bdc5073137fde2a74bd35bc`.
-Installer confirmed game closed, backed up to `build/bridge-darkness-cleanup-backup-20260905-170239`,
-and verified all eight installed runtime/script hashes. Save preserved, SDK/declarations and
-mission scripting enabled. User launches manually; no game launch or process stop.
-Latest user instruction: enable darkness only on completed Ghost interaction starting bridge
-extension; release after bridge/passenger/Sun/helipad squads all clear. Earlier catwalk/pipe/
-Mercury combat is excluded. Release outside landing; return to cleared area cannot reactivate.
-Controller tests cover initial disable, stale/incomplete scans, duplicate scan, last remaining
-hangar squad, completed return, reload and unfinished earlier fights not blocking release.
-
-Native `set_darkness_zone` uses exact type35/class808099BD/schema808099BF. E4C110 reads
-Auth .0; E4C0A0 additionally compares lifetime native+12 against current bubble. Roster
-assembly derives policy from its final SDK Auth estate, pairs lifetime .5 with region/8 (64→8),
-and clears participation+736 delay bypass while active. Wipe countdown remains inactive:
-hardwipe .3=-1, nested .4 timer inactive. This is NOT verified full checkpoint/wipe recovery;
-manual death/restriction/HUD testing is still required. No claim that this fixes Bird.
-
-Live observation proved all four entry/exit revisions acknowledged. Each A/B had two native
-cargo refs; C/D had none. All four outgoing disabled bodies reached native Auth, but actor
-indices stayed live minutes later. Root cause: live Auth callback AB6E20 invokes AB71E0,
-which retires only on generation CHANGE; AB7350's enabled=false only inhibits creation.
-Previous AB78B0-based assumption described a different callback and was insufficient.
-Retirement now advances generation1→2 at second path completion and sends empty delivery
-manifest in the SAME update to avoid old cargo replay after the lifecycle reset. Native
-mode0 uses AB89A0 detach and 56A8F0 destruction. Visual warp animation is not established;
-this fixes the demonstrated missing cleanup, pending manual verification. Do not claim doors
-fixed: native delivery completes without the user observing door opening. Mode1 was inspected
-and routes through damage/death handling (3BD720/B43090/DDA5C0); do not assume it means warp.
-
-Sun and helipad groups now start with bridge activation (five squads), once each; seven deck
-defenders retain their crossing trigger. A/B unload delay remains3000ms. No ship timing change.
-Native header/codec tests nine CTest passed; all three Lua suites passed. Release build passed.
-Peak54 durable variables; native limit512. Generated Lua declarations now include deliver_squads.
-Capture archive: build/first-encounter-audit/harvester-two-carriers-20260905-1623.log;
-live snapshots ship-runtime-20260905-162329 through163042. Reviewed user's local recording
-2026-09-05 16-23-18.mp4; ships visibly park after flying. No causal proof for random deaths yet.
-
-
-## Three-second delay and two-carrier manifest installed — 2026-09-05 16:18
-
-Installed DLL SHA256 `d240bb7f528a3f97885e861e0ba2cdf380efad85d4fd78398a18e59a6252a3ea`.
-Backup `build/two-carriers-transport-backup-20260905-161841`; eight runtime/script hashes verified.
-Installer confirmed Destiny closed; save preserved; SDK/declarations and mission scripting on.
-User launches manually. No game launch or process stop.
-
-- Delay now3000ms per ship after its first authored path completion (user requested +1s).
-- A carries supportA+meleeA; B carries supportB+meleeB. C/D have no bridge passengers.
-  This follows the user's two-on-bridge observation and the authored entry/anchor positions;
-  exact original script pairing still reconstructed. All four retain bridge objective AI.
-- Added bounded `deliver_squads` native manifest1..8 exact distinct same-registry squads.
-  Two refs are one187-bit payload, preventing second-squad assignment replacing the first.
-  Existing single-squad API remains supported. A/B wait for native delivery completion and
-  positive population from both of their squads. C/D depart after their arrival timer without
-  waiting for a nonexistent delivery. All retire once on second path completion.
-- Native movement/doors/warp-out are NOT claimed fixed. Latest objective test still had
-  one stationary ship and no acknowledged exit revision before Bird. Log archived as
-  first-encounter-audit/harvester-objective-test-20260905-1548.log. Bird at208531 had free0,
-  allocs430 and an allocator-exhausted line; unlike the prior free116 run, this IS index
-  exhaustion. Keep both failure modes distinct.
-- Exact transported ship bodies now log `ev=ember_transport stage=auth_staged slot=... bits=... body=...`
-  for only the four Ember members. Next run compare outgoing exit revision2 with live Auth
-  +256 and Sense +396. Use /tmp/ember-capture-door-details.py while ships remain alive; the
-  linked-block base calculation now wraps subtraction to64bits (previous Python overflow).
-  Prior parked snapshots152740..153138 showed authority TRUE for all four ships; do not
-  assume missing entity authority without new evidence. Door boolean mapping previously
-  traced via44E340 actually uses generic component enable/disable through590B10/594D70;
-  it is not established as an open-door animation command.
-
-Validation: Release built, eight CTest plus three Lua suites passed. Tests cover two refs
-surviving exit-program replacement, duplicate/empty manifest rejection, 2999/3000ms boundary,
-independent empty-ship exits, both passenger squads per carrier, transit/reload and no replay.
-
-## Harvester objective membership and +2s unload installed — 2026-09-05 15:42
-
-Release built successfully. Candidate DLL SHA256
-`151a83a68a0d01b7ac66ac402cf73f782473f825b2ce4d109507395e3d986f27`.
-Installed and SHA-256 verified all eight runtime/script files after the installer confirmed
-Destiny was closed in the real process namespace. Backup:
-`build/objective-delay-transport-backup-20260905-154242`. Save/account progress preserved;
-SDK, Lua declarations and mission scripting enabled. User launches manually. No game launch.
-
-User explicitly requested restoring the previous arrival-based unload, delayed roughly2s,
-and assigning ships to objectives exactly as carried enemies are assigned. Implemented:
-- All four empty ship parent squads assign EMBER_POWERHOUSE_BRIDGE_OBJECTIVE revision1,
-  task group-1, then use the shared native task-cost selector across14 authored task groups.
-  Parent counts remain0 so adding AI cannot create duplicate stationary ships.
-- First entry revision1/cursor1 starts a durable2000ms timer per ship. Expiry requests native
-  delivery once. No bridge Sense prerequisite. Duplicate arrivals cannot restart the timer;
-  callbacks continue across region transit and script reload.
-- Second authored path completion still retires each ship immediately and once.
-- Removed the new generic device-event implementation and six-device completion gate.
-
-Live test archived as first-encounter-audit/harvester-doors-test-20260905-1533.log. Snapshots
-ship-runtime-20260905-152740 through153141 contain four live ship Auth/actor states. No delivery
-was requested in this test: the discarded device helper compared schema table index with hash,
-so it ignored native completion. All six device primary channels were0/sequence2 in capture
-152920. B/D/C entry finished t226246/227389/228420; A did not report completion until298318.
-
-User reports Bird when killing one of the bridge-start defenders, after waiting minutes for
-ships that never unloaded. At t463844 native create failed with116 free entity indices and571
-allocations. This favors initializer failure over exhausted index bitmap; do not assert it was
-caused by failed ship removal. Door animation/objective hypothesis still needs a manual test.
-
-Portable eight CTest and three Lua suites pass; controller validates actual timer_name field,
-1999ms/2000ms boundary, independent late ship, reload/transit, objective cost selection, and
-unchanged empty parent spawn requests. Peak53 variables, native capacity512.
-
-## Bridge-ready gate and region-independent cleanup installed — 2026-09-05 15:21
-
-User reports entry/drop working but a few seconds before bridge connection, doors remain shut,
-ships fail to disappear, and Bird recurs. User clarified retirement must happen immediately
-on completion of each ship's SECOND authored path. No timers or region-exit cleanup were added.
-
-Installed DLL SHA-256 `d64edd4463d8d0a1ecad6d53ed16d5438bd423304c6af4a155e1ad2bedb9630f`.
-Backup `build/bridge-ready-transport-backup-20260905-152123`; eight hashes verified.
-User confirmed game closed, installer checked real /proc, save retained, SDK/declarations and
-mission scripting enabled. No game launch or stop. User tests manually.
-
-- New device_state event reports primary native Sense position and accepted sequence for type23.
-  Sparse field updates are retained; invalid/nonfinite positions are rejected.
-- Bridge initialization uses primary sequence1/retracted1, extension uses sequence2/extended0.
-  Cargo waits for all six bridge devices to report sequence2 at position <=0.001 AND its own
-  entry-path completion. Each event order is handled; duplicate receipts cannot redeliver.
-- Actor-path and ongoing squad receipts continue after leaving region64. Previously the main
-  controller discarded them outside that region. Exit revision2/cursor1 disables that exact
-  native ship once, without waiting for player transit or another ship. Native AB8C00 routes
-  disable through entity destruction (56A8F0) when the ship has authority.
-- Bird evidence is now native network-entity creation, not proof of a ship retirement failure.
-  `transport-doors-latest.log` captures 16EE180->170F190 via B428D0/3BD440/4D7110. Creation can
-  fail in allocator1711D10 OR initializer170B0F0. Existing allocator exhaustion is now warn;
-  each bounded retail failure stack also logs current free count and allocation count. No
-  extra allocator hook, automatic retries, or pool-restock settings were introduced.
-
-Validation: Release build passed; nine CTest cases and three Lua suites passed. New tests cover
-sparse device receipts, stale generation, partially connected bridge, early and late arrivals,
-refinery transit during unload/departure, exact second-path retirement and replay suppression.
-Peak54 durable variables; native capacity512. Live visual behavior remains to be tested.
-
-**Door animation remains unresolved and unchanged in this install.** Native delivery1029330
-sets an authored boolean through44E340 and starts four effect bindings (+80/160/240/320 via
-C7AFA0 -> 3D30F0/587970); these are not established door commands. The Harvester actor80FE22FC
-binds delivery component template80C0E5D2, component instance ordinal15, to actor template
-80FE22C9 ordinal5 through the boolean ref at actor-file0xB68. Destination definition offset1018
-uses interface815B963A (80803A2B/80803A2A), implemented by actor component80803C23 feature6.
-Extracted assets are under first-encounter-audit/tags. Need capture that actor boolean/animation
-state during delivery, and entry/exit Auth plus authority while ships are alive. Do not invent
-an open-door hash or replace the existing Auth body without preserving path and delivery.
-
-## Endpoint and sequencing candidate installed — 2026-09-05 14:43
-
-User confirmed that the prior build moved ships, but too late; troops fell to their deaths,
-some squads spawned early, ships remained above the bridge, and Bird disconnected the run.
-The prior candidate is not an accepted implementation.
-
-Installed DLL SHA-256 `166c596f720094c2fa9d69e70bf29bd3495a0c1a20176c4c3ec7d87e487ed8d5`.
-Backup `build/endpoint-transport-backup-20260905-144335`; all eight DLL/script hashes verified.
-Installer checked the real process namespace and found the game closed. Save unchanged,
-SDK/Lua declarations and mission scripting enabled. User launches manually; no game launch.
-
-Concrete correction: the kind-3 path command used marker 0, the START of each authored curve.
-Native AB0C30 indexes the curve's marker array; marker 1 is the far endpoint. Both entry and
-exit now use marker 1. Read-only `sequence-runtime-20260905-143010` contains all eight source
-and curve assets. `transport-curve-markers-20260905.json` summarizes them. A entry runs from
-(-380,151,-2.5) to (-365,117,-22.5), approximately 20m lower. C entry's marker parameters are
-[0,5], others [0,4]; these are spline parameters, NOT flight durations in seconds.
-
-Cargo reservations and empty parent requests now prepare before Ghost is enabled, without
-creating ships or ground passengers. Console completion extends the bridge and requests the
-four entry flights without first queueing later ground waves. Bridge monitors arm at this
-point; early bridge receipts cannot consume Lua progression. Ground bridge/Sun/helipad groups
-enter on 25/75/100% crossings with existing forward catch-up and once-only guards. Actual
-crossing behavior still needs a fresh manual test: earlier runs sometimes lacked receipts on
-the initial traverse, and late ground placement has previously missed streaming readiness.
-
-Video rechecked at half-second intervals from 3:10 through 3:34.5. Ghost deployed around3:15,
-machinery moves during3:16–22, ships sweep into view3:24–29, and crossing combat follows.
-Four reference contact sheets saved under first-encounter-audit/reference-arrival/drop-*.
-The recording does not establish the full four-ship passenger manifest. Current one-squad-per-
-ship pairing remains reconstructed; passenger A-family support/melee anchors cluster near A/B
-entry endpoints, so original manifest deserves further verification before claiming full fidelity.
-
-Bird remains unresolved. Archived `transport-bird-20260905-1419.log` contains first-session
-failed sobject creation at t248202, after region transition240599–245393. Exit cursor receipts
-completed230157–233507; those receipts alone cannot prove visible flight and can include native
-timeout fallback. New bounded warning-level caller/stack traces preserve the native creation
-failure boundary if Bird recurs, without globally enabling debug or suppressing the error.
-
-Validation: Release build succeeded; eight CTest cases and three Lua suites passed. Packet
-regressions reject the former start-marker payload. Controller covers deferred preparation,
-scan-gated monitors/waves, arrival-only unload, independent exits, and no replay on backtracking
-or script reload. Peak52 variables; runtime capacity512. Flight, safe unloading, departure,
-full passenger composition and no Bird still require the user's fresh run.
-
-## Auth replacement fix installed — 2026-09-05 13:23
-
-User reports missing pre-bridge enemies; ships stationary; troops appeared in ships but fell
-to their deaths; some enemies appeared on the bridge; far-side closets worked as expected.
-No mission fault/capacity error in the archived `native-transport-live-test.log`.
-The log contains zero actor_path events. Do not describe the prior candidate as successful.
-
-Read-only captures establish the lost-state problem:
-- `ship-runtime-20260905-130600` and the 130638..130657 series: all four named actors
-  exist, enabled, initialized, generation1; Auth movement revision/seed/count are all zero,
-  while delivery revision1 and the correct type1 squad reference are present.
-- `encounter-runtime-20260905-130934` captures both F6FFB59E and D8CE8390, 69 rows,
-  no read errors. Active ground squads such as F6FFB59E/33,34,36,56,57 have objectives
-  and task groups but zero Auth requested-count array length and generation0.
-- The earlier 130812 encounter capture has squad Auth read errors (wrong handle offset).
-  Its component bytes exist, but use 130934 for Auth evidence. The native Auth storage
-  handle is at component+368 for both types; squad's inline Auth copy starts at384.
-- `actor-authority-20260905-130526` and 1307 captures preserve actor state; many actors
-  no longer carry squad refs. Free actor slots retain stale bytes; count live first words only.
-
-Native overrides replace complete Auth, not absent-root patches. Host now materializes
-these bounded mission API patches against the last transported same-ClientRef estate.
-Movement and delivery roots coexist; objective updates retain counts/profile/generation;
-retirement explicitly replaces requested counts with zero or actor enabled with false.
-Unsupported nested layouts are refused. Unrelated SDK APIs retain their existing behavior.
-Squad lease storage now allows the full 1313-bit schema, including objective and counts.
-
-The delivery routine begins unloading. Lua now reserves troops during flight and calls
-`deliver_squad` only after entry progress completes, then waits for native delivery completion
-and positive squad evidence before requesting exit. Packed transport state now uses FIVE bits
-per ship; this candidate requires a fresh mission, not reload into the four-bit live state.
-
-Release candidate built: `dce57b2cb894e3a08cc320f2657d6ed07bab08f41ed4c38db78fd4ea1614fcbd`.
-Installed and SHA-256 verified eight runtime/script files after the user closed the game.
-Backup: `build/auth-state-transport-backup-20260905-132343`. Save preserved, SDK and
-mission scripting enabled, no launch. Installer: `/tmp/install-ember-auth-state.py`.
-Validation: all eight portable tests and three Lua suites pass. New test covers sequential
-movement/delivery/exit/retirement, repeated AI updates preserving spawn counts and mode,
-zero-count retirement, malformed/short input, and maximum-category squad lease capacity.
-Live visual movement and correct unload location remain unverified for this new candidate.
-
-User's next correction, explicitly deferred until dropships are implemented: war-beast door
-must use `LANDING_MERCURY_DOOR_BUTTON_OBJECT` (F6FFB59E/type4/19), not proximity.
-Object activation/rendering, native interaction/destruction criteria, and object/device links
-need auditing. Nearby devices are lever7, screen8, door9. No object behavior changed yet.
-Current object Sense decoder refuses nonempty dynamic reply lists; this is part of that audit.
-
-## Installed native transport and completed-area safeguards — 2026-09-05 13:01
-
-Installed eight runtime/script files into the user's game, verified SHA-256 after copying.
-DLL: `d688af8ed3b6b738689494d34b2e5e7b15c9be97a9f6b4839a126d8474394e43`.
-Backup: `build/one-shot-transport-backup-20260905-130149`.
-Save untouched; SDK and mission scripting enabled; image capture disabled. No game launch.
-This supersedes the short-lived 12:52 candidate, which still had the host's old mode check.
-
-**Root cause corrected:** type-2 Auth `.7` is passenger delivery, not a list of flight paths.
-AB6600 dispatches its type-1 squad references to interface 80807DB4. On failure it calls
-4E8270 on those squads, so sending type-58 references there was structurally wrong.
-Movement now uses Auth `.6`, one kind-3 80807F7A action; A97BB0/4FFEC0 accepts type58
-and resolves the authored curve. Movement completion uses nested Sense `.3`/80807F6E
-progress and revision. Root Sense 5/6 now remain explicitly separate delivery fields.
-
-Harvester actor asset 80FE22FC contains component 8080670A at 0x554C. Its registered
-1029330 delivery implementation collects reserved requests (102A920), creates passengers
-in vehicle seats (1022890), and completes after attachment handles clear (102DC80).
-Passenger squads now use mode3, which 4E4580 excludes from ordinary ground placement.
-Objective assignments and cost-driven changes preserve that mode. Codec, SDK preflight,
-and host submission share the same accepted-mode predicate.
-
-Bridge completion requests bridge, landing-sun and helipad defenders before crossing volumes
-can trigger next-region streaming. It also reserves all four passenger squads and starts the
-four named ships. Each ship departs independently after movement completion, native delivery
-completion, and evidence its passenger squad existed. Current manifest is one squad per ship
-in roster order; original passenger-to-ship pairing is **not established** by the shared names.
-There is no timed ground-spawn fallback. Native completion can include cancellation/timeout;
-these receipts still require visual confirmation and actor capture in the next user-run test.
-
-Completed encounters now replace their native requested counts with zero exactly once.
-Their durable phase prevents subsequent placements and AI reassignment. Advancing route
-progress suppresses old, previously skipped triggers on backtracking; objectives/dialogue
-cannot rewind. Successful ship exit (or a dead ship) sends a disabled member body: AB78B0
-releases the actor and AB7350 prevents recreation when retained Auth is replayed.
-These rules persist through region transit and a script reload in the same mission session.
-A new Director mission starts a new session; save/checkpoint resume of all stages is not implemented.
-
-Validation: Release build completed; seven portable CTest cases and all three Lua suites passed.
-Production msg-6 fixtures distinguish delivery changes from actual action progress and reject
-truncated input. Reservation and retirement encodings are checked independently. Controller
-regressions cover independent departures, duplicates, clearing all 38 ground/passenger squads,
-zero-count retirement, ship retirement, refinery transit/reload/return, and late early triggers.
-Peak durable variables: 51 under the former 64 limit; installed runtime still supports 512.
-
-**Next manual acceptance:** fresh mission, initial landing/door/console/bridge preserved; see
-ships actually follow entry and exit curves, carry and unload every passenger, then see all
-far-side groups before refinery. Kill passengers and backtrack after clear. Capture native
-ship movement/delivery/Auth and far-side actors while powerhouse registry is still loaded.
-Visible flight, full compositions/placement, and return behavior remain unverified in-game.
-
-## Live 512-variable test — 2026-09-05
-
-User manually launched and crossed into the refinery. No script/capacity fault in the
-archived `build/first-encounter-audit/capacity-512-live-test.log`; structured squad/flight
-summary is alongside it. User reports no visible post-bridge enemies. Do not treat server
-spawn staging or positive Sense populations as proof of correct visible placement.
-
-Cutscene completed at t=112840. Catwalk actor capture `actor-authority-20260905-120413`
-confirms native objective D8CE8390/type3/index0, revision 1, assigned task groups, computed
-costs, and AI area refs. Mercury groups cleared and Ghost completion started bridge/ships.
-All four entry paths stop in 12–14 ms; exits stop in 13 ms. Read-only captures saved before
-and after ship creation: `ship-runtime-20260905-120639`, `ship-runtime-20260905-120724`; full actor snapshot
-at 120724 contains all four ships. Both 120820 and 120857 are empty after registry unload;
-they do not contain member, Auth or entity evidence.
-
-Bridge 25/75/100 triggers fired at t=369200/386691/390960 without fault; corresponding
-spawn and objective commands staged. First positive populations for bridge slots 38/39
-arrived at 399270/399470; 33/34/35 at 400133/399983/400746 (about 30 seconds late).
-Slots 36/37 reported zero only. Need explain delayed population and actual placement,
-and missing sun/helipad groups; capacity is no longer blocking callbacks. All four
-passenger groups reached zero after previously positive counts without the old disconnect
-before the user continued into refinery.
-
-Native ship capture resolves all eight type58 path refs to class 80807D9B objects, excluding
-missing registry mapping at capture time. All four ship members show generation/actor state
-consistent with spawned ships, delivery revision 2, inactive, dead=false, initialized=true.
-The later helper supports Auth/entity capture, but 120820 contains no rows. This does not
-prove readiness at first dispatch. Those old revision/state fields described delivery, not movement.
-No game files changed during this monitoring turn; no launch/shutdown or memory writes.
-
-## Variable capacity update — 2026-09-05 12:00
-
-User reports almost all enemy behavior now works, but stationary ships, missing passenger
-and post-bridge enemies, then `mission variable capacity exceeded`. Run archived as
-`build/first-encounter-audit/ai-working-flight-cancel-capacity.log`. The bridge 25% trigger
-faulted while creating encounter state, preventing later mission callbacks.
-
-Raised the shared C++ mission variable capacity from 64 to 512 at the user's request.
-All variable-count fields are size_t; VM, transactional state, and SDK capacity publication
-use the shared constant. Also packed per-squad AI assignments into 12 five-bit lanes per
-integer. Full-route regression reproduces exhaustion with the previously installed scripts;
-updated scripts peak at 57 variables under the original 64-slot test budget. A fresh mission
-is required; an already faulted run is not migrated.
-
-Release build, all seven portable tests, three Lua suites, and whitespace check passed.
-Installed and SHA-256 verified eight DLL/script files, preserving save and enabling SDK and
-mission scripting. Backup: `build/capacity-512-backup-20260905-120000`.
-DLL SHA-256: `004fcd781af52bc8eaa3fc2e56be9ab447d0ef23335902fb0f576c2ac844eaf2`.
-No game launch or shutdown.
-
-Flights remain unresolved: all four entry paths report active then inactive within 13–15 ms;
-exit paths also stop within 14–16 ms. Current transport interprets this transition as arrival,
-but it is not evidence of a completed flight. All four passenger group slots reported positive
-population, which does not establish complete composition or correct boarding/unloading.
-Need a read-only capture of stationary ships during the next manual run to inspect path
-resolution, actor readiness and component dispatch. No speculative flight change installed.
-
-## Objective and flight test candidate — 2026-09-05
-
-User confirmed bridge extension, then reported stationary ships spawning only on the return
-bridge crossing and a world disconnect after killing passengers. The installed baseline had
-no flight requests and used Bridge 25% as its spawn trigger. Candidate moves ship creation to
-Ghost completion and starts all four named members on their authored entry sequences. Native
-path-state receipts gate passenger creation, then exit sequences. Passenger attachment/unload
-is still incomplete: this candidate creates passengers at their authored ground anchors after
-arrival. Native flight/cancellation behavior requires the user's test.
-
-Actor capture `build/first-encounter-audit/actor-authority-20260905-112423` contains 10 full
-actors and five catwalk squad blocks. Actor and squad-owner authority bits are true. Squad
-Auth root 0 is unset, so they are not assigned to an authored combat objective.
-
-Derived objective assignment: 4E2A90 copies root 0 to squad +1520, root 13 to +1540,
-and root 16 to +1532. 4EC710 links the squad into the selected objective group. AB8510 /
-A99FA0 computes group costs for squads matching the objective's registry/ref; 4EB8E0 clamps
-costs at 2040, and 4E84E0 echoes the completed calculation revision into Sense root 1.
-AB83D0 / A9AD70 then selects native tasks within assigned groups; 4E2C40 applies the
-combat-area reference to the squad AI group and refreshes its actors.
-
-Candidate assigns all 38 ground/passenger squads to their corresponding same-registry
-objective, initially group -1, revision 1. Retained native costs select the lowest reachable
-authored group, preserving ties. This selection policy is reconstructed, not recovered retail
-host code. Bounds from objective source +136: catwalk 17 (80B3CA03), Mercury 11 (80B3DCC1),
-bridge 14 (80B3C8D6), pipe 3 (80B3DCC7), sun 4 (80B3DCC4), helipad 4 (80B3DDB2).
-Cost-only Sense deltas retain population counts so AI updates cannot manufacture deaths.
-
-Disconnect log archived as `build/first-encounter-audit/dropship-kill-disconnect.log`:
-t=339052 four failed sobject creations, then prerequisite failure and world cleanup. No
-verified fix yet. Existing lease-release diagnostic setting is unchanged: its applicability
-to this run has not been established.
-
-Validation: Release DLL build, all seven portable tests, three Lua suites, whitespace check.
-DLL SHA-256: `b14c56923797150227f9b1e9b1feaa4b8198dde40c669a5ab65406624407bda6`.
-Installed and SHA-256 verified at 11:44. Backup: `build/objective-flight-backup-20260905-114409`.
-Save retained; SDK and mission scripting enabled. No game launch.
-
-## Ghost completion decoder fix — 2026-09-05 10:45
-
-User confirmed the beast door opens and the Ghost console is scannable, but the bridge
-stays retracted. Archived that run as `build/first-encounter-audit/console-scannable-no-bridge.log`.
-All eleven Mercury squads cleared and console enable Auth was staged at t=278017.
-There were no decoded Ghost Sense receipts. The native msg-6 decoder lacked the
-80804D3E branch, so the new Lua completion handler could never receive its reports.
-
-Added the exact required boolean, raw float and biased signed generation fields from
-SDK schema 80804D3E and native E4A590. Independent complete packet fixtures now run
-through the production decoder and Ghost state reader: initial, active half-progress,
-and completed generation 2; a truncated packet is rejected. The test failed with the
-old decoder's partial status and passes with the fix. Both Lua suites and all six
-portable tests pass; Release DLL build and whitespace checks pass.
-
-Installed and hash-verified DLL/scripts with backup
-`build/ghost-decoder-backup-20260905-104521`. DLL SHA-256:
-`26611bdea79a39305d2fcda7a64ec06627c5a07ec9af8ee64e009d393cb42dc3`.
-No save edits or game launch. Fresh manual run still needed to confirm that completing
-the native scan triggers all six bridge animations. AI and transport work remains open.
-
-## Installed bridge, door and console test — 2026-09-05 10:34
-
-User confirmed the opening cinematic and player arrival work. Preserve that path.
-Installed Release DLL and five Lua files, with source/installed SHA-256 verification.
-Backup: `build/bridge-console-backup-20260905-103443`.
-DLL: `eed579d58c7131090bb87d6b7521b9ef8063f4c5a95f70ff68dc3a0a88bf83c9`.
-Save files were not modified. SDK and mission scripting remain enabled. Native image
-capture is now disabled because the complete image has been archived. No game launch.
-
-This candidate initializes all six bridge devices to position 1 (retracted inferred from
-native value passthrough and the user's observation that position 0 was extended), opens
-the beast door at the authored Mercury 40% trigger, and enables the native Ghost console
-only after all eleven Mercury squads report real populations and then clear. The squad
-state flag previously called removal_flag is not a reliable unload indicator: observed
-real kills set it, and it no longer suppresses death accounting. Exact type-65 Auth and
-retained partial Sense fields connect console completion to animated bridge extension.
-
-Validation: Release build current, git diff whitespace check clean, both Lua suites pass,
-and all six portable CTest tests pass. These are implementation checks; bridge direction,
-door timing and native console completion still require a fresh manual mission test.
-AI movement/idle and carrier/passenger attachment remain unresolved and are not fixed
-by this candidate. In the prior run, Mercury bonus-anchor squad slot 42 remained alive.
-
-Transport investigation checkpoint: type-2 Auth root 7 carries an authored sequence list
-and revision; native AB6600 resolves it and starts actor sequences, with Sense revision
-and active fields at offsets 448/452. This is distinct from loose squad creation or
-adopting an existing actor. No transport change has been installed. Squad Auth root 0
-is not proven to reference a dropship; its resolved target expects a controller layout,
-so do not implement a carrier link through that field on this evidence.
-
-## Phase-zero sandbox fault — 2026-09-05
-
-The manual run failed before cinematic activation. Installed log at t=52632 reports
-`landing.lua:52: attempt to call a nil value (global 'pairs')`. Sunrise deliberately removes
-unordered iteration from its mission sandbox; the standalone Lua test had left it available.
-Both controller uses now iterate an explicit route-ordered array with `ipairs`, including the
-squad-state callback. The regression disables `pairs` and `next` before loading the controller:
-it reproduced the exact old fault and passes the full route/reload test with this fix.
-No native DLL change or save reset is required. Start a fresh mission instance after installation.
-
-The user's manual run also produced a complete native image dump (zero unreadable bytes).
-It and the fault log are preserved under `build/first-encounter-audit` for offline investigation.
-
-## Powerhouse route test build — 2026-09-05
-
-Correction to the earlier spawn investigation: `powerhouse_landing_mercury` is NOT the mission
-start. It is the later console combat platform. The authored `default` set (`0x2EA8FB98`) has
-an opening three-point cluster around (-400, -192, -1.9), surrounded by the
-`powerhouse_catwalk_002_dialog_trigger_volume`; the catwalk progress volumes then lead north.
-That is the new selected set. Actual native spawn selection still needs the user's fresh run.
-
-The Lua route now binds all 42 powerhouse squads once in nine groups. The first five are the
-catwalk entry squads; 15%/35%/100% catwalk crossings stage mid-catwalk, pipe and Mercury.
-Pipe 40% starts cue 2. Bridge 25% stages bridge/dropship/passenger groups; bridge 75% stages
-landing-sun, and bridge 100% stages helipad. Later crossings catch up skipped narrow volumes.
-These choices are reconstructed policy using authored spatial identities and the reference
-video, not a claim that the packages contain recovered retail host conditions.
-
-Fixed a native Sense reader bug: a presence-gated alive-count field exists as a DecodedValue
-row even when omitted. Checking only the row pointer interpreted that omission as zero.
-The reader now requires a present root count. Added squad population receipts, exact resolved
-player-trigger identities, and bounded change-only bridge/console Sense diagnostics.
-
-Validation: two Lua suites pass (cinematic ordering, all 42 roster entries, route progression,
-exact-trigger matching, skipped volumes, removal, repeat/reload guards); all five portable
-CTest tests pass, including omitted/zero/nested/invalid alive-count cases; six SDK inspector
-tests pass. All 42 squad bindings and seven route triggers resolve against the generated SDK.
-These checks do not prove live actor creation, native AI, Ghost interaction or bridge movement.
-
-MissionDocs were reviewed. The Forest race switch is specific to Omega/Vex and is not suitable
-for Ember/Cabal. The documented member-bound actor path needs native verification before
-applying it to harvesters and other named Ember members; loose squad requests alone do not
-establish their correct creation/attachment. Ghost-console Auth semantics and bridge direction
-also remain unresolved. No automatic console completion or bridge opening was added.
-
-User constraint: apply and save to the installed game, but NEVER launch it automatically.
-The next manual boot will use Sunrise's existing native-image diagnostic to supply readable
-code for offline analysis; disable the diagnostic once captured. Existing save/settings,
-scripts and DLL are backed up before installation. SDK and mission scripting remain enabled.
-
-Installed and SHA-256 verified the Release DLL and all five Ember Lua files.
-Save/account state compared equal before and after this installation. Backup:
-`build/powerhouse-route-backup-20260905-090720`. DLL SHA-256:
-`26edd28c539beb6444c6181684154c1e43abb8940cf26c3d5eec87cfd54bcfac`.
-The installed diagnostic flag is enabled for the next MANUAL boot. No game launch was made
-after the user's manual-launch-only instruction. Next validation: fresh Director launch,
-cinematic completion, grounded starting platform, catwalk/pipe population and AI, then console.
-
-## Earlier landing spawn attempt — superseded
-
-The next playtest confirmed the movie completes and the player spawns, but in the wrong place
-under the map. Logs now resolve both cinematic notifications and arm the transition to state 64.
-The installed arrival override selected the cinematic region without an explicit spawn set.
-
-The native build-data catalogue contains a three-point landing set whose hash matches
-`powerhouse_landing_mercury` (`0x9C58857A`). Its points align with later Mercury anchors. The claim that this was the opening area
-was incorrect; see the correction above. Settings now explicitly select that set while retaining
-bubble 6/state 49 for the movie; Lua still selects playable state 64 after termination.
-
-The spawn-binding filter previously discarded explicit sets when their activity package was
-absent from the scenario's direct package list. That list does not inventory transitive
-references. Explicit overrides now retain their documented precedence; inferred spawn sets
-keep the existing package filter. Both roster and global-state publishers use this helper.
-
-Validation: all four portable CTest tests passed, including explicit override precedence,
-absent/zero overrides and the existing inferred-set filtering behavior. The Release DLL built
-successfully and the native settings probe accepted the updated configuration with SDK and
-mission scripting enabled. Installed and verified the root DLL and arrival setting, preserving
-other preferences. Backup: `build/landing-spawn-backup-20260905-081125`.
-The selected landing position still requires a fresh in-game test after the cinematic.
-
-## Cinematic handoff fix — 2026-09-05
-
-Playtest confirmed the arrival movie plays, but the player remained black after fade-out.
-Both native notifications arrived (targets 5239 and 1685); the mission runtime rejected each
-as `cinematic absent`. Inspection of the retained records showed zero-filled payloads and
-ClientRefs decoded as registry 0, type -1, slot -32768.
-
-Root cause: `incident::validate` advanced past selector, optional words and payload without
-retaining their contents. It now reads and stores those fields through the bit reader,
-including unaligned payloads. This restores the source identity needed to deliver the exact
-cinematic-termination event to Lua and select landing state 64. Player-trigger events used
-the same discarded payload path. Cinematic resolution now logs the source identity and
-successful start/termination for the next playtest.
-
-Validation: the new regression fails against the original parser and passes with the fix.
-It covers every payload alignment, maximum selector/payload lengths, truncated frames,
-optional fields, cinematic encode/parse/decode/catalogue resolution and player-trigger decode.
-All three portable CTest tests and the Lua handoff test pass; Release build passed.
-
-Installed and SHA-256 verified the rebuilt root DLL. Previous DLL is in
-`build/cinematic-handoff-backup-20260905-075750`. No SDK or settings regeneration was needed.
-The restored notification path still needs the next in-game spawn test.
-
-## 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 completed after the game closed. The previous runtime, SDK, scripts and settings
-are backed up in `build/video-audit-backup-20260905-074152`. All installed file hashes matched
-the prepared files. Settings now select bubble 6/state 49 for cinematic arrival. A follow-up
-Lua gate additionally rejects early playable-region reports before cinematic completion.
-
-Millie supplied a separate 21-second arrival cutscene reference and specified ordinary fly-in,
-then the cutscene, then player spawn. The reference document records that evidence. The selected
-bookend asset still needs visual confirmation during play.
-
-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
-
-The opening landing controller is implemented and loads with the regenerated SDK. It publishes
-the powerhouse state only when the client holds region 64, places the six landing squads and
-changes the directive after all six have been observed and cleared. It leaves bridge movement
-for the authored interaction. This is a development slice, not a complete implementation of 1AU.
-
-Build: `build/x64/Release/steam_api64.dll`.
-Regenerated SDK: `build/sdk-generated`.
-Prepared installation overlay: `build/playtest`.
-
-The overlay was installed into the game's `bin/x64` directory on 2026-09-04. It contains the
-rebuilt root DLL, generated SDK and opening scripts. The previous files are backed up in
-`build/installed-backup-20260904-224413`, with an `installation.json` listing replaced paths.
-The scripting switch `server.activation.mission_scripting` was already enabled; no settings
-were changed. DLL, SDK-pack and entry-controller copies were checked against the prepared files.
-
-## Fixed extraction problem
-
-The squad linker resolved object keys across the entire content estate. Ember and its arcade
-variant reuse object keys, so a spawn-rule slot in the arcade object could invalidate an exact
-campaign reference. The linker now restricts target descriptors to objects sharing an authored
-scenario with the source. Ambiguity inside a shared scenario remains a refusal.
-
-Package evidence: decoded all 162 Ember spawner configs using the existing package reader and
-installed Oodle codec in an isolated Wine prefix. Their 173 valid raw references all resolve to
-unique slots inside Ember; 116 collide in the global slot lookup. This census establishes the
-scope problem; it does not establish that every spawner passes every subsequent runtime gate.
-
-The production offline generator was run with the rebuilt code into an isolated output tree,
-reusing 468 matching scenario shards and publishing 1,643 Lua files. The verified output was then
-installed for the opening playtest, retaining the originals in the backup above.
-
-| Ember resource | Installed SDK | Regenerated SDK |
-| --- | ---: | ---: |
-| Squad sensors | 162 | 162 |
-| Squad definitions | 31 | 77 |
-| Definitions passing static placement checks | 29 | 64 |
-| Sensors without a squad definition | 131 | 87 |
-
-The six opening squads all have exact actor bindings, anchors and passing definition flags.
-The remaining missing definitions and refused rows require further work; the scope fix is not
-a claim that all mission combat is available. Static placement checks cover definition flags,
-member counts and the default actor-profile compatibility gate. Live lease, transport and AI
-behavior still require in-game validation.
-
-## Region map
-
-Ownership is taken from SDK state/occurrence joins. This table is not a complete event-order map.
-
-| Region | Authored name | Relevant content | Squad sensors | Passing static placement checks |
-| --- | --- | --- | ---: | ---: |
-| 64 | powerhouse | Landing, bridge, catwalk, helipad | 42 | 28 |
-| 56 | link | Processing encounter and machinery | 15 | 2 |
-| 40 | cinder | Foundry, chamber, grinder, ascent, sunlit deck | 49 | 31 |
-| 0 | apex | Interceptor/security, reactor targets, fusion-cell area and escape | 56 | 3 |
-| 1, 2 | apex alternatives | Alternate authored states | 0 | 0 |
-| 48, 49 | unnamed globals | Shared mission sensors and bookends | 0 | 0 |
-
-The generated-world shard's authored-squad context vectors are empty. Its projection helper
-`append_scenario_graph_contexts` has no caller in the current generation path. The opening
-controller uses the runtime pack's squad definitions, so wiring that separate graph projection
-was not included in the scope fix.
-
-## Verification
-
-- Full Release Windows DLL build passed using clang-cl and the existing Windows SDK.
-- CMake now includes the embedded Lua sources/headers and enables C++ exceptions, matching the
-  Visual Studio project's requirements.
-- Eight production-resolver regressions passed with address and undefined-behavior sanitizers.
-  Leak detection was disabled because the execution sandbox uses tracing.
-- Six SDK-inspector validation tests passed, including corrupted/truncated inputs and wrong
-  scenario identity.
-- Lua encounter and controller checks passed for initial empty reports, duplicate events,
-  removals, explicit reset, region transit, region ownership and controller reload.
-- The opening controller rejects the old SDK's missing squad binding and loads with the newly
-  generated SDK. No in-game playthrough has been performed.
-
-## Next checkpoint
-
-Launch campaign 1AU and check that the landing enemies appear,
-fight and allow the directive to reach the bridge controls. Check death and re-entry behavior
-without expecting complete authored checkpoint support yet. Keep the mission-script/runtime
-log from this run so spawn or lease refusals can be distinguished from missing AI behavior.
-
-Then verify the bridge's interaction event and extend the controller through the next encounter.
-The later mission, full checkpoint/wipe handling, dialogue, cinematics, rewards and terminal
-completion remain unimplemented. Do not mark the mission complete on the basis of this test build.
-
-## Live-test correction: VM arena allocation
-
-The first opening-area test reported `mission arena block could not be taken from the heap`
-before any events. An isolated Windows executable linked against the Release objects reproduced
-it: arena initialization returned false with capacity zero. LLVM IR showed that the nothrow
-byte-array new-expression had been reduced to the failure branch, with no allocation call.
-An explicit `::operator new[]` call preserves the allocation; the 64 MiB limit is unchanged.
-The rebuilt production allocator passed allocation, growth with data preservation, capacity
-refusal, free/coalescing and three release/reopen cycles under Wine. Regression source:
-`tests/mission_arena_test.cpp` (run with Release optimization; checks do not rely on assert).
-The corrected DLL is installed; backup: `build/dll-before-arena-fix-20260904-230158.dll`.
-A fresh in-game launch is still required to verify the next mission startup stage.

+ 0 - 2661
OpusHandoff.md

@@ -1,2661 +0,0 @@
-╭─── Claude Code v2.1.241 ─────────────────────────────────────────────────────╮
-│                                                    │ Tips for getting        │
-│                Welcome back Millie!                │ started                 │
-│                                                    │ Run /init to create a … │
-│                       ▐▛███▛█                      │ ─────────────────────── │
-│                      ▝▜██████▀                     │ What's new              │
-│                        ▝▝ ▝▝                       │ Bug fixes and reliabil… │
-│    Opus 5 · Claude Pro · ameliahesketh@pm.me's     │ Bug fixes and reliabil… │
-│    Organization                                    │ Cost estimates (`/cost… │
-│      ~/Documents/Sunrise-builds/mission-ember      │ /release-notes for more │
-╰──────────────────────────────────────────────────────────────────────────────╯
-
-
-❯ Hi Claude. Please read every markdown file in                                 
-  ~/Documents/Sunrise-docs/MissionEmberPlanDocs, then repeat the plan back to   
-  me, what you're going to change, etc, and I'll give you to go ahead if I'm    
-  happy with it.                                                                
-
-● I'll read all the markdown files in that directory.
-
-  Listed 1 directory (ctrl+o to expand)
-
-● Note: Mission_Ember_So_Far.md is 23 MB — I'll read the others in full and
-  sample that one strategically.
-
-  Read 2 files, ran 10 shell commands (ctrl+o to expand)
-
-● Now the transcript. It's a 290k-line Codex conversation log — I'll extract the
-  user turns and the tail, which carry the directives.
-
-  Read 1 file, ran 1 shell command (ctrl+o to expand)
-
-● Now let me verify the repo state matches what the plan describes.
-
-  Listed 1 directory, ran 2 shell commands (ctrl+o to expand)
-
-● I've read all seven files. Here's the plan back to you.
-
-  What I read
-
-  Six docs in full: MISSION_EMBER_PLAN.md, MISSION_EMBER_REFERENCE.md,
-  MISSION_EMBER_REACTOR.md, MISSION_EMBER_SQUADS.md, MISSION_EMBER_STATUS.md,
-  and MISSION_EMBER_FINAL_ISSUES_PLAN.md.
-
-  Mission_Ember_So_Far.md is 23 MB / 290k lines — the full Codex transcript. I
-  can't hold that in context, so I read its structure and extracted all 136 of 
-  your turns verbatim, which is where the directives and bug reports live. I
-  have not read every agent activity log inside it. If there's something
-  specific buried in the agent output you want me to pull, point me at it.
-
-  I also verified the repo state matches the handoff doc: HEAD is 50e68b7, tree
-  clean apart from the untracked MISSION_EMBER_FINAL_ISSUES_PLAN.md
-  (byte-identical to the docs copy), all 16 scripts/mission_ember/*.lua present,
-  the game is not running, and the cached evidence survived —
-  /tmp/ember-reference/1au.mp4, build/full-mission-audit/, build/reactor-audit/,
-  build/reactor-playtest/, and the /tmp/install-ember-*.py installers.
-
-  Where the project stands
-
-  1AU (mission_ember) is scripted end to end — opening cinematic, landing,
-  catwalk/pipe/Mercury, Harvester dropships with working doors and exit, bridge,
-  Processing/refinery, Sunside/cinder, Foundry, Light's End, reactor targets,
-  fusion cell, escape. That all works in game and is the regression baseline I
-  must not break. What's left is a finale that doesn't finish and a set of
-  visual/native behaviours the last patch failed to fix. Critically: the
-  previous agent's unit tests all pass and the last install is verified — and
-  none of that is gameplay confirmation. I'll treat passing tests as necessary,
-  not sufficient.
-
-  The eight acceptance criteria
-
-  1. Electron Controllers warp out of the Interceptor room and end up
-     unreachable behind the door; both must stay reachable and both deaths must
-     open the gate.
-  2. Escape applies shock; it must be fire/scorch.
-  3. The climb pipes toward the fusion-cell deposit must apply that same scorch
-     — before deposit, while carrying.
-  4. Beam must shut off after the final cell is deposited.
-  5. Beam must surge with the screen effect before clamshell exposure, not sit
-     visually constant.
-  6. Escape explosions must fire progressively as you run it, not all at
-     deposit.
-  7. Escape freezes before the ending movies; both must play and complete,
-     skippable.
-  8. Objectives flash/re-update constantly without progressing.
-
-  What I'd change, in order
-
-  1 — Ending freeze (ending.lua, routes.lua, apex.lua, 
-  mission_script_runtime_dispatch.cpp, roster publication). The current
-  region-49 staging detour was built on a same-bubble-teardown hypothesis and
-  still freezes. I'm removing it, not extending it — no more staging states, no
-  sleeps. The Omega excerpt says authority receipt and native application are
-  separate stages, and application was blocked because allocated global records
-  were never seeded. I'll capture requested/current/held region, seed-lease
-  revision, publication state, received cinematic authority and applied
-  controller state at the failure point; enumerate the allocated global records
-  and find which are unseeded; then fix the actual producer in roster
-  construction. I will not fabricate zero bodies or globally bypass the
-  readiness gate, and I won't copy Omega's record ID or counts. Targets stay
-  STATE_80B3C09E_0000_0001_80B3C091/region 1 and ..._0002_80B3C093/region 2.
-
-  2 — Objectives (route_support.lua, landing.lua, 
-  mission_script_lua_slot_api.cpp, auth codec). Two separate bugs. The republish
-  signature is hash .. ':' .. shown, so every navpoint or combat change
-  republishes the whole directive — that's the flash. And the type-70 engagement
-  audience binding added last patch didn't produce correct progression, so that
-  inference is incomplete. I'll log actual outgoing directive
-  hash/element/state/lane/audience against native applied HUD entry and
-  membership changes, separate objective transitions from navpoint visibility,
-  and fix the banner lifecycle. No debounce papering over wrong text.
-
-  3 — Electron Controllers (route_roster.lua, encounter.lua, combat_ai.lua, 
-  apex.lua). The fixed_tasks={9,10} pin was inferred from task ordering and
-  failed in game. I'm dropping that assumption rather than adding another guess,
-  capturing live actor position, selected objective/task and candidate areas
-  before and after they escape the room, and determining whether the warp is
-  objective-driven or a separate actor behaviour. Then constrain it to reachable
-  positions. Both deaths — not vehicle boarding, not supports — open
-  SECURITY_DOOR_DEVICE. I won't freeze all AI to achieve this.
-
-  4 — Beam (apex.lua:beam()). Position and power on the laser/ring devices both
-  failed, so I won't try a third polarity. I'll trace the placed-object effect
-  graph and the separate player screen effect from the cached components, verify
-  the authority actually lands on the intended placed instance, and match surge
-  → pulse → shutter against 23:38–23:45. On final deposit, stop the cycle and
-  shut the beam down — current code wrongly opens both devices — while
-  preserving escape geometry, and keep it off across a checkpoint restart.
-
-  5 — Hazards and explosions (hazards(), effect filters). Find what actually
-  causes shock and identify the real fire/scorch resource by behaviour, not by a
-  suggestive slot name. Move the pipe scorch from phase 6 to phase 5 so it's
-  live during the carry, restricted to the five narrow hot-pipe volumes (slots
-  3/4/5/6/8 — not slot 7, that's a kill volume), removed on leaving contact. The
-  rail-top filter using the whole slot-414 volume is suspect and I'll check it
-  before extending it. Then arm the four authored explosion triggers (type31
-  slots 105–108 / type60 volumes 242–245) to fire as you reach each section,
-  one-shot, instead of dumping both scenes at deposit.
-
-  Constraints I'll hold to
-
-  Never launch or stop the game — read-only captures only, and I'll confirm it's
-  closed before installing. Backup + SHA-verify every install, preserve
-  save/settings/SDK. Progress updates at least every minute, no silent stalls.
-  Completed areas stay completed on backtracking. Tube deaths stay untouched —
-  you withdrew that as noclip. Commit without a co-author tag, no push unless
-  you ask. Sandbox has no string.find/match/gmatch/gsub or dump. Budget headroom
-  is tight: 233/512 variables, 61/64 intents per callback — I won't crowd more
-  into existing callbacks.
-
-  One recommendation
-
-  Items 1 and 3 are diagnosis-first — I may not be able to fix them without a
-  capture from a live run, which is a two-playtest cycle. Items 2, 4 and 5 I can
-  implement directly. So I'd suggest one install containing the 2/4/5 fixes 
-  plus the diagnostics for 1 and 3, you do a single run, and I get both those
-  fixes validated and the freeze/warp evidence in one pass. That beats burning a
-  playtest on diagnostics alone. If you'd rather I chase the freeze in
-  isolation first, say so.
-
-  Two things I want to flag honestly: I can't confirm any of this visually
-  myself, so every "fixed" I report will be qualified until you've played it;
-  and item 1 is the one where I genuinely don't know yet whether the fix is
-  small or deep, because the root cause isn't established.
-
-  Happy to adjust any of the above — give me the go-ahead when you're ready.
-
-✻ Brewed for 2m 14s
-
-❯ I think you can probably resolve 1 and 3 without live captures, all the       
-  information you need should already exist in the sdk and/or pkg files. Make a 
-  backup of everything you touch first incase you break everything, then go     
-  ahead and execute your plan. Only alert me when all the fixes are done.       
-
-● Executing. Starting with a full backup, then working through the five items.
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-
-● Backup at build/final-issues-backup-20260906-115511. Now reading the current
-  implementation.
-
-  Read 1 file, ran 5 shell commands (ctrl+o to expand)
-
-● Key finding already: regions 0, 1, 2 share slice_set_index 0, but the staging
-  state (region 49) is slice set 48. Let me check the native seeding path.
-
-  Searched for 2 patterns, listed 1 directory, ran 2 shell commands (ctrl+o to expand)
-
-● Strong lead on the freeze. Let me check the roster capacities against actual
-  per-region group counts.
-  Searched for 1 pattern, listed 1 directory, ran 9 shell commands (ctrl+o to expand)
-
-● refuse_seed logs at debug level, so refusals are invisible in these info-level
-  logs. Let me quantify the group counts per region.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Capacity hypothesis is wrong (37 groups vs 256). Checking the dedup path
-  instead — shared object tags across regions 0/1/2.
-
-  Searched for 1 pattern, ran 4 shell commands (ctrl+o to expand)
-
-● Root cause found. Let me confirm the slice-set vs region-index conflation.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Confirmed the deadlock. Let me verify the supporting APIs before changing
-  anything.
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-
-● Item 1 is fully diagnosed. Now investigating items 3–5 from the SDK.
-
-  Read 1 file, ran 4 shell commands (ctrl+o to expand)
-
-● Trigger volumes carry AABB geometry. This resolves item 5 directly.
-
-  Ran 6 shell commands (ctrl+o to expand)
-
-● All configs are already extracted. Let me decode the effect resources.
-
-  Ran 6 shell commands (ctrl+o to expand)
-● Identified the scorch resource. Let me check how effect/filter pairing is
-  validated natively.
-
-  Searched for 4 patterns, ran 8 shell commands (ctrl+o to expand)
-
-● Let me try extracting the one missing effect resource to confirm the
-  shock/fire identification.
-
-  Listed 2 directories (ctrl+o to expand)
-
-● The extractor is available. Pulling the one missing effect resource plus the
-  AI task-area data I need for item 3.
-
-  Ran 6 shell commands (ctrl+o to expand)
-
-● Now item 3 — parsing the access objective's task groups.
-
-  Ran 9 shell commands (ctrl+o to expand)
-
-● Let me confirm the room geometry from the authored trigger volumes.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Item 3 confirmed by geometry. Now the beam surge — looking for authored
-  sequences and screen effects.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Now item 2 — the HUD directives. Let me inspect the authored directive data
-  and the native encoder.
-
-  Searched for 2 patterns, ran 2 shell commands (ctrl+o to expand)
-
-● I have all five diagnosed. Reading the last two modules before editing.
-
-  Read 1 file (ctrl+o to expand)
-● All five items diagnosed from the SDK and packages. Now implementing. Starting
-  with the native ending-freeze fixes.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Update(Sunrise/src/server/bap/bap_route.cpp)
-  ⎿  Added 10 lines, removed 2 lines
-      1128                  }
-      1129                  lease.registeredRegions[lease.registeredRegio
-            nCount++] = plan.effectiveRegion;
-      1130              }
-      1131 -            // A region change replaces the instantiated worl
-           -d. Publications keep answering the                           
-      1131 +            // A SLICE-SET change replaces the instantiated w
-           +orld. Publications keep answering the                        
-      1132              // previous plan until the client's post-arrival 
-            solicited answer advances the region
-      1133              // epoch, because registering the new region's gr
-            oups mid-teardown races the teardown.
-      1134 -            if (lease.configured && lease.plan.effectiveRegio
-           -n != plan.effectiveRegion) {                                 
-      1134 +            //                                               
-      1135 +            // An authored region is `sliceSetIndex + stateOr
-           +dinal`, so sibling states share one                          
-      1136 +            // slice set: Ember's apex gameplay (0) and both 
-           +ending bookends (1, 2) are all slice                         
-      1137 +            // set 0. Moving between them instantiates nothin
-           +g new, and the client's current region                       
-      1138 +            // leg only advances on an actual slice-set switc
-           +h. Waiting for an arrival there is a                         
-      1139 +            // deadlock: the roster withholds the new region'
-           +s groups forever and the client never                        
-      1140 +            // finishes synchronizing. Only a real slice-set 
-           +change opens the arrival window.                             
-      1141 +            if (lease.configured && lease.plan.effectiveRegio
-           +n != plan.effectiveRegion                                    
-      1142 +                && lease.plan.sliceSetIndex != plan.sliceSetI
-           +ndex) {                                                      
-      1143                  lease.previousPlan = lease.plan;
-      1144                  lease.regionArrivalPending = true;
-      1145              }
-
-● Now the teleport arming fix:
-
-● Update(Sunrise/src/server/activity/mission/mission_script_runtime_dispatch.cpp
-        )
-  ⎿  Added 13 lines
-      212              instance.view.binding.sessionId, membership::kAbse
-           ntSliceSetIndex, 0));
-      213          return;
-      214      }
-      215 +    // A region is `sliceSetIndex + stateOrdinal`, so a siblin
-          +g state sits in the slice set the                             
-      216 +    // client already holds and there is nothing to transition
-          + to. Arming anyway hands the client                           
-      217 +    // a slice-set index that is not a slice set (region 1 wit
-          +h slice set 0's name hash), and it                            
-      218 +    // starts a teleportation it can never finish. Clear the a
-          +rm and let the roster publish the                             
-      219 +    // new state's groups into the world that is already stand
-          +ing.                                                          
-      220 +    const std::int32_t heldSliceSet =                         
-      221 +        membership::reported_slice_set(instance.view.binding.s
-          +essionId);                                                    
-      222 +    if (heldSliceSet >= 0 && heldSliceSet == static_cast<std::
-          +int32_t>(plan.sliceSetIndex)) {                               
-      223 +        static_cast<void>(membership::arm_host_teleport(      
-      224 +            instance.view.binding.sessionId, membership::kAbse
-          +ntSliceSetIndex, 0));                                         
-      225 +        log_line(core::log::Level::info, &instance, "state_reg
-          +ion", "teleport_not_required");                               
-      226 +        return;                                               
-      227 +    }                                                         
-      228      const std::string_view name(reinterpret_cast<const char*>(
-           destination.packageName.data()),
-      229                                  destination.packageNameLength)
-           ;
-      230      ::sunrise::state::build_data::scenarios::Definition layout
-           {};
-  ⎿  Found 1 new diagnostic issue in 1 file (ctrl+o to expand)
-
-● Update(Sunrise/src/server/bap/encrypted/push/activity/activity_mission_seed_ro
-        ster.cpp)
-  ⎿  Added 3 lines, removed 1 line
-      28                                        static_cast<int>(reason.s
-          ize()),
-      29                                        reason.data());
-      30      if (written > 0) {
-      31 +        // A refusal strands the selected state: its records ar
-         +e never seeded and the client waits                            
-      32 +        // for content that never arrives. That is not a debug 
-         +detail, so it is reported at warn.                             
-      33          core::log::write(core::log::Channel::server,
-      32 -                         core::log::Level::debug,              
-      34 +                         core::log::Level::warn,               
-      35                           {line.data(), static_cast<std::size_t>
-          (written)});
-      36      }
-      37      return MissionSeedRosterResult::refused;
-  ⎿  Found 4 new diagnostic issues in 1 file (ctrl+o to expand)
-
-  Ran 1 shell command (ctrl+o to expand)
-  ⎿  Found 3 new diagnostic issues in 1 file (ctrl+o to expand)
-
-● Now the Lua side. Let me check how the root controller derives region state.
-
-  Searched for 2 patterns, read 1 file, ran 2 shell commands (ctrl+o to expand)
-
-● Now rewriting ending.lua to drop the staging detour:
-
-● Write(scripts/mission_ember/ending.lua)
-  ⎿  Added 23 lines, removed 23 lines
-       1 --- Authored post-escape cinematic regions; exact native complet
-         -ion/skip incidents advance once.                               
-       1 +-- Authored post-escape cinematic states; exact native completi
-         +on/skip incidents advance once.                                
-       2 +--                                                             
-       3 +-- An authored region is `sliceSetIndex + stateOrdinal`, so ape
-         +x gameplay (region 0) and both                                 
-       4 +-- ending bookends (regions 1 and 2) are sibling states of one 
-         +slice set. Selecting a bookend                                 
-       5 +-- instantiates no new world: the client keeps the slice set it
-         + already holds and never reports                               
-       6 +-- a new held region, so this must not wait for one. The earlie
-         +r region-49 staging detour forced                              
-       7 +-- a cross-slice-set round trip to manufacture that report and 
-         +stalled instead.                                               
-       8 +--                                                             
-       9 +-- The selection intent completes only once its own roster revi
-         +sion publishes, and intents are                                
-      10 +-- dispatched in order, so the activation queued on a later cal
-         +lback always follows the seed.                                 
-      11  return function(m)
-      12      local movies = {
-      13          {state = m.states.STATE_80B3C09E_0000_0001_80B3C091, sl
-          ot = m.Slot.PF_CINEMATIC_BOOKEND_STM_CINEMATIC},
-      14          {state = m.states.STATE_80B3C09E_0000_0002_80B3C093, sl
-          ot = m.Slot.PF_CINEMATIC_BOOKEND_CNN_CINEMATIC},
-      15      }
-      16      local E = {}
-       8 -    local staging = assert(m.states.STATE_80B3C09E_0006_0001_80
-         -B3C09A)                                                        
-       9 -    local function transit(c)                                  
-      10 -        c:set_variable("ember.ending.transit", true)           
-      11 -        c:select_state(staging)                                
-      12 -    end                                                        
-      17      local music = require("mission_ember.music")(m)
-      18 +    local function select_movie(c, index)                      
-      19 +        c:set_variable("ember.ending", index)                  
-      20 +        c:set_variable("ember.ending.selected", index)         
-      21 +        c:select_state(assert(movies[index].state))            
-      22 +    end                                                        
-      23      function E.start(c, s)
-      24          if s:variable("ember.ending") then return end
-      16 -        c:set_variable("ember.ending", 1)                      
-      25          music.update(c, s)
-      18 -        transit(c)                                             
-      26 +        select_movie(c, 1)                                     
-      27      end
-      28      function E.client(c, s, e)
-      29          local index = s:variable("ember.ending")
-      30          local row = index and movies[index]
-      23 -        if not row or s:variable("ember.ending.playing") == ind
-         -ex then return end                                             
-      24 -        local held = e.held_region_index or e.current_region_in
-         -dex                                                            
-      25 -        if s:variable("ember.ending.transit") then             
-      26 -            if held == staging.region_index then               
-      27 -                c:clear_variable("ember.ending.transit")       
-      28 -                c:select_state(row.state)                      
-      29 -            end                                                
-      30 -            return                                             
-      31 -        end                                                    
-      32 -        if held == row.state.region_index then                 
-      33 -            c:set_variable("ember.ending.playing", index)      
-      34 -            c:slot(assert(row.slot)):set_cinematic_active{activ
-         -e = true}                                                      
-      35 -        end                                                    
-      31 +        -- Activate on a callback after the selection, never in
-         + the one that requested it.                                    
-      32 +        if not row or s:variable("ember.ending.selected") ~= in
-         +dex                                                            
-      33 +            or s:variable("ember.ending.playing") == index then
-         + return end                                                    
-      34 +        c:set_variable("ember.ending.playing", index)          
-      35 +        c:slot(assert(row.slot)):set_cinematic_active{active = 
-         +true}                                                          
-      36      end
-      37      function E.terminated(c, s, e)
-      38          local index = s:variable("ember.ending")
-     ...
-      40          if not row or s:variable("ember.ending.playing") ~= ind
-          ex then return end
-      41          local slot = c:slot(row.slot)
-      42          if e.registry_key ~= slot.registry_key or e.slot_type ~
-          = slot.slot_type or e.slot_index ~= slot.slot_index then return
-           end
-      43 -        c:set_variable("ember.ending", index + 1)              
-      43          slot:set_cinematic_active{active = false}
-      45 -        if movies[index + 1] then transit(c)                   
-      44 +        if movies[index + 1] then select_movie(c, index + 1)   
-      45          else
-      46 +            c:set_variable("ember.ending", index + 1)          
-      47              c:set_variable("ember.complete", true)
-      48              -- Native lifetime 6 enters the completion/reward b
-          ranch (BEA9D0/B37100).
-      49              c.lifetime:set{state = c.sdk.lifetime_states:at(6)}
-
-● Now item 2 — the flashing HUD directives:
-
-● Update(scripts/mission_ember/route_support.lua)
-  ⎿  Added 8 lines, removed 1 line
-       99          c:set_variable("ember.checkpoint.hash", hash)
-      100          c:set_variable("ember.checkpoint.name", name)
-      101      end
-      102 +    -- Each authored directive carries both HUD lines: `title`
-          + is the main objective and                                    
-      103 +    -- `description` the sub-objective, so the banner advances
-          + by selecting a new element.                                  
-      104 +    -- Native type-68 state 0 ENTERS the directive, and the Lu
-          +a surface always publishes it,                                
-      105 +    -- so every republication replays the banner's entry anima
-          +tion. Republishing whenever the                               
-      106 +    -- navpoint changed therefore made the objective flash on 
-          +each combat transition while its                              
-      107 +    -- text stayed put. The directive is now keyed on its elem
-          +ent alone and carries one stable                              
-      108 +    -- navpoint, so the body changes only at a real milestone 
-          +and enters exactly once there.                                
-      109      function api.directive(c, s, hash, marker, combat)
-      110          local rank = ({[56] = 1, [40] = 2, [0] = 3})[s:variabl
-           e("ember.region")] or 0
-      111          if rank < (s:variable("ember.r.furthest") or 0) then r
-           eturn end
-      112          music.update(c, s)
-      106 -        local shown = combat and "" or (marker or "")         
-      113 +        local shown = marker or ""                            
-      114          local signature = hash .. ":" .. shown
-      115          if s:variable("ember.r.guidance") == signature then re
-           turn end
-      116          local binding = assert(directives[hash], "missing auth
-           ored directive " .. hash)
-
-  Searched for 2 patterns, ran 1 shell command (ctrl+o to expand)
-
-● All 17 call sites updated. Now item 3 — the Electron Controllers' objective:
-
-● Update(scripts/mission_ember/route_roster.lua)
-  ⎿  Added 10 lines, removed 2 lines
-      29      {name = "foundry_final", region = 40, objective = "EMBER_CI
-          NDER_FOUNDRY_OBJECTIVE", task_groups = 16, squads = {"FOUNDRY_A
-          NCHOR_A_SQUAD", "FOUNDRY_SUPPORT_G_SQUAD", "FOUNDRY_SUPPORT_H_S
-          QUAD", "FOUNDRY_SUPPORT_I_SQUAD"}},
-      30      {name = "access1", region = 0, objective = "EMBER_APEX_ACCE
-          SS_OBJECTIVE", task_groups = 14, squads = {"ACCESS_JUMP_ONE_SUP
-          PORT_A_SQUAD", "ACCESS_JUMP_ONE_SUPPORT_B_SQUAD", "ACCESS_JUMP_
-          ONE_SUPPORT_C_SQUAD"}},
-      31      {name = "access2", region = 0, objective = "EMBER_APEX_ACCE
-          SS_OBJECTIVE", task_groups = 14, squads = {"ACCESS_JUMP_TWO_SUP
-          PORT_A_SQUAD", "ACCESS_JUMP_TWO_SUPPORT_B_SQUAD", "ACCESS_JUMP_
-          TWO_SUPPORT_C_SQUAD"}},
-      32 -    {name = "electron_controllers", region = 0, objective = "EM
-         -BER_APEX_ACCESS_OBJECTIVE", task_groups = 14, fixed_tasks = {9,
-         - 10}, squads = {"DISPENSER_SUPPORT_A_SQUAD", "DISPENSER_SUPPORT
-         -_B_SQUAD"}},                                                   
-      33 -    {name = "dispenser", region = 0, objective = "EMBER_APEX_AC
-         -CESS_OBJECTIVE", task_groups = 14, squads = {"DISPENSER_SUPPORT
-         -_C_SQUAD"}},                                                   
-      32 +    -- The dispenser squads hold the security room, not the acc
-         +ess approach. Their authored                                   
-      33 +    -- anchors (-363,2643,181.5) and (-360,2645,181.5) are both
-         + inside                                                        
-      34 +    -- `security_center_player_trigger` (-387..-344.5, 2639.5..
-         +2662, 177..197), while every                                   
-      35 +    -- access volume lies east of x=-331 (jump one -331..-312, 
-         +jump two -256.5..-224, inner                                   
-      36 +    -- door -224..-174). Under the access objective the native 
-         +task selector pulled the two                                   
-      37 +    -- Electron Controllers out to those eastern areas -- throu
-         +gh the security door and out of                                
-      38 +    -- reach. No access task group could hold them, so pinning 
-         +a pair of them never could work.                               
-      39 +    -- The security objective owns this room, so they keep thei
-         +r own cost-selected tasks here.                                
-      40 +    {name = "electron_controllers", region = 0, objective = "EM
-         +BER_APEX_SECURITY_OBJECTIVE", task_groups = 7, squads = {"DISPE
-         +NSER_SUPPORT_A_SQUAD", "DISPENSER_SUPPORT_B_SQUAD"}},          
-      41 +    {name = "dispenser", region = 0, objective = "EMBER_APEX_SE
-         +CURITY_OBJECTIVE", task_groups = 7, squads = {"DISPENSER_SUPPOR
-         +T_C_SQUAD"}},                                                  
-      42      {name = "security", region = 0, objective = "EMBER_APEX_SEC
-          URITY_OBJECTIVE", task_groups = 7, squads = {"SECURITY_LEDGE_SU
-          PPORT_A_SQUAD", "SECURITY_LEDGE_SUPPORT_B_SQUAD"}},
-      43      {name = "reactor_east_entry", region = 0, objective = "EMBE
-          R_APEX_REACTOR_CLAMSHELL_EAST_OBJECTIVE", task_groups = 19, squ
-          ads = {"REACTOR_CLAMSHELL_EAST_DEFENSE_A_SQUAD", "REACTOR_CLAMS
-          HELL_EAST_SUPPORT_A_SQUAD", "REACTOR_CLAMSHELL_EAST_SUPPORT_B_S
-          QUAD"}},
-      44      {name = "reactor_east_reinforce", region = 0, objective = "
-          EMBER_APEX_REACTOR_CLAMSHELL_EAST_OBJECTIVE", task_groups = 19,
-           squads = {"REACTOR_CLAMSHELL_EAST_DEFENSE_B_SQUAD", "REACTOR_C
-          LAMSHELL_EAST_MELEE_A_SQUAD", "REACTOR_CLAMSHELL_EAST_MELEE_B_S
-          QUAD", "REACTOR_CLAMSHELL_EAST_SUPPORT_C_SQUAD", "REACTOR_CLAMS
-          HELL_EAST_SUPPORT_D_SQUAD"}},
-
-● Let me check fire_trigger and scene:activate semantics before wiring the
-  explosions.
-
-  Searched for 4 patterns, ran 1 shell command (ctrl+o to expand)
-
-● fire_trigger arms the authored trigger (the escape dialogue cues rely on that
-  today). Now editing apex.lua for items 4 and 5:
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 32 lines, removed 11 lines
-      41          end
-      42          a.device(c, "REACTOR_SHIELD_DEVICE", open, snap)
-      43      end
-      44 +    -- The three placed ring objects carry the beam's own autho
-         +red effect graphs; the two                                     
-      45 +    -- devices are its mechanical lanes. Holding all three obje
-         +cts active for the whole fight                                 
-      46 +    -- is what made the beam look constant, so the surge is tho
-         +se authored objects coming up                                  
-      47 +    -- with their devices at the warning and going dark again a
-         +t recovery.                                                    
-      48 +    local ring_objects = {"SPECOPS_APEX_RING_LASER_OBJECT", "SP
-         +ECOPS_APEX_RING_CORE_OBJECT",                                  
-      49 +        "SPECOPS_APEX_RING_RING_OBJECT"}                       
-      50      local function beam(c, active, snap)
-      45 -        -- Both devices address the placed ring/laser objects a
-         -nd their native VFX graphs.                                    
-      51 +        a.objects(c, ring_objects, active)                     
-      52          a.device(c, "SPECOPS_APEX_RING_LASER_DEVICE", active, s
-          nap)
-      53          a.device(c, "SPECOPS_APEX_RING_RING_DEVICE", active, sn
-          ap)
-      54          -- FX power is independent of mechanical position; puls
-          e both authored graphs.
-      55          lane(c, "SPECOPS_APEX_RING_LASER_DEVICE", active and "p
-          ower_on" or "power_off", snap)
-      56          lane(c, "SPECOPS_APEX_RING_RING_DEVICE", active and "po
-          wer_on" or "power_off", snap)
-      57      end
-      52 -    local function hazards(c, s, enabled)                      
-      53 -        a.effect(c, s, "REACTOR_MOTHER_BRAIN_HOT_PIPES_THERMAL_
-         -HOP_ON",                                                       
-      54 -            "REACTOR_MOTHER_BRAIN_HOT_PIPES_OBJECT_FILTER_80B3C
-         -09F",                                                          
-      55 -            {players = true, inside_any = {                    
-      56 -                a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_02_TR
-         -IGGER_VOLUME"),                                                
-      57 -                a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_03_TR
-         -IGGER_VOLUME"),                                                
-      58 -                a.slot(c, "SLOT_0005_80B3C09F"), a.slot(c, "SLO
-         -T_0006_80B3C09F"),                                             
-      59 -                a.slot(c, "SLOT_0008_80B3C09F")}}, enabled)    
-      60 -        a.effect(c, s, "AOD_REACTOR_RAIL_TOP_HOP_ON", "AOD_REAC
-         -TOR_RAIL_TOP_OBJECT_FILTER",                                   
-      61 -            {players = true, inside = a.slot(c, "SLOT_019E")}, 
-         -enabled)                                                       
-      58 +    -- `REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON` and `FOUNDRY_TH
-         +ERMAL_DOT_HOP_ON` both reference                               
-      59 +    -- effect resource 80C1D9E0 -- the burn already working in 
-         +the Foundry. The hot-pipe and                                  
-      60 +    -- rail-top hop-ons carry 80B82484 and 80C1D389 instead, wh
-         +ich is why contact read as a                                   
-      61 +    -- shock rather than a scorch. Apex drives the one authored
-         + scorch for both hazards and                                   
-      62 +    -- swaps only its filter, so the climb and the escape share
-         + the same effect. A new revision                               
-      63 +    -- removes the previous attachment (native 9EF8A0/9F1F10) b
-         +efore attaching the new filter.                                
-      64 +    local function rail_filter(c) return {players = true, insid
-         +e = a.slot(c, "SLOT_019E")} end                                
-      65 +    local function hazards(c, s, mode)                         
-      66 +        if mode == "climb" then                                
-      67 +            -- The five narrow authored pipe volumes on the way
-         + up to the deposit. Slot 7 is a                                
-      68 +            -- broad kill volume well below the walkable route 
-         +and is deliberately not a pipe.                                
-      69 +            a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP
-         +_ON",                                                          
-      70 +                "REACTOR_MOTHER_BRAIN_HOT_PIPES_OBJECT_FILTER_8
-         +0B3C21C",                                                      
-      71 +                {players = true, inside_any = {                
-      72 +                    a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_0
-         +2_TRIGGER_VOLUME"),                                            
-      73 +                    a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_0
-         +3_TRIGGER_VOLUME"),                                            
-      74 +                    a.slot(c, "SLOT_0005_80B3C09F"), a.slot(c, 
-         +"SLOT_0006_80B3C09F"),                                         
-      75 +                    a.slot(c, "SLOT_0008_80B3C09F")}}, true)   
-      76 +        elseif mode == "escape" then                           
-      77 +            a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP
-         +_ON",                                                          
-      78 +                "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filt
-         +er(c), true)                                                   
-      79 +        else                                                   
-      80 +            a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP
-         +_ON",                                                          
-      81 +                "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_filt
-         +er(c), false)                                                  
-      82 +        end                                                    
-      83      end
-      84      local function doors(c, side, open, snap)
-      85          for _, part in ipairs({"DOOR_A", "DOOR_B", "LIGHT_A", "
-          LIGHT_B", "TARGET"}) do
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now the deposit callback — beam shutdown and progressive explosions:
-
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 6 lines, removed 4 lines
-      157              a.device(c, "MOTHER_BRAIN_ENGINE_RIGHT_DEVICE", tr
-           ue)
-      158              a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", true)
-      159              a.objects(c, {"REACTOR_GETAWAY_SHIP_OBJECT", "SUNB
-           URN_DAMAGE_OBJECT"}, true)
-      160 -            a.device(c, "SPECOPS_APEX_RING_LASER_DEVICE", true
-          -)                                                             
-      161 -            a.device(c, "SPECOPS_APEX_RING_RING_DEVICE", true)
-      160 +            -- The weapon is dead once the cell is in: the bea
-          +m shuts down and stays down,                                  
-      161 +            -- including across an escape checkpoint restart. 
-          +The previous code opened both                                 
-      162 +            -- devices here, which left it running through the
-          + whole escape.                                                
-      163 +            beam(c, false, true)                              
-      164 +            c:cancel_timer(vent_timer(s))                     
-      165              a.scene(c, "MOTHER_BRAIN_HOLE_EXPLOSION_SCENE")
-      163 -            a.scene(c, "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_T
-          -RIGGERED_EXPLOSIONS_SCENE")                                   
-      164 -            for _, name in ipairs(escape_triggers) do a.slot(c
-          -, name):fire_trigger{} end                                    
-      166 +            arm_escape(c)                                     
-      167              a.cue(c, s, 51); A.guidance(c, s)
-      168          end)
-      169      function A.enter(c, s)
-
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 9 lines
-      16      local escape_triggers = {"APEX_MOTHER_BRAIN_005_DIALOG_PLAY
-          ER_TRIGGER", "APEX_MOTHER_BRAIN_006_DIALOG_PLAYER_TRIGGER",
-      17          "APEX_MOTHER_BRAIN_007_DIALOG_PLAYER_TRIGGER", "APEX_MO
-          THER_BRAIN_008_DIALOG_PLAYER_TRIGGER",
-      18          "APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_PLAYER_TRIGGER"}
-      19 +    -- The authored explosion prefab's four player triggers, we
-         +st to east along the rails:                                    
-      20 +    -- set A x -448.75..-438.75, B -403.75..-393.75, C -368.75.
-         +.-358.75, D -323.75..-313.75,                                  
-      21 +    -- all spanning y 2967..3002.5 and z 185..212.5. They were 
-         +never armed, so the sequence                                   
-      22 +    -- had nothing to advance it and everything the scene did h
-         +appened at the deposit.                                        
-      23 +    local explosion_triggers = {}                              
-      24 +    for _, set in ipairs({"A", "B", "C", "D"}) do              
-      25 +        explosion_triggers[#explosion_triggers + 1] =          
-      26 +            "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_EXPLOSION_SET
-         +_" .. set .. "_PLAYER_TRIGGER"                                 
-      27 +    end                                                        
-      28      local function phase(s) return s:variable("ember.apex.phase
-          ") or 0 end
-      29      local function set(c, p) c:set_variable("ember.apex.phase",
-           p) end
-      30      local function generation(s) return s:variable("ember.apex.
-          generation") or 1 end
-
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 7 lines
-       90                  "AOD_REACTOR_RAIL_TOP_OBJECT_FILTER", rail_fil
-           ter(c), false)
-       91          end
-       92      end
-       93 +    -- Activate the authored explosion scene once, then arm it
-          +s own four progress triggers                                  
-       94 +    -- alongside the escape dialogue volumes so each section d
-          +etonates as the player reaches it.                            
-       95 +    local function arm_escape(c)                              
-       96 +        a.scene(c, "EMBER_APEX_EXPLOSION_SEQUENCE_PREFAB_TRIGG
-          +ERED_EXPLOSIONS_SCENE")                                       
-       97 +        for _, name in ipairs(explosion_triggers) do a.slot(c,
-          + name):fire_trigger{} end                                     
-       98 +        for _, name in ipairs(escape_triggers) do a.slot(c, na
-          +me):fire_trigger{} end                                        
-       99 +    end                                                       
-      100      local function doors(c, side, open, snap)
-      101          for _, part in ipairs({"DOOR_A", "DOOR_B", "LIGHT_A", 
-           "LIGHT_B", "TARGET"}) do
-      102              local name = "REACTOR_CLAMSHELL_" .. side .. "_" .
-           . part .. "_DEVICE"
-
-● Now removing the always-on ring objects from A.enter and updating the
-  timer/reset/resume paths:
-
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 1 line, removed 2 lines
-      194          a.device(c, "ACCESS_DOOR_OUTER_DEVICE", true)
-      195          a.device(c, "SECURITY_DOOR_DEVICE", false, true)
-      196          a.slot(c, "SECURITY_PLACED_INTERCEPTOR_OBJECT"):set_in
-           teractable_object{generation = 1}
-      197 -        a.objects(c, {"SPECOPS_APEX_RING_LASER_OBJECT",       
-      198 -            "SPECOPS_APEX_RING_CORE_OBJECT", "SPECOPS_APEX_RIN
-          -G_RING_OBJECT"}, true)                                        
-      197 +        -- The ring objects are not activated here: `beam()` o
-          +wns them so they can surge.                                   
-      198          for _, side in ipairs(sides) do
-      199              doors(c, side, false, true)
-      200              a.device(c, "CLAMSHELL_PIPES_" .. side .. "_DEVICE
-           ", false, true)
-
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 6 lines, removed 1 line
-      296      end
-      297      function A.timer(c, s, e)
-      298          if e.timer_name == "ember.apex.hazards" then
-      299 -            if phase(s) == 6 and s:variable("ember.region") ==
-          - 0 then hazards(c, s, true) end                               
-      299 +            -- The climb pipes burn while the cell is being ca
-          +rried up (phase 5), well before                               
-      300 +            -- the deposit; the rail top burns during the esca
-          +pe (phase 6).                                                 
-      301 +            if s:variable("ember.region") == 0 then           
-      302 +                if phase(s) == 5 then hazards(c, s, "climb")  
-      303 +                elseif phase(s) == 6 then hazards(c, s, "escap
-          +e") end                                                       
-      304 +            end                                               
-      305              return true
-      306          end
-      307          if carry.timer(c, s, e) then return true end
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-
-● Update(scripts/mission_ember/apex.lua)
-  ⎿  Added 3 lines
-      273              set(c, 5)
-      274              c:cancel_timer(vent_timer(s)); c:cancel_timer("emb
-           er.apex.explain." .. generation(s))
-      275              coffin_doors(c, true)
-      276 +            -- The weapon keeps firing until the cell goes in,
-          + so the beam stays up here.                                   
-      277              beam(c, true)
-      278 +            -- Arm the climb scorch now: the pipes burn while 
-          +the cell is carried up.                                       
-      279 +            c:start_timer("ember.apex.hazards", 1)            
-      280              unlock(c, "MOTHER_BRAIN_DOOR_DEVICE")
-      281              unlock(c, "COFFIN_BUNKER_DOOR_SOUTH_DEVICE")
-      282              a.device(c, "MOTHER_BRAIN_DOOR_DEVICE", true)
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● All Lua parses. Now running the test suites:
-
-  Ran 5 shell commands (ctrl+o to expand)
-
-● Intent budget exceeded — beam() now costs more per call. Making it idempotent:
-
-  Ran 4 shell commands (ctrl+o to expand)
-
-● 62/64 intents is tighter than I want. Let me check the cap and trim the peak.
-
-  Ran 5 shell commands (ctrl+o to expand)
-
-● The peak is the reactor checkpoint reset (45 squad retirements). Moving the
-  hazard clear to its own callback:
-
-  Ran 1 shell command (ctrl+o to expand)
-● Back to the previous 61/63 peak. Removing the probe and running the full
-  suite:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● All five Lua suites pass. Now building the native side:
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● The ending fix needs coverage. Let me extract the decision into a testable
-  predicate:
-
-● Update(Sunrise/src/server/bap/encrypted/push/activity/activity_mission_seed_ro
-        ster.h)
-  ⎿  Added 25 lines
-      30  }
-      31  
-      32  /**
-      33 + * Checks whether moving to a newly selected state actually rep
-         +laces the instantiated world.                                  
-      34 + *                                                             
-      35 + * An authored region is `sliceSetIndex + stateOrdinal`, so sib
-         +ling states of one slice set are                               
-      36 + * different regions inside the same instantiated content: Embe
-         +r's apex gameplay (region 0) and                               
-      37 + * its two ending bookends (regions 1 and 2) all live in slice 
-         +set 0. Only a slice-set change                                 
-      38 + * tears the world down and rebuilds it. Treating a sibling mov
-         +e as a replacement deadlocks the                               
-      39 + * publication: the roster withholds the new region's groups un
-         +til the client reports holding it,                             
-      40 + * while the client's current region leg advances only on a rea
-         +l slice-set switch.                                            
-      41 + *                                                             
-      42 + * @param currentSliceSetIndex Slice set the lease's published 
-         +plan belongs to.                                               
-      43 + * @param currentEffectiveRegion That plan's authored region.  
-      44 + * @param selectedSliceSetIndex Slice set the newly selected pl
-         +an belongs to.                                                 
-      45 + * @param selectedEffectiveRegion The newly selected plan's aut
-         +hored region.                                                  
-      46 + * @return True only when the client must tear down and rebuild
-         + its world.                                                    
-      47 + */                                                            
-      48 +[[nodiscard]] constexpr bool                                   
-      49 +mission_seed_region_change_replaces_world(std::uint32_t current
-         +SliceSetIndex,                                                 
-      50 +                                          std::uint32_t current
-         +EffectiveRegion,                                               
-      51 +                                          std::uint32_t selecte
-         +dSliceSetIndex,                                                
-      52 +                                          std::uint32_t selecte
-         +dEffectiveRegion) noexcept {                                   
-      53 +    return currentEffectiveRegion != selectedEffectiveRegion   
-      54 +           && currentSliceSetIndex != selectedSliceSetIndex;   
-      55 +}                                                              
-      56 +                                                               
-      57 +/**                                                            
-      58   * Adds the exact selected-state Auth groups before retained sq
-          uad groups are considered.
-      59   * The full set goes out only once the client reports holding t
-          he selected region.
-      60   * @param hostedBubbles One bit per bubble this link hosts; a r
-          egion outside them adds nothing.
-
-● Update(Sunrise/src/server/bap/bap_route.cpp)
-  ⎿  Added 6 lines, removed 2 lines
-      1138              // leg only advances on an actual slice-set switc
-            h. Waiting for an arrival there is a
-      1139              // deadlock: the roster withholds the new region'
-            s groups forever and the client never
-      1140              // finishes synchronizing. Only a real slice-set 
-            change opens the arrival window.
-      1141 -            if (lease.configured && lease.plan.effectiveRegio
-           -n != plan.effectiveRegion                                    
-      1142 -                && lease.plan.sliceSetIndex != plan.sliceSetI
-           -ndex) {                                                      
-      1141 +            if (lease.configured                             
-      1142 +                && encrypted::push::activity::mission_seed_re
-           +gion_change_replaces_world(                                  
-      1143 +                    lease.plan.sliceSetIndex,                
-      1144 +                    lease.plan.effectiveRegion,              
-      1145 +                    plan.sliceSetIndex,                      
-      1146 +                    plan.effectiveRegion)) {                 
-      1147                  lease.previousPlan = lease.plan;
-      1148                  lease.regionArrivalPending = true;
-      1149              }
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-  ⎿  Found 2 new diagnostic issues in 1 file (ctrl+o to expand)
-
-● Write(Sunrise/src/server/bap/encrypted/push/activity/mission_seed_world_change
-       .h)
-  ⎿  Wrote 36 lines to Sunrise/src/server/bap/encrypted/push/activity/missi
-     on_seed_world_change.h
-       1 #pragma once
-       2 
-       3 #include <cstdint>
-       5 namespace sunrise::server::bap::encrypted::push::activity {
-       6 
-       7 /**
-       8  * Checks whether moving to a newly selected state actually repl
-       9  *
-      10  * An authored region is `sliceSetIndex + stateOrdinal`, so sibl
-         ing states of one slice set are
-     … +42 lines (ctrl+o to expand)
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now the portable regression for the ending fix:
-
-● Write(tests/mission_seed_world_change_test.cpp)
-  ⎿  Wrote 87 lines to tests/mission_seed_world_change_test.cpp
-      1 // Regression for the selected-state arrival window.
-      3 // An authored region is `sliceSetIndex + stateOrdinal`. Ember's 
-        apex gameplay and both ending
-        ), while every earlier area is
-      5 // its own slice set. Only a slice-set change replaces the client
-      6 //
-      7 // Treating a sibling move as a replacement is what froze the mis
-      8 // withheld region 1's groups until the client reported holding r
-        egion 1, and the client's
-        o the report never came.
-     … +103 lines (ctrl+o to expand)
-
-  Searched for 1 pattern, ran 2 shell commands (ctrl+o to expand)
-  ⎿  Found 1 new diagnostic issue in 1 file (ctrl+o to expand)
-
-● Everything builds and passes. Final verification of all suites, then
-  installation:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now installing into the closed game with a backup and hash verification:
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● False positive — my own shell matched on the repo path. Tightening the
-  detection:
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Installation verified; save/settings/SDK untouched. Committing:
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Recording the install evidence in the status doc, per the established
-  convention:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● All five items are fixed, built, tested, installed and committed. The game is
-  closed and untouched — ready for you to launch.
-
-  What was wrong, and what I changed
-
-  1. Ending freeze — root cause found in the SDK, not a staging problem.
-  An authored region is sliceSetIndex + stateOrdinal. Apex gameplay (region 0)
-  and both ending bookends (regions 1 and 2) are sibling states of slice set 0 —
-  moving between them instantiates no new world. But the code treated any
-  region change as a world replacement, so two things deadlocked: the roster
-  opened an "arrival window" and withheld region 1's groups until the client
-  reported holding region 1 (which only happens on a real slice-set switch), and
-  the teleport arm handed the client slice-set index 1 with slice set 0's name
-  hash. Your 10:49 log shows it exactly: Starting teleportation to slice-set '1'
-  ... PRV01.01 (HASH: 0xdfbc1cd9) and then no Finished synchronizing — every
-  working transition has that line. Now only a genuine slice-set change opens
-  the arrival window, an intra-slice-set move arms no teleport at all, and
-  ending.lua selects each bookend directly. The region-49 staging detour is gone
-  — it was forcing a cross-bubble round trip to manufacture a report that will
-  never come.
-
-  I also raised seed refusals from debug to warn. A refusal strands the selected
-  state, and at debug level it was invisible in every log we have.
-
-  2. Electron Controllers — geometry settles it. Both anchors sit inside
-  security_center_player_trigger (x −387…−344.5), while every access volume is
-  east of x=−331. The access objective was pulling them out through the door —
-  and no access task group could have held them, which is why pinning groups
-  9/10 failed. They now use EMBER_APEX_SECURITY_OBJECTIVE, which owns that room,
-  with normal cost selection.
-
-  3. Fire vs shock — identified from the packages.
-  REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON and FOUNDRY_THERMAL_DOT_HOP_ON both
-  reference effect resource 80C1D9E0 — the burn already working in your Foundry.
-  The hot-pipe and rail-top hop-ons carry 80B82484 and 80C1D389, which is why
-  it read as shock. Apex now drives that one authored scorch for both hazards,
-  swapping only the filter: the five narrow pipe volumes during the climb (phase
-  5, before the deposit), then the rail top during the escape.
-
-  4. Beam. All three ring objects were held active the whole fight, so nothing
-  could surge. The laser and ring now rise with their devices at the warning and
-  go dark at recovery; the core stays as the weapon's body. The deposit shuts
-  the beam down (the old code opened both devices there) and it stays off across
-  an escape restart.
-
-  5. Explosions. The prefab's four authored triggers were never armed — nothing
-  existed to advance the sequence past the deposit. They're armed now and fire
-  per section as you run east.
-
-  6. Objectives. Native type-68 state 0 enters the directive, and the Lua
-  surface always publishes it — so republishing on every navpoint change
-  replayed the entry animation while the text stayed put. That was the flashing.
-  The directive is keyed on its element alone with one stable navpoint,
-  entering once per milestone.
-
-  Verification
-
-  22 portable native tests (including a new mission_seed_world_change regression
-  covering sibling states vs. real slice-set changes) and all five Lua suites
-  pass. Full route peaks at 234/512 variables, 61/63 intents per callback, 3/32
-  timers — the same intent peak as the previous build. Installed 18 files, all
-  SHA-verified; backup at build/final-issues-install-backup-20260906-122441,
-  save/settings/SDK preserved. Committed as d7276cd, no co-author tag, not
-  pushed.
-
-  Two things I'd flag honestly: none of this is gameplay confirmation — passing
-  tests only prove the script progression and wire shapes. And apex has no 
-  authored screen-effect slot, so the 23:38 screen pulse should come from the
-  beam objects themselves brightening; I didn't fabricate an overlay for it. If
-  it doesn't appear, that's the one item still genuinely open.
-
-  Worth watching on your run: the controller fight staying in the room, scorch
-  on the climb pipes before the deposit, the beam surging then going dark after
-  it, explosions following you, a stable banner during the reactor fight, and
-  both movies playing through.
-
-✻ Churned for 31m 22s
-※ recap: Goal was fixing the eight remaining 1AU mission issues; all five work items are done, tested, installed and committed. Next: you launch the game manually and play through to confirm the beam, controllers, scorch, explosions, objectives and both ending movies. (disable recaps in /config)
-
-❯ Close, the beam vfx now cycle between disabled and on instead of on and       
-  surging, the red-hot pipes whilst carrying the fusion core in apex aren't     
-  applying the scorch, and the game still freezes when trying to load the       
-  ending cutscene. Also the weapon is fully disabled at the end, it should have 
-  an enabled but powered off state, because otherwise all the structuring       
-  around the beam doesn't exist.                                                
-
-● Four things. Let me start with the freshest evidence — your run's log.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Found it — teleport_armed still fired. Let me see why and what followed.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● No seed refusals — the roster fix worked. The remaining blocker is the
-  teleport arm: my guard read a stale value. Let me confirm and check the
-  scorch.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The climb hazard's volumes sit below the player. Let me check against where
-  you actually walked.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Those positions are the escape run, not the climb. Let me map your full path
-  through apex.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now I have what I need. Four fixes. First the teleport guard — I used a stale
-  source:
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-
-● Now the beam — it must stay enabled with power as the surge lane:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now the climb scorch — I paired the effect with a filter in a different object
-  than its volumes:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now updating the tests for the new beam states and rebuilding:
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● All green. Installing and committing:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The game is still open (destiny2.exe, pid 801895) — I won't touch it.
-  Committing while I wait:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● All four are fixed, built, and tested — but the game is still running
-  (destiny2.exe, pid 801895), so I haven't installed. Close it and I'll install
-  immediately.
-
-  What each one actually was
-
-  Ending freeze — my guard read the wrong source. Your log settles it. The
-  roster half of the previous fix worked: no seed refusal was logged anywhere in
-  the run. But teleport_armed still fired, and the client did exactly what it
-  did before — Starting teleportation to slice-set '1' ... PRV01.01 (HASH: 
-  0xdfbc1cd9), then no Finished synchronizing, then the network_send hitch.
-
-  The reason: I used reported_slice_set(), which reads the newest D6 host 
-  teleport — that was still region 64 from the mission's opening move, because
-  every later area (56, 40, 0) was reached on a z-leg, which doesn't update it.
-  So the guard compared 64 against 0 and never matched. It now derives the held
-  slice set from the region the client actually reports, rounded down to the
-  slice-set factor. Apex reports region 0 → slice set 0 → target region 1 is
-  slice set 0 → no teleport.
-
-  Beam — I was deleting the weapon, not dimming it. You're right, and it's the
-  same root cause as your fourth point. set_object_active{false} removes the
-  placed objects, so the low point wasn't "dark", it was gone, and the deposit
-  left it gone permanently. The three ring objects are now placed once and never
-  deactivated. The beam has three states driven by the two devices instead:
-
-  - off — powered off but still installed: the pose after the cell goes in, so
-    the beam and everything built around it stay in the world
-  - idle — powered and running: the baseline between exposures
-  - surge — powered and driven open: the intensification before the shutters
-    expose a target
-
-  So the cycle is now idle ↔ surge, and the ending state is
-  enabled-but-powered-off, as you described.
-
-  Climb scorch — a cross-object filter that published but attached nothing. I
-  had pointed the apex-object hot-pipes filter at globals-object volumes. The
-  wire accepts that (the refs carry a registry key), which is why nothing
-  errored, but it attached nothing. The filter living in the same object as
-  those volumes is the pairing that demonstrably delivered contact damage before
-  — that's where your escape shock came from. It now uses that filter; the only
-  thing I changed from the original is the effect resource.
-
-  Worth noting the geometry cross-checks out: the five pipe volumes climb from
-  z≈172 at x=−395 to z≈186 at x=−460, tracking the same westward-and-upward path
-  as the mother-brain dialogue volumes (001→004), which is the carry route to
-  the deposit. Two of them (x −324, −294) sit on the escape rails instead —
-  that's why the old build's shock hit you during the escape.
-
-  State
-
-  22 native tests and all five Lua suites pass; route peaks unchanged at 234/512
-  variables, 61/63 intents, 3/32 timers. Committed as de8c95e. The build is
-  ready in build/x64/Release/.
-
-  Close the game and say the word — I'll install with a fresh backup and
-  hash-verify, and you can retest. On that run the things to watch are whether
-  the beam now reads as running-vs-surging rather than blinking, whether the
-  pipes burn on the way up, and whether the ending reaches the first movie — the
-  log line to look for is state_region result=teleport_not_required instead of
-  teleport_armed.
-
-✻ Baked for 6m 37s
-
-❯ sorry you can install now                                                     
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Installed and verified — ready to launch whenever you are.
-
-  Install: 18 files, all SHA-verified. DLL c10b5192f4f7c503…. Backup at
-  build/final-issues-install-backup-20260906-124732, manifest at
-  build/final-issues-installation.json. Save, settings and SDK untouched. I
-  archived your 12:26 run as
-  build/reactor-playtest/20260906-122600-teleport-armed.log before it gets
-  overwritten, since it's the log that proved the teleport diagnosis.
-
-  What's in this build:
-
-  - Ending — the guard now derives your held slice set from the region you
-    actually report, instead of the stale D6 teleport record that still said
-    "64" from the opening move.
-  - Beam — three ring objects placed once and never removed; three states on the
-    devices: powered-off-but-installed after the deposit, powered-and-running
-    as the baseline, powered-and-driven-open as the surge.
-  - Climb scorch — filter and volumes back in the same object, keeping only the
-    corrected scorch resource.
-
-  What to look for on this run:
-
-  The single most diagnostic line is in the log at the escape: state_region 
-  result=teleport_not_required. If you see that instead of teleport_armed, the
-  bogus slice-set transition is gone and the first movie should follow. If it
-  still says teleport_armed, the guard is still missing and I'll want the log
-  again.
-
-  Otherwise: whether the beam reads as running↔surging rather than blinking in
-  and out, whether it's still visibly present but dead after the deposit, and
-  whether the pipes burn on the way up with the cell.
-
-  One thing I want to be straight about: the beam surge is the least certain of
-  the four. I've established that the ring objects are the visual carrier (your
-  report proved that — toggling them changed what you saw), and that the
-  devices' power and drive lanes are the authored lanes. But I have not proven
-  that driving those lanes produces a visible intensity change, since the
-  earlier build's position+power toggling looked flat to you. If it still reads
-  as constant, that tells me the surge lives somewhere I haven't found yet and
-  I'll go back into the device configs (80B3D4EB/80B3D4EE) rather than guess
-  again.
-
-✻ Cogitated for 46s
-※ recap: We're finishing the Destiny 2 mission 1AU (mission_ember), and the latest build fixing the ending freeze, beam surge, climb scorch and controller warp is installed and verified. Next: launch the game and playtest it. (disable recaps in /config)
-
-❯ Beam VFX still doesn't work as we're expecting, it stays on the whole time    
-  even after dunking the fusion cell, the game no longer freezing when trying   
-  to load the cutscene but it is stuck in an infinite load, the cutscenes never 
-  actually play.                                                                
-
-● Let me get the log from that run first.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● teleport_not_required — that fix landed. Now let me see what happens after.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Only the opening cinematic appears — no cinematic_enqueued for the bookend at
-  all. Let me see what the client did after the selection.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The client never sent another state change after the selection — so my
-  E.client activation never ran. Let me confirm and check the beam objects in
-  the package.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Confirmed — last client region report was t=379518, well before the selection
-  at t=432565. Now the beam objects:
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● The devices are ordinary type-23s (identical class refs to a working door), so
-  they don't drive these visuals — only object presence does. Let me see which
-  object is the beam and which is the structure.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● LASER carries effect components; CORE and RING carry the solid geometry and
-  LOD tables — so the laser is the beam and the other two are the structure.
-  Implementing:
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Now the ending — queue the cinematic directly instead of waiting for a client
-  report that never comes:
-
-● Write(scripts/mission_ember/ending.lua)
-  ⎿  Added 16 lines, removed 19 lines
-       2  --
-       3  -- An authored region is `sliceSetIndex + stateOrdinal`, so ape
-          x gameplay (region 0) and both
-       4  -- ending bookends (regions 1 and 2) are sibling states of one 
-          slice set. Selecting a bookend
-       5 --- instantiates no new world: the client keeps the slice set it
-         - already holds and never reports                               
-       6 --- a new held region, so this must not wait for one. The earlie
-         -r region-49 staging detour forced                              
-       7 --- a cross-slice-set round trip to manufacture that report and 
-         -stalled instead.                                               
-       5 +-- instantiates no new world, so no slice-set teleport is armed
-         + for it -- and that also means the                             
-       6 +-- client never sends another region report. Waiting for one le
-         +aves the movie unstarted forever,                              
-       7 +-- which is exactly what a run with the teleport removed showed
-         +: the state was selected and no                                
-       8 +-- cinematic was ever enqueued.                                
-       9  --
-       9 --- The selection intent completes only once its own roster revi
-         -sion publishes, and intents are                                
-      10 --- dispatched in order, so the activation queued on a later cal
-         -lback always follows the seed.                                 
-      10 +-- So the activation is queued with the selection instead of on
-         + a later callback. Intents are                                 
-      11 +-- dispatched in order and a state selection completes only onc
-         +e its own roster revision has                                  
-      12 +-- published, so the cinematic Auth always follows the seed tha
-         +t carries its slot. Slot handles                               
-      13 +-- resolve against the static SDK definition table, not the sel
-         +ected state, so naming the                                     
-      14 +-- bookend before its state is live is safe.                   
-      15  return function(m)
-      16      local movies = {
-      17          {state = m.states.STATE_80B3C09E_0000_0001_80B3C091, sl
-          ot = m.Slot.PF_CINEMATIC_BOOKEND_STM_CINEMATIC},
-     ...
-      19      }
-      20      local E = {}
-      21      local music = require("mission_ember.music")(m)
-      18 -    local function select_movie(c, index)                      
-      22 +    local function play(c, index)                              
-      23 +        local row = assert(movies[index])                      
-      24          c:set_variable("ember.ending", index)
-      20 -        c:set_variable("ember.ending.selected", index)         
-      21 -        c:select_state(assert(movies[index].state))            
-      25 +        c:set_variable("ember.ending.playing", index)          
-      26 +        c:select_state(assert(row.state))                      
-      27 +        c:slot(assert(row.slot)):set_cinematic_active{active = 
-         +true}                                                          
-      28      end
-      29      function E.start(c, s)
-      30          if s:variable("ember.ending") then return end
-      31          music.update(c, s)
-      26 -        select_movie(c, 1)                                     
-      32 +        play(c, 1)                                             
-      33      end
-      28 -    function E.client(c, s, e)                                 
-      29 -        local index = s:variable("ember.ending")               
-      30 -        local row = index and movies[index]                    
-      31 -        -- Activate on a callback after the selection, never in
-         - the one that requested it.                                    
-      32 -        if not row or s:variable("ember.ending.selected") ~= in
-         -dex                                                            
-      33 -            or s:variable("ember.ending.playing") == index then
-         - return end                                                    
-      34 -        c:set_variable("ember.ending.playing", index)          
-      35 -        c:slot(assert(row.slot)):set_cinematic_active{active = 
-         -true}                                                          
-      36 -    end                                                        
-      34      function E.terminated(c, s, e)
-      35          local index = s:variable("ember.ending")
-      36          local row = index and movies[index]
-     ...
-      38          local slot = c:slot(row.slot)
-      39          if e.registry_key ~= slot.registry_key or e.slot_type ~
-          = slot.slot_type or e.slot_index ~= slot.slot_index then return
-           end
-      40          slot:set_cinematic_active{active = false}
-      44 -        if movies[index + 1] then select_movie(c, index + 1)   
-      41 +        if movies[index + 1] then play(c, index + 1)           
-      42          else
-      43              c:set_variable("ember.ending", index + 1)
-      44              c:set_variable("ember.complete", true)
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Only Lua changed this round, so the installed DLL is already correct.
-  Committing and archiving the run:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Committed and ready to install — close the game and I'll push it out (Lua only
-  this time; the DLL from 12:47 is already correct).
-
-  What the log showed
-
-  The freeze fix worked. state_region result=teleport_not_required, no bogus
-  slice-set transition, no network_send hitch. But the ending still didn't play,
-  and the log says exactly why: no cinematic_enqueued was ever emitted. The
-  only cinematic events in the entire run are the opening one at t=54s.
-
-  The cause is my own design, and it's a consequence of the fix: I was
-  activating the bookend on the next client state-change callback. Removing the
-  teleport also removed the thing that produces a client state-change — your
-  last region report was t=379518, and the selection was t=432565, with nothing
-  after. So the activation was waiting on an event that no longer exists. The
-  client sat in region 0 with the mission state pointing at region 1, ticking
-  normally — which is the "infinite load" you saw.
-
-  The bookend is now queued together with its state selection. Intent ordering
-  still guarantees the cinematic Auth lands after the seed publishes, and slot
-  handles resolve against the static SDK table rather than the selected state,
-  so naming the bookend early is safe.
-
-  The beam
-
-  You're right that it stayed on. I've now confirmed why: both ring devices are
-  ordinary type-23s — their configs carry the same class refs as a clamshell
-  door — and neither position nor power changes anything visible. Object 
-  presence is the only lever that moves this beam.
-
-  Comparing the three models settles which is which: core and ring carry solid
-  geometry with full LOD tables; the laser carries the emitter's effect
-  components and no geometry LODs. So core + ring are the structure, laser is
-  the beam. They're now split — structure placed once and never removed, laser
-  switched off at the deposit. That gives you the installed-but-dark state you
-  asked for, and the exposure cycle no longer toggles it (presence-switching is
-  what read as blinking).
-
-  I have not implemented the surge, and I'm not going to guess at it a third 
-  time. I've now shown that neither device lane does anything and that presence
-  is binary, so nothing available to the script produces an intensity change.
-  Two failed attempts is enough — the honest position is that the authored
-  mechanism is unidentified.
-
-  If you want it chased properly, the concrete next step is the one that cracked
-  the Harvester doors: dig into the laser model 80B7117C and the ring 80B71218
-  for a named channel or model controller, then drive it the way dropship_doors
-  was driven. That's a real investigation rather than another swap, so I'd
-  rather do it deliberately than bolt a third guess onto this build.
-
-  What to check next run
-
-  The beam should be lit through the reactor fight, then visibly dark but still
-  physically there after you dunk the cell — with the ring structure intact. And
-  the log line that matters for the ending is cinematic_enqueued shortly after
-  state_selected; if that appears and the movie still doesn't render, the
-  problem has moved from the script to playback, which is a different hunt. The
-  climb scorch is also still untested since you didn't get that far last time.
-
-✻ Baked for 5m 20s
-
-❯ We're treating the object, we're not treating the device linked to it for the 
-  beam. We have specops_apex_ring.laser_device and                              
-  specops_apex_ring.ring_device could that be the issue?                        
-
-● Good question — and it's testable. Let me check whether those devices ever
-  accepted the transitions I've been sending.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● They're structurally identical to the clamshell/bridge devices that do animate
-  — so they should respond. Let me look at exactly what a transition sends.
-  Searched for 2 patterns (ctrl+o to expand)
-
-● Let me check whether device state is even reported back — that determines
-  whether my "devices are inert" conclusion is sound.
-
-  Searched for 2 patterns, ran 1 shell command (ctrl+o to expand)
-
-● The device-sense diagnostic is hard-scoped to the landing bridge only — so I
-  have no observation of apex device state. Let me check its storage before
-  widening it.
-
-  Searched for 1 pattern (ctrl+o to expand)
-
-● The diagnostic only ever watched the landing bridge — so I genuinely cannot
-  tell whether apex device transitions are applied. Widening it, and driving the
-  devices properly:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now driving both device lanes properly, with the object lever guaranteeing the
-  end state:
-
-  Ran 7 shell commands (ctrl+o to expand)
-
-● You're right to push on that, and checking it turned up something that
-  undercuts what I told you.
-
-  Your instinct was justified — my "devices don't work" claim wasn't supported
-
-  I was driving both devices, but I'd concluded from "the beam didn't change"
-  that the device lanes are inert. That conclusion doesn't hold, because nothing
-  was ever observing those devices. The device-sense diagnostic is hard-scoped
-  to registry F6FFB59E, slots 0–5 — the landing bridge. The apex ring devices
-  (a3b76c64, slots 48/49) were never reported, and there were zero device_state
-  lines in the whole run.
-
-  So "the device didn't move the beam" and "the transition never reached the
-  device" look identical in the evidence I had. I shouldn't have stated the
-  first as established. The devices are also structurally identical to the
-  clamshell and bridge devices that visibly animate — same sense schema, same
-  auth schema, same component class, same descriptor offset — so there's no
-  structural reason they'd be inert.
-
-  What I changed
-
-  Both halves are now driven together, the way the Mercury lever pairs its
-  type-23 device with the type-4 object it animates:
-
-  - Structure (core + ring objects) — placed once, never removed, so the framing
-    around the beam always exists.
-  - Beam (laser object) — presence is the lever already proven to make the beam
-    appear and vanish; off at the deposit.
-  - Devices — snap-initialised to a baseline, then transitioned without snap,
-    which is exactly the pattern that made the lever animate rather than jump.
-    The warning drives them open as the surge; recovery returns them; the
-    deposit powers them off and returns the drive to baseline.
-
-  The surge no longer removes anything, so if the devices do move the emitter,
-  that's what you'll see — and if they don't, the beam is still stable and
-  correctly dark at the end.
-
-  I also widened the diagnostic so the next run answers your question directly.
-  Look for ring_device_sense lines in the log: they carry the ring devices'
-  native position and power. If they appear and change at the warning, the
-  devices are live and the surge is a matter of which lane; if they never
-  appear, the transitions aren't landing and that's the real bug.
-
-  One thing worth flagging: the transition vocabulary is closed — position,
-  power, lock, each with just a high/low endpoint. There's no intensity channel.
-  So if the devices are live and open/close still doesn't read as a surge, the
-  answer is likely a named channel on the model itself, the way dropship_doors
-  was found on the Harvester — not another lane in this API.
-
-  Build and tests are green (22 native, 5 Lua suites; 234/512 variables, 61/63
-  intents). Committed as 53c84bb. The game's still running — close it and I'll
-  install; this one does include a DLL change for the diagnostic.
-
-✻ Cogitated for 4m 36s
-
-❯ see because the sdk showed that the devices were not actually active and had  
-  no place in the world. Install now please.                                    
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Installed. 18 files, all hash-verified; DLL dc668f42…. Backup at
-  build/final-issues-install-backup-20260906-131220. Save, settings and SDK
-  untouched.
-
-  And your reading fits the evidence better than mine did. The type-4 objects
-  carry package transforms and are explicitly placed by set_object_active —
-  that's why toggling them visibly changed the beam. The type-23 devices are
-  declarations that were never given a world placement, so the transitions I
-  kept sending landed nowhere. That explains the whole pattern cleanly: presence
-  of the objects was the only thing that ever moved, and no amount of position
-  or power on the devices was going to do anything.
-
-  That's also the real reason my earlier "the devices are inert" line was
-  unsupported — I'd reached the right conclusion by the wrong route, from
-  "nothing changed visually" rather than from the placement data, which is why I
-  then talked myself back out of it when you pushed.
-
-  The build as installed is still correct under that reading: the object lever
-  is what produces the behaviour (structure always placed, laser off at the
-  deposit), and the device drive is a few spare intents that do nothing. The
-  ring_device_sense diagnostic will confirm it outright — if those devices have
-  no placed instance, you'll see no such lines at all, and that settles the
-  surge question: it isn't reachable through the device API, and the next place
-  to look is a named channel on the laser model itself, the way dropship_doors
-  was found.
-
-  What this run should show: beam lit through the reactor fight and visibly dark
-  but still physically present after you dunk the cell; the climb pipes burning
-  on the way up (still untested — you haven't reached it with a working build
-  yet); and for the ending, cinematic_enqueued shortly after state_selected,
-  since the bookend is now queued with its own selection rather than waiting on
-  a client report that no longer arrives.
-
-✻ Cogitated for 36s
-
-※ recap: We're finishing 1AU's remaining bugs; the latest build is installed and ready for you to launch and test. Next: run the mission and report the beam, climb pipes, and ending cutscenes. (disable recaps in /config)
-
-❯ Loading in towards the laser beam now                                         
-
-● Watching. Let me set up a read-only wait on the log for the markers that
-  matter.
-
-● Monitor(1AU apex: ring device state, cinematic and ending markers)
-  ⎿  Monitor started · task b08pmwbbk · persistent
-
-● Watching the log live. I'm filtering for:
-
-  - ring_device_sense — the decisive one for your point. If those devices have
-    no world placement, nothing will appear here at all, and that closes out the
-    beam question: it isn't reachable through the device API, and the surge
-    would have to come from a named channel on the laser model itself (the
-    dropship_doors route).
-  - cinematic — I want cinematic_enqueued shortly after state_selected, then
-    started. That's the ending fix.
-  - state_region — should say teleport_not_required.
-  - Failures — seed refusals, refused intents, faults, errors, hitches, and any
-    slice-set transition.
-
-  Things worth your eye as you go through apex, since the log won't show them:
-
-  - The beam lit through the reactor fight, and whether the warning window reads
-    as any kind of change.
-  - After you dunk the cell: beam dark, but the ring structure still physically
-    there.
-  - The climb pipes burning on the way up with the cell — that path has never
-    actually been reached with a working hazard, so it's untested.
-
-  I'll flag anything that lands.
-
-✻ Baked for 27s
-
-● Monitor event: "1AU apex: ring device state, cinematic and ending markers"
-
-● The devices are live and reporting — and ordinal 0 is changing rapidly. Let me
-  decode the full values.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Monitor event: "1AU apex: ring device state, cinematic and ending markers"
-
-● Monitor event: "1AU apex: ring device state, cinematic and ending markers"
-
-● Monitor event: "1AU apex: ring device state, cinematic and ending markers"
-● Monitor event: "1AU apex: ring device state, cinematic and ending markers"
-
-● Decisive data — and it contradicts the SDK reading. Let me quiet the noise and
-  decode it.
-
-● Stop Task
-  ⎿  tail -F -n 0
-     /home/millie/Games/Sunrise/bin/x64/Sunrise/logs/sunrise.log
-     2>/dev/null | grep -E --line-buffered
-     "ring_device_sense|state_region|cinematic|mission… · stopped
-
-● Monitor event: "1AU apex: ring device state, cinematic and ending markers"
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Monitor(1AU ending markers, faults and ring device sequence lanes)
-  ⎿  Monitor started · task bry6ytn6i · persistent
-
-❯ yeah so the devices aren't tied to the object, that's what needs to change    
-
-● Understood — the devices animate but nothing binds them to the placed objects.
-  Let me look at what the object Auth can carry.
-
-  Searched for 1 pattern (ctrl+o to expand)
-
-● There's a with parameter and a general set_channel. Let me read both.
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-
-● Your refinement matches the data — and the first half of the premise doesn't.
-  Here's what the run just proved.
-
-  The ring devices are live, placed, and animating
-
-  ring_device_sense fired 226 times for slots 48 and 49. Decoding the lanes:
-
-  - ordinal 0 = position, a float that ramps smoothly: slot 48 went 0.0023 →
-    0.0046 → 0.0069 → 0.0093 → 0.0116 → 0.0139 → 0.0162…, slot 49 climbed ~30×
-    faster (0.069 → 0.139 → 0.208). Both are travelling 0→1 in response to the
-    transitions I send.
-  - ordinal 1 / 3 / 5 = sequence guards, moving −1 → 1 → 2 as each channel
-    accepts a command.
-  - ordinal 2 = 1.0f, the lane's target.
-
-  So the transitions land, the devices exist in the world, and they physically
-  animate. They aren't inert declarations. But the placed object doesn't follow
-  them — which is exactly your point, and it's the only reading left once you
-  can see the device moving and the beam not changing.
-
-  What the API actually offers
-
-  The type-23 vocabulary is three channels — position, power, lock — and
-  transition only ever sends the two endpoints of a lane. There's a second entry
-  point I haven't used, set_channel, which takes a continuous unit scalar plus
-  a sequence and snap. That's a real intensity lever if the binding is fixed:
-  you could drive position to 0.35 rather than slam it to 1.
-
-  Also worth noting for the binding hunt: set_object_active hardcodes entryIndex
-  = 0. If the beam's placed entry isn't entry 0, we'd be instantiating a
-  different entry from the one the device drives — which would produce exactly
-  this split.
-
-  What I'd do next
-
-  Chase the object↔device binding in the package: the three object configs
-  (80B3D4E2/E5/E8) and the two device configs (80B3D4EB/EE) all sit in the same
-  prefab, so the link should be declared there — the same way the Harvester's
-  delivery component was found bound to actor template 80FE22C9 at a specific
-  ordinal. That tells us whether the device targets a different entry index, or
-  whether the object needs something published before it will accept the
-  device's drive.
-
-  I'm not going to change anything mid-run. Finish the playthrough — I still
-  want to know whether the climb pipes scorch and whether the ending reaches
-  cinematic_enqueued, since neither has ever been observed working. The
-  monitor's still watching for those (I've filtered out the position spam). Then
-  I'll do the binding investigation properly rather than bolt on another guess.
-
-✻ Worked for 2m 0s
-
-❯ core_weapon_laser_beam_device , cinder_core_weapon_laser_beam_device,         
-  specops_apex_ring.laser_device are not enabled though                         
-
-● Let me check what the scripts actually do with that whole laser-beam family.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● You're right, and the grep proves it. The objects are placed but their devices
-  are never enabled:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Confirmed — the weapon's beam device is enabled only in apex. Powerhouse,
-  processing and cinder place the beam object but never drive its device. Let me
-  see the activation sites.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now enabling the weapon's beam device in every region, matching what apex
-  already does:
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now the powerhouse one, in the confirmed-working landing module:
-
-  Ran 4 shell commands (ctrl+o to expand)
-
-● The slot exists in the real SDK; the controller test uses a hand-built mock
-  that lacks it. Adding it:
-
-  Searched for 2 patterns, ran 6 shell commands (ctrl+o to expand)
-❯ and the cutscenes still don't load following the end-of-mission fade-to-black 
-  screen                                                                        
-
-● Let me check what the ending did in this run.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● state_selected fired at t=531300 but no cinematic_enqueued followed — same
-  failure. Let me first rule out a stale install.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The new script is live and queues both. Let me look at what
-  set_cinematic_active actually requires.
-
-  Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
-
-● Found the actual failure — and it's new information.
-
-  Searched for 2 patterns (ctrl+o to expand)
-
-● Both threads have answers now — one good, one a genuine bind.
-
-  The beam device gap is real, and it's fixed
-
-  Your reading was right, and the grep proves it. The weapon's beam is an object
-  and a device, and only apex ever enabled the device:
-
-  ┌────────────┬──────────────────────────────────────────┬─────────────────┐
-  │   region   │                  device                  │ what the script │
-  │            │                                          │       did       │
-  ├────────────┼──────────────────────────────────────────┼─────────────────┤
-  │ 64         │                                          │ never           │
-  │ powerhouse │ POWERHOUSE_CORE_WEAPON_LASER_BEAM_DEVICE │ referenced at   │
-  │            │                                          │ all             │
-  ├────────────┼──────────────────────────────────────────┼─────────────────┤
-  │ 56         │                                          │ object placed,  │
-  │ processing │ CORE_WEAPON_LASER_BEAM_DEVICE            │ device          │
-  │            │                                          │ untouched       │
-  ├────────────┼──────────────────────────────────────────┼─────────────────┤
-  │            │                                          │ object placed,  │
-  │ 40 cinder  │ CINDER_CORE_WEAPON_LASER_BEAM_DEVICE     │ device          │
-  │            │                                          │ untouched       │
-  ├────────────┼──────────────────────────────────────────┼─────────────────┤
-  │ 0 apex     │ SPECOPS_APEX_RING_*_DEVICE               │ unlocked and    │
-  │            │                                          │ powered         │
-  └────────────┴──────────────────────────────────────────┴─────────────────┘
-
-  Each region now unlocks and powers its beam device beside the object. Position
-  is only one of a type-23's three lanes — a device that's never unlocked and
-  powered stays inert no matter how its position is driven, which fits the apex
-  ring animating its position while the beam ignored it. Committed as 2285d58;
-  22 native tests and all five Lua suites pass. Not installed — the game's still
-  running.
-
-  The ending: I found the actual failure, and it's a real bind
-
-  It isn't silence this time. At t=531300 the state was selected
-  (teleport_not_required), and then at t=591315:
-
-  warn ev=mission_script stage=intent_refused result=intent_timeout
-       error="intent exceeded its delivery lifetime"
-
-  Exactly 60 seconds. The set_cinematic_active intent was queued — my change
-  worked — but it never reached the host. set_cinematic_slot_active_reserved
-  calls current_behavior_occurrence to resolve the bookend slot's live
-  occurrence, that returns "mission seed still publishing", so the intent
-  retries until its lifetime expires. That's why there's no cinematic_enqueued.
-
-  The reason it can't resolve is the bind:
-
-  - The occurrence resolver wants the slot's live region. The client is standing
-    in region 0, so region 1's cinematic has no live occurrence — the seed
-    publishes its groups but the client never instantiates them.
-  - The only mechanism that moves a client's region is the host teleport. Arming
-    it for a sibling state is what caused the original freeze, because it hands
-    the client slice-set index 1 carrying slice set 0's name hash (PRV01.01 
-    (HASH: 0xdfbc1cd9)), and it never finishes synchronizing.
-
-  So: no teleport → content never instantiates. Teleport as currently
-  parameterised → hangs. Both ends of that are now demonstrated rather than
-  assumed, which is progress, but I don't yet know the third path — how an
-  intra-slice-set region change is meant to be signalled to the client.
-
-  I'm not going to guess at it again. Three attempts on this one is enough, and
-  each guess has cost you a playthrough. The next step is to read how
-  current_behavior_occurrence decides a live occurrence and what the membership
-  region record needs to say for region 1 to count as live — the answer is in
-  that path, not in the Lua. That's a proper sit-down investigation rather than
-  a swap.
-
-  Say the word when you've closed the game and I'll install the beam-device fix,
-  which is independent of all this and worth having in front of you.
-
-✻ Cogitated for 5m 13s
-
-❯ for the ending cinematics just trigger them the same way we trigger the       
-  opening cinematic surely                                                      
-
-● Let me look at exactly how the opening one is driven, since that's the path
-  that demonstrably works.
-
-  Searched for 1 pattern, read 1 file (ctrl+o to expand)
-
-● effectiveRegion is the selected region, so the resolver should find it. The
-  gate before it is scene_binding_status — let me read that.
-
-  Searched for 2 patterns, ran 2 shell commands (ctrl+o to expand)
-
-● report_intent_status logs at debug — which is why the 60 seconds looked
-  silent. Same blind spot as the seed refusal. Raising it:
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● I tried exactly that, and it's already what the current build does — the
-  reason it still fails isn't the trigger, it's a gate underneath. But chasing
-  your suggestion is what found the answer.
-
-  Why the opening's path doesn't transfer
-
-  The opening does two things: it makes region 49 the mission's initial_state,
-  so the client loads into that slice set as part of mission load, and then it
-  waits for a held-region report before calling set_cinematic_active.
-
-  The second half is transferable; the first isn't. Region 49 is its own slice
-  set (48) with its own bubble hash, so the client can actually be in it.
-  Regions 1 and 2 are sibling states of slice set 0 — they share bubble ordinal
-  0 with apex gameplay, so bubbleHashes[0] resolves to PRV00.00. That's why the
-  teleport produced PRV01.01 (HASH: 0xdfbc1cd9) — an index saying region 1 with
-  a hash naming region 0. The client can't teleport there because region 1 isn't
-  a slice set. So there is no "hold region 1" report to wait for, which is why
-  the held-region gate I originally wrote never fired.
-
-  What's actually blocking it
-
-  The current build already triggers it the opening's way, minus the wait. The
-  intent is queued correctly — that part works. It just never reaches the host:
-
-  set_cinematic_slot_active_reserved → current_behavior_occurrence →
-  scene_lease_status, and that returns a still-publishing lease, so the
-  dispatcher retries rather than delivering. It retried for 60 seconds and
-  expired. That's the intent_timeout at t=591315, 60,015 ms after
-  state_selected.
-
-  Encouragingly, the occurrence resolver uses link.effectiveRegion, which is the
-  selected region, not the held one — so it should find region 1's bookend
-  without the client being there. The blocker is one layer up, in the seed
-  lease: either publicationPending or outputBusy.
-
-  The one thing I changed
-
-  I'm not guessing which. report_intent_status was logging the reason at debug —
-  the same blind spot as the seed refusal earlier — which is why those 60
-  seconds looked completely silent. It's now at info, deduped to one line per
-  distinct status.
-
-  So the next run prints the exact status in a loop between state_selected and
-  the timeout. That converts the last unknown into a fact, and the status name
-  tells us directly whether the roster is staged and never flushing, or the
-  lease revision never publishes for a sibling state.
-
-  Two commits waiting to install, both independent of this: 2285d58 (the beam
-  device enabling you identified) and 8dbf417 (this diagnostic). Close the game
-  and I'll put them in — the beam change is worth seeing on its own, and the
-  ending run will finally say why it's stalling.
-
-✻ Baked for 2m 11s
-
-❯ okay, install.                                                                
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Monitor(1AU: intent wait reasons, cinematic and ending markers)
-  ⎿  Monitor started · task b8wzizkyt · persistent
-
-● Installed and verified. 18 files hash-checked, DLL ba1d1ac7…, backup at
-  build/final-issues-install-backup-20260906-133119. Save, settings and SDK
-  untouched. I archived the run I just analysed as
-  20260906-1310-intent-timeout.log so the evidence survives.
-
-  Two things in this build:
-
-  The beam device enabling you identified — all four regions now unlock and
-  power their weapon beam device beside the object, not just apex. Powerhouse
-  had never referenced its device at all; processing and cinder placed the
-  object and left the device dead.
-
-  Visible intent wait reasons — the thing that made the last 60 seconds look
-  silent. report_intent_status was at debug.
-
-  What to look for
-
-  For the beam: whether it now reads as an actual firing beam in the earlier
-  regions rather than a static prop, and in apex whether it's dark but still
-  physically present after you dunk the cell.
-
-  For the ending, the log is now the interesting part. Between state_selected
-  and any timeout you should get a line like:
-
-  ev=mission_script stage=intent result=<status>
-
-  That status name is the answer I've been guessing at — most likely
-  mission_seed_pending or output_busy. Whichever it is tells us directly whether
-  the roster is staged and never flushing, or the lease revision never
-  publishes for a sibling state. Either way it's a fact rather than an
-  inference, and it points at one specific place in the seed publication path.
-
-  I'm watching the log for it. One caveat worth setting: I don't expect the
-  ending to work this run — nothing about the blocker itself changed, only its
-  visibility. If the cutscenes still don't play, that's expected, and the log
-  line is what I need from the attempt.
-✻ Brewed for 29s
-
-❯ in apex it was dark and physically present after dunking the cell, but the    
-  surge still never happens.                                                    
-
-● Good — that half is confirmed working. For the surge, let me follow up on
-  something I noticed earlier: set_object_active hardcodes entryIndex = 0.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● prepare_object doesn't bound-check the entry — the type-4 body carries an
-  entry index and Lua pins it to 0. Let me see if entries are enumerable.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Let me check the one unexplored lever before saying anything: whether the
-  laser object has more than one authored entry.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Monitor event: "1AU ending markers, faults and ring device sequence lanes"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Stop Task
-  ⎿  tail -F -n 0
-     /home/millie/Games/Sunrise/bin/x64/Sunrise/logs/sunrise.log
-     2>/dev/null | grep -E --line-buffered
-     "state_region|cinematic|mission_seed_refusal|resu… · stopped
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU ending markers, faults and ring device sequence lanes"
-
-● The info-level change made my filter far too noisy. Tightening it:
-
-● Stop Task
-  ⎿  tail -F -n 0
-     /home/millie/Games/Sunrise/bin/x64/Sunrise/logs/sunrise.log
-     2>/dev/null | grep -E --line-buffered "stage=intent
-     |stage=cinematic|state_region|state… · stopped
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor event: "1AU: intent wait reasons, cinematic and ending markers"
-
-● Monitor(1AU ending: cinematic, state selection, intent waits and refusals)
-  ⎿  Monitor started · task bc3vnhpul · persistent
-
-● Good — that half is now confirmed working: dark and physically present after
-  the deposit is exactly the state you asked for.
-
-  On the surge, I've run out of levers that the script API actually exposes, and
-  I want to be straight about that rather than try a fourth variation.
-
-  Here's the complete picture from this session's evidence:
-
-  - Object presence is the only thing that changes the beam visually. Proven
-    twice — deactivating it made the beam vanish, and it's what produces the
-    dark-but-present state you just confirmed.
-  - The devices are live and genuinely animating. ring_device_sense showed their
-    position lane ramping smoothly 0→1 (slot 48 at ~0.0023/step, slot 49 about
-    30× faster), with sequence guards accepting each command. They move. The
-    beam ignores them.
-  - The device vocabulary is closed: position, power, lock — that's all three
-    lanes, and I've driven all of them.
-
-  So a surge would need the beam to have an intermediate visual state, and
-  nothing reachable from the script can express one. Object presence is binary;
-  the devices move something that isn't the beam's appearance.
-
-  The one thing I found that I haven't tried: the type-4 Auth body carries an
-  entry index, and set_object_active hardcodes it to 0 — prepare_object doesn't
-  even bound-check it. If the laser object has more than one authored entry,
-  selecting a different one would swap the visual, which is precisely how a
-  "same object, brighter variant" gets authored. But I checked the three object
-  configs and they're byte-for-byte structurally identical to each other, with
-  no obvious per-object entry list — so I can't claim they have multiple
-  entries, only that the lever exists and is currently pinned shut.
-
-  Confirming that means reading the container placement tables for object
-  80B3C21C to count entries per object. The reader for that format already
-  exists in the codebase (container_placement_reader.cpp), so it's a real piece
-  of work but not a fishing expedition. That's the honest next step, and it's
-  the only one I'd put money on.
-
-  Two options, your call:
-
-  1. Chase the entry index properly — extract the placement table, count entries
-     on the laser object, and if there's more than one, expose entry on
-     set_object_active and drive it as the surge.
-  2. Bank the beam as-is — it fires through the mission, it's correctly dark and
-     present after the deposit, and the surge stays unimplemented while I put
-     the remaining effort into the ending, which is still the thing blocking a
-     complete playthrough.
-
-  I'd lean toward (2) first, because the ending is the one outstanding item that
-  stops the mission finishing at all, and I now have live logging that should
-  name its blocker on this run. Worth noting: I saw some trigger_refused /
-  slot_auth_refused with target_unavailable early in region 64 on this run —
-  I'll check whether those are pre-existing or new once you're further in.
-
-✻ Brewed for 2m 6s
-
-❯ in the SDK specops_apex_ring.*_device are both still listed as "This row has  
-  no place in the world"                                                        
-● That's decisive if it's the SDK's own placement check. Let me find what emits
-  that.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● You're right, and I was wrong about that — I should correct it clearly.
-
-  That tooltip fires on anchors.empty() in the scriptable browser: the row has
-  no anchors, no world placement. That's authoritative, and it beats my
-  inference.
-
-  What I misread: I saw ring_device_sense reporting a position lane ramping
-  smoothly 0→1 with sequence guards accepting each command, and concluded the
-  devices were "live and physically animating." That was an over-claim. Sense
-  reflects the device component's channel state — it interpolates the value we
-  command and echoes it back — which happens whether or not anything is placed
-  in the world. A smooth ramp proves the channel is being driven, not that
-  there's an entity to drive. You'd said the SDK showed them unplaced two
-  messages earlier and I talked myself out of it on weaker evidence.
-
-  So the position is now consistent with everything observed: the beam is
-  entirely object-presence driven, the two devices are unplaced declarations,
-  and no amount of position, power or lock on them will ever produce a visual.
-  That's why every device-based attempt failed, and why the only things that
-  ever worked were object activations.
-
-  Which means a surge isn't expressible the way I've been trying — object
-  presence is binary, and the devices are inert by construction.
-
-  There's one lever left, and you can check it in seconds in the browser you 
-  already have open. The type-4 Auth carries an entry index, and
-  set_object_active hardcodes it to 0 — prepare_object doesn't even bound-check
-  it. Entries correspond to placements, which is exactly what that tooltip
-  counts. So:
-
-  ▎ Hover the render checkbox on specops_apex_ring.laser_object, .core_object 
-  ▎ and .ring_object — the objects, not the devices. If a row has places it says
-  ▎ "N of M places ticked".
-
-  If the laser object reports more than one place, that's the surge: a second
-  authored entry for the same object, almost certainly a brighter variant, and I
-  expose entry on set_object_active and drive it at the warning. If they all
-  report exactly one place, then the surge genuinely isn't reachable from the
-  script API, and the only remaining route is driving a named channel on the
-  model itself — the way dropship_doors was found on the Harvester — which is a
-  much bigger piece of work and worth deciding on deliberately rather than
-  drifting into.
-
-  Either way that one tooltip settles it, and it's a fact rather than another
-  inference from me.
-
-✻ Baked for 48s
-
-● Monitor event: "1AU ending: cinematic, state selection, intent waits and
-refusals"
-
-● Stop Task
-  ⎿  tail -F -n 0
-     /home/millie/Games/Sunrise/bin/x64/Sunrise/logs/sunrise.log
-     2>/dev/null | grep -E --line-buffered "stage=cinematic
-     |state_region|state_selected|int… · stopped
-
-● Monitor event: "1AU ending: cinematic, state selection, intent waits and
-refusals"
-
-● Monitor(1AU ending: cinematic, state selection, intent waits and refusals)
-  ⎿  Monitor started · task bo4qph0m3 · persistent
-
-● That was my monitor filter misfiring, not anything from your run — pending was
-  matching pending=0 in the routine probe lines. I've retightened it to only
-  the mission-script cinematic, state-selection, intent-wait and refusal events,
-  so it should stay quiet until the ending actually does something.
-
-  Nothing has changed in my read of the beam. The question from my last message
-  still stands whenever you're back at the browser:
-
-  ▎ On specops_apex_ring.laser_object, .core_object and .ring_object — the 
-  ▎ objects, not the devices — what does the render checkbox tooltip say? Either
-  ▎ "no place in the world" or "N of M places ticked".
-
-  If the laser object has more than one place, that's the surge, and it's a
-  small change: expose entry on set_object_active and drive the second entry at
-  the warning. If they each have exactly one, the surge isn't reachable from the
-  script API at all and we should stop spending playtests on it.
-
-✻ Baked for 22s
-
-❯ I don't see what you're asking me to look at                                  
-
-● Fair — let me get you the actual navigation from the UI code rather than
-  describing it vaguely.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● You were on the Devices page — that's the right place to see the devices are
-  unplaced. I need you on a different page.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Confirmed — the Position column is exactly the count I need, and it's a value
-  you can read straight off the row without hovering anything.
-
-  Where to go: same SDK view, but switch from the Devices page to the Objects
-  page. The devices and objects are listed separately, which is why you couldn't
-  find these — specops_apex_ring.*_device live on Devices (where you correctly
-  saw "no place in the world"), and the three objects live on Objects.
-
-  What to find: three rows —
-
-  - specops_apex_ring.laser_object
-  - specops_apex_ring.core_object
-  - specops_apex_ring.ring_object
-
-  What to read: the Position column (columns are Draw | Name | Bubble | State |
-  Kind | Position | Shape | Link). It shows one of:
-
-  - N linked — N real world places
-  - N context — greyed out, context-only
-  - - — none
-
-  That number is the anchor count, the same thing the tooltip counts, and it's
-  precisely the entry count I'm after.
-
-  What it decides:
-
-  - If laser_object shows more than 1 linked — that's the surge. Multiple
-    authored entries for the same object, and set_object_active is pinning us to
-    entry 0. Small change: expose entry and drive the second one at the
-    warning.
-  - If it shows exactly 1 linked — the surge isn't reachable from the script API
-    at all, and I'd stop spending playtests on it. The only route left would be
-    driving a named channel on the model, which is Harvester-door-sized work
-    and worth deciding on separately.
-
-  Just the three numbers is all I need.
-
-✻ Sautéed for 38s
-
-❯ 1 Linked, Shape none, linx exact same on all fo them                          
-  both the specops_apex_ring.laser_device and specops_apex_ring.ring_device     
-  says state unnamed, position 60 context only, link context only               
-
-● That closes it — all three routes are now ruled out by evidence rather than
-  inference.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● That settles the beam. All three routes are dead, and I've written the finding
-  into MISSION_EMBER_STATUS.md so nobody re-litigates it later.
-
-  1 linked, shape none, link exact — on all three objects. One placement each.
-  There's no second entry, so the entryIndex lever is dead: nothing to switch
-  to.
-
-  state unnamed, 60 context only, link context only on both devices. That's
-  linkedPositions == 0 — the row has no anchor of its own, and the 60 is just
-  the owning object's context count. Exactly the condition behind "this row has
-  no place in the world." So the devices can never produce a visual, and
-  position/power/lock is the entire type-23 vocabulary.
-
-  That also means my ring_device_sense reading was wrong, and I want to name
-  that plainly rather than let it sit in the record: the smooth 0→1 position
-  ramp I treated as "the devices are live and physically animating" was the
-  device component interpolating the value I commanded and echoing it back. It
-  proves the channel is driven, not that anything exists to be driven. You told
-  me the SDK said these were unplaced and I argued back on weaker evidence. You
-  were right both times.
-
-  So: the surge isn't reachable from the script API at all. Not device lanes,
-  not entry selection, not presence toggling. The only route left is driving a
-  named channel on the laser model 80B7117C directly — the way dropship_doors
-  was recovered from the Harvester's model controller and driven through the
-  named-scalar actor command. That's native work on the scale of the Harvester
-  door investigation, not a script change.
-
-  My recommendation: bank the beam and move to the ending. What you have now is
-  correct — it fires through the reactor fight, and it's dark but structurally
-  intact after the deposit, which is what you asked for. The surge is one absent
-  VFX beat. The ending is the thing that stops the mission finishing at all,
-  and it's the only outstanding item of that weight.
-
-  The current build has live logging on exactly that path, so the next time you
-  reach the escape the log should name the blocker instead of going silent for
-  sixty seconds — I'm watching for it. If you'd rather I take on the
-  model-channel work for the surge afterwards, I'm happy to, but I'd want it
-  scoped as its own task rather than folded into the finale.
-
-✻ Cooked for 48s
-
-❯ I'm moving to the ending now, then do the beam immediately after              
-
-● Understood. Monitor's armed for the ending. While you play, let me do the
-  read-only groundwork for the beam so I'm not starting cold — the first thing
-  to establish is whether the laser model even has named channels.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● No named-channel table in the laser components we have — those "strings" are
-  float noise. Let me pull the remaining laser-side resources for a definitive
-  answer.
-  Ran 1 shell command (ctrl+o to expand)
-
-❯ I do think you're wrong about the beam device fwiw, because for every other   
-  device+object combo both are drawn, the beams device isn't drawn at all       
-
-● You're right, and that's a much better read than mine — an authored device
-  that's undrawn while every sibling device+object pair is drawn is an anomaly,
-  not a design. Let me find why those two rows have no placement.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● A type-23 device only gets drawn if its descriptor has a placementLinkRow into
-  type23PlacementLinks. Let me find where those links are built.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Now I have the mechanism. A type-23 device is only drawn when its descriptor's
-  placement identifier resolves to exactly one active container placement — and
-  there are diagnostics for every way that can fail.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● You've changed my mind, and the code backs you up. Let me correct the record.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● You're right and I closed that out too early. Tracing your observation through
-  the code changes the conclusion.
-
-  A type-23 device is only drawn when its descriptor has a valid
-  placementLinkRow and type23_source::build succeeds. That link comes from
-  append_type23_placement_links, which matches the descriptor's
-  placementIdentifier against container placements and only reaches
-  ReferenceJoin::exact when the link is complete and exactly one candidate is
-  active.
-
-  The ring devices pass the builder's filter fine — 80B3D4EB (slot 48) and
-  80B3D4EE (slot 49) both carry component class 0x80804F45, the type-23 class.
-  So they should get a link. Which means the link is being built and failing to
-  resolve.
-
-  And there's a detail that made this look cleaner than it is: link_label tests
-  contextPositions before partial. So a device whose placement link failed still
-  reads "context only" whenever its owning object has context positions — which
-  is exactly your case, 60 of them. The partial flag was being masked. I read
-  "context only" as "no placement authored" when it can equally mean "placement
-  authored, join failed."
-
-  So the beam device isn't inherently unplaceable. Its placement link resolves
-  to zero or ambiguous candidates, and that's why nothing is drawn and why
-  driving it does nothing. That's a defect, and plausibly the same class as the
-  scenario-layout catalogue omission that previously cost 83 squad definitions —
-  a generator problem, not an authoring one.
-
-  The builder already counts every way this fails — zeroIdentityMatches,
-  multipleIdentityMatches, zeroActiveCandidates, multipleActiveCandidates,
-  unreadIdentifiers, droppedLinks — but nothing surfaces those counters
-  anywhere. So the first move when I pick this up is an offline dump of the link
-  state for slots 48/49 of 80b3c21c: identity matches, candidate count, active
-  count, join and complete. That names the failure precisely, and if it's a
-  generator bug it's fixable rather than a dead end.
-
-  I've corrected the status doc — route 1 is reopened, routes 2 and 3 (entry
-  index, presence toggling) stay closed on the evidence, and the model-channel
-  route drops to a fallback only if the link can't be repaired.
-
-  Carry on with the ending; the monitor's watching for the cinematic and
-  intent-wait lines. I'll start on the placement link the moment you're through.
-
-✻ Worked for 2m 50s
-
-❯ Ending is done, we're stuck in the black loading screen.                      
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The new logging named it. Let me confirm the mechanism.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● No re-adoption — so that hypothesis is out. The gate passed at t=756058 and
-  failed 12 ms later. Let me find what moves publishedRevision.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● Now I can see the whole chain. publishedRevision only commits when the roster
-  body is actually staged and sent. The intent gate passed at t=756058 and the
-  identical check failed 12 ms later — so one side moved, and I want the numbers
-  rather than another theory.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Two occurrences — I need the cinematic one. Let me check the available
-  snapshot type first.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● The new logging paid off — your run named the blocker precisely.
-
-  t=756058  state_region result=teleport_not_required
-  t=756058  delivery result=state_selected
-  t=756070  intent result=mission_seed_pending      ← 12 ms later
-  t=816076  intent_refused result=intent_timeout    ← 60 s later
-
-  So the queued set_cinematic_active is fine — it's the seed lease refusing to
-  deliver it. mission_seed_pending means scene_lease_status found
-  publishedRevision != revision.
-
-  Here's what makes that interesting: the dispatcher gate that let
-  state_selected complete tests exactly the same condition. It passed at
-  t=756058 and the identical check failed 12 ms later. So one of those two
-  numbers moved in between, and the status name alone can't say which. I also
-  ruled out my first theory — there's no mission_seed result=adopted anywhere in
-  the log, so the lease wasn't re-adopted and wiped.
-
-  I've traced the plumbing: publishedRevision only commits to revision inside
-  the roster push, when a body is actually staged carrying that revision
-  (hasMissionSeedRevision), which itself requires regionArrivalPending to be
-  false — that part my earlier fix already satisfies. So either the revision got
-  bumped again by a second selection, or a staged publication was rolled back.
-
-  Rather than guess a fourth time, the pending report now carries the lease
-  itself: configured, revision, publishedRevision, publicationPending,
-  regionArrivalPending, the link's effective region, and the plan's region and
-  state row. report_intent_status dedups on status, so it's one line per stall,
-  not per retry. That single line will say which number moved and settle it.
-
-  Committed as e281668; 22 native tests and five Lua suites pass; your run
-  archived as 20260906-1340-mission-seed-pending.log.
-
-  I know this is another "install and run it again," and I'd rather not keep
-  spending your playthroughs. Two things worth saying about that:
-
-  You don't need a full run to get this line — it only needs a state selection
-  to stall, so if you can get to the escape by any means (you've been using
-  teleport), that's enough. And it's an in-place install: DLL plus scripts, no
-  save impact, so if the game's still up from this run you can close, let me
-  install, and go straight back to the reactor on a checkpoint rather than
-  replaying the mission.
-
-  Say when it's closed. Meanwhile I'll start the beam placement-link dump, since
-  that's offline work I can do without you.
-
-✻ Baked for 3m 2s
-
-❯ Fix the beam, fix ending cutscene. Don't talk to me until it's done           
-
-● Working. Starting with the ending — the arrival window is the one path that
-  produces exactly both observed symptoms.
-  Ran 5 shell commands (ctrl+o to expand)
-
-● Ending fix is in and tested. Now the beam placement link — let me find the
-  offline SDK generation path so I can read the diagnostics without another
-  playtest.
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● The catalog is built live in the client, so the link diagnostics only exist at
-  runtime. Adding bounded logging for unresolved type-23 links:
-
-  Ran 4 shell commands (ctrl+o to expand)
-
-● Done. Installed and verified — 18 files, DLL hash matches the build, backup at
-  build/final-issues-install-backup-20260906-135458, save/settings/SDK
-  untouched.
-
-  Ending cutscene — fixed
-
-  Your run's logging gave me the cause, and it explains both symptoms at once.
-
-  The seed lease for the ending state never published, so every scene lease on
-  it returned mission_seed_pending and the queued set_cinematic_active expired
-  after 60 seconds. The culprit is the region-arrival window: the roster refuses
-  to commit a published revision while regionArrivalPending is set, and that
-  window only closed when the client reported holding the pending region. A
-  sibling state never produces that report — its content is in the slice set the
-  client already holds, and the current region leg only advances on a slice-set
-  switch. So the window stayed open forever.
-
-  That also explains the thing that had been bothering me: state_selected
-  completed instantly instead of waiting for publication. Its gate is skipped
-  entirely while the arrival window is open. The selection looked healthy
-  precisely because the lease was stuck.
-
-  The window now also closes when the pending region is a sibling of the slice
-  set the client already holds — the same reasoning that removed the bogus
-  teleport. Extracted as mission_seed_arrival_window_closed with portable
-  coverage, including that a genuine slice-set change still holds the window
-  open so a publication can't race a teardown.
-
-  Beam — instrumented, not fixed, and I won't claim otherwise
-
-  I could not fix this one, and here's exactly where it stands.
-
-  Your read was right: the apex ring devices resolve to no exact placement,
-  which is why they're never drawn and why driving them does nothing. What I
-  couldn't determine offline is why the placement link fails — and that decides
-  whether it's repairable. The three possibilities are a placement identifier
-  that was never read, one that matches no container placement, or one that
-  matches ambiguously. The first and third are generator defects and fixable;
-  the second means the content genuinely has no placement and the surge isn't
-  reachable at all.
-
-  The builder already counts all of these, but nothing ever surfaced them — and
-  the browser actively hid it, since a failed join reads as "context only"
-  whenever the owning object has context positions. The link pass now reports
-  its counters and names each unresolved type-23 row with its identity-match,
-  candidate and active counts.
-
-  The catalog is built live in the client, so that only appears when you load
-  the mission. On your next run, ev=type23_placement stage=unresolved slot=48
-  (and 49) will name the failure, and I can act on it directly rather than
-  guessing.
-
-  The beam otherwise remains in the state you confirmed: firing through the
-  reactor fight, dark but physically present after the deposit.
-
-  Both changes are in commit bd4a0cc; 22 native tests and all five Lua suites
-  pass.
-
-✻ Sautéed for 5m 5s
-
-❯ Loading into 1AU now, go ahead and check the beam so you can fix it           
-
-● Monitor(1AU: type-23 placement link results and ending markers)
-  ⎿  Monitor started · task b5d6eejzz · persistent
-
-● Watching. The type23_placement lines are emitted when the scenario catalog
-  builds, so they should land as 1AU loads — before you even reach the reactor.
-
-  Two things I'm looking for:
-
-  ev=type23_placement stage=summary — the counters across all type-23 devices:
-  how many links, and totals for unread, zero_identity, multi_identity,
-  zero_active, multi_active, scenario_resolved.
-
-  ev=type23_placement stage=unresolved slot=48 and slot=49 — the ring laser and
-  ring devices specifically, each with its placement identifier, identity-match
-  count, candidate count, active count, complete flag and join.
-
-  That combination tells me which of three things is happening:
-
-  - identity_matches=0 → the descriptor's placement identifier matches no
-    container placement. Either the identifier is being read wrong, or the
-    content genuinely has no placement for these devices.
-  - unread counted → the identifier was never read out of the descriptor at all,
-    which is a reader defect and fixable.
-  - identity_matches>1 with active 0 or >1 → it matches, but candidate selection
-    can't pick one. That's an ownership/bubble filter problem, and also
-    fixable.
-
-  The first two are generator-side and I can act on them directly. Only "matches
-  nothing and the identifier is correct" would mean the devices are genuinely
-  unplaced in the authored content.
-
-  Since you're doing a full run anyway, the ending fix gets tested on the same
-  pass — if the arrival-window change worked, set_cinematic_active should reach
-  the host instead of stalling on mission_seed_pending, and you'll get the
-  bookend movies rather than the black screen.
-
-  I'll dig in as soon as the lines appear.
-
-✻ Baked for 21s
-
-● Monitor event: "1AU ending: cinematic, state selection, intent waits and
-refusals"
-
-● Stop Task
-  ⎿  tail -F -n 0
-     /home/millie/Games/Sunrise/bin/x64/Sunrise/logs/sunrise.log
-     2>/dev/null | grep -E --line-buffered "ev=mission_script
-     stage=cinematic |ev=mission_sc… · stopped
-
-● Monitor event: "1AU ending: cinematic, state selection, intent waits and
-refusals"
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● No type23_placement lines yet — the catalog hasn't been built this session.
-  Let me confirm what triggers it.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The diagnostic is in, but it hasn't fired — and that's my oversight, not a
-  problem with your run.
-
-  The scriptable catalog is built on demand by the UI, not at mission load.
-  append_type23_placement_links only runs when something asks for that catalog —
-  which is why you saw those rows earlier only because you had the browser
-  open. There's nothing in the log this session because nothing has requested
-  it.
-
-  So one small thing from you, whenever convenient during this run:
-
-  ▎ Open the SDK scriptable browser and go to the Devices page (the same place 
-  ▎ you read "no place in the world").
-
-  That builds the catalog and emits the diagnostic immediately. I'm watching for
-  it. You don't need to do anything else there — just landing on the page is
-  enough.
-
-  What I'll get from it:
-
-  - ev=type23_placement stage=summary — totals across every type-23 device:
-    unread, zero_identity, multi_identity, zero_active, multi_active,
-    scenario_resolved.
-  - ev=type23_placement stage=unresolved slot=48 / slot=49 — the ring devices
-    with their placement identifier and match counts.
-
-  If zero_identity is high across the board, it's a reader-side defect and
-  fixable. If it's only these two rows, then the content genuinely has no
-  placement for them and the surge isn't reachable — I'd stop there rather than
-  keep digging.
-
-  One other thing I noticed in the meantime, unrelated but worth flagging: there
-  are six device_refused with target_unavailable around t=118956 in region 64,
-  plus some trigger_refused. Those are new to this build — the previous run had
-  trigger and slot_auth refusals there but no device ones. That timing lines up
-  with the landing initialize, so it may be the powerhouse beam device I added
-  rejecting its transitions. I'll confirm against the placement data rather than
-  guess, since if that device is also unplaced it'd refuse exactly like this —
-  and that would be the same root cause showing up in a second place.
-
-✻ Cooked for 56s
-
-❯ I've opened the SDK menu for you                                              
-
-  Ran 2 shell commands (ctrl+o to expand)
-
-● The worker only logs at debug, so I can't tell if it ran. Rather than another
-  round trip, let me read the identifier out of the package myself.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The identifier is 8 bytes at descriptor offset 0x58. I have all these configs
-  extracted — let me read them directly.
-  Ran 1 shell command (ctrl+o to expand)
-
-● The ring devices do have valid, non-zero placement identifiers — so the
-  descriptor side is fine. The failure must be that nothing matches them. Let me
-  test that directly.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Now I have it. The working devices' identifiers live in a placement table; the
-  ring devices' live in their paired object's config. Let me confirm the
-  structure.
-
-  Ran 1 shell command (ctrl+o to expand)
-  ⎿  Interrupted · What should Claude do instead?
-
-❯ Good news, the beam works but it's backwards. the default is surging and      
-  surging is the normal                                                         
-
-● Beam's alive — and inverted polarity is exactly the pattern the bridges have.
-  Fixing it.
-
-  Ran 3 shell commands (ctrl+o to expand)
-
-● Fixed and committed — waiting on you to close the game to install (it's
-  Lua-only, so a checkpoint restart at the reactor will do; no full replay
-  needed).
-
-  The polarity fix: these devices carry the same inverted pose as the landing
-  and clamshell bridges, where the authored position-0 lane is the driven end
-  rather than the resting one. I had it the other way round. Resting is now open
-  and the surge drives to close. I replaced beam_drive with beam_pose, which
-  takes whether the weapon is surging rather than which endpoint to send, so the
-  mapping is stated in one place instead of being re-derived at each call site.
-  The dark pose after the deposit also rests in open now rather than sitting in
-  the surge.
-
-  Worth recording, since it overturns two things I told you:
-
-  The devices do drive the beam. I'd argued from linkedPositions == 0 that they
-  were unplaced and inert. They're not — enabling them properly was the missing
-  piece, and the surge you're seeing is those devices working. My "closed out as
-  not reachable" call was wrong, and your original instinct — that the device
-  is the thing to treat — was right from the start.
-
-  The placement data explains why they looked unplaced. I got a decisive answer
-  offline while you were loading: the ring devices have valid non-zero placement
-  identifiers, but those identifiers appear in their paired object's config
-  (80B3D4E2 for the laser, 80B3D4E8 for the ring), not in the container
-  placement table 80B3D356 where every working device's identifier lives. The
-  SDK's link builder only searches container placements, so it finds no match,
-  produces no link, and the browser draws nothing — while the device itself is
-  perfectly real at runtime.
-
-  So "no place in the world" was a limitation of the link builder, not a fact
-  about the device. That's a genuine generator defect worth fixing so the
-  browser stops lying about these rows, but it's cosmetic now that the beam
-  works — I'd treat it as separate cleanup rather than hold anything up for it.
-
-  The ending fix from earlier is in the same pending install, so once you're
-  back in, that run tests both: the surge reading correctly, and whether the
-  arrival-window change lets the bookend movies play instead of stalling on
-  mission_seed_pending.
-
-✻ Brewed for 1m 9s
-
-❯ the escape ship needs to follow its authored path, there should be a screen   
-  effect during the surge, and the cutscene didn't fire, it began to fade and   
-  then kept me in the mission.                                                  
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● The lease detail gives the answer: arrival=1. My fix was in the right place
-  but unreachable.
-
-  Ran 1 shell command (ctrl+o to expand)
-
-● Three separate causes. Let me fix the two I can act on.
-
-  Ran 1 shell command (ctrl+o to expand)
-  ⎿  You've hit your session limit · resets 4:40pm (Europe/London)
-     /upgrade to increase your usage limit.
-
-✻ Cooked for 1m 53s

+ 1 - 14
Sunrise/Sunrise.vcxproj

@@ -280,15 +280,7 @@
     <ClCompile Include="src\client\hooks\bootflow\orbit_slice_set.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\region_private.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\world_step.cpp" />
-    <ClCompile Include="src\client\hooks\ember_movies\ember_movies.cpp" />
-    <ClCompile Include="src\client\hooks\ember_movies\resources.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\ember_movie_tick.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\ember_sunburn.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\spawn_hold.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\fade_release.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\spawn\spawn_gate_probe.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\spawn\spawn_gate_targets.cpp" />
-    <ClCompile Include="src\client\hooks\bootflow\spawn\spawn_gate_record_dump.cpp" />
+    <ClCompile Include="src\client\hooks\bootflow\spawn\slice_set_sample.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\activity\activity_arrival.cpp" />
     <ClCompile Include="src\middleware\bap\activity_host_manager\request\selection\activity_manager_selection_bits.cpp" />
     <ClCompile Include="src\middleware\bap\activity_host_manager\request\selection\activity_manager_selection_snapshot.cpp" />
@@ -1249,11 +1241,6 @@
     <ClCompile Remove="vendor\lua\lzio.c" />
   </ItemGroup>
   <ItemGroup>
-    <ClInclude Include="src\client\hooks\ember_movies\ember_movies.h" />
-    <ClInclude Include="src\client\hooks\ember_movies\resources.h" />
-    <ClInclude Include="src\client\hooks\ember_movies\readiness_rules.h" />
-    <ClInclude Include="src\client\hooks\ember_movies\sunburn_rules.h" />
-    <ClInclude Include="src\client\hooks\ember_movies\playback_rules.h" />
     <ClInclude Include="resources\resource.h" />
     <ClInclude Include="src\core\logging\log.h" />
     <ClInclude Include="src\core\logging\snapshot\snapshot.h" />

+ 16 - 0
Sunrise/src/client/content/activity/activity_sdk_generation_scenario_shards.cpp

@@ -15,6 +15,7 @@
 #include "../../../core/logging/log.h"
 #include "../../../state/activity_sdk/generated_world/codec.h"
 #include "../../../state/activity_sdk/generation/definition.h"
+#include "../../../state/build_data/runtime.h"
 #include "activity_sdk_generation_worker_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::worker_internal {
@@ -252,6 +253,18 @@ void publish_parallel_progress(ScenarioBuildBatch& batch, const Scenario& scenar
     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());
@@ -284,6 +297,9 @@ void run_scenario_worker(ScenarioBuildBatch& batch) noexcept {
                                                 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)) {

+ 0 - 372
Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp

@@ -215,378 +215,6 @@ void log_stage_duration(std::string_view stage,
     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. */
 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) {

+ 2 - 2
Sunrise/src/client/content/activity/activity_sdk_squad_graph_edges.cpp

@@ -536,8 +536,8 @@ template <typename Index> void canonicalize_scenarios(Index& index) {
                 edge.targetDescriptors = target.descriptors;
                 std::sort(edge.targetDescriptors.begin(),
                           edge.targetDescriptors.end(),
-                          [&graph](std::uint32_t left, std::uint32_t right) {
-                              return graph.descriptors[left].id < graph.descriptors[right].id;
+                          [&graph](std::uint32_t first, std::uint32_t second) {
+                              return graph.descriptors[first].id < graph.descriptors[second].id;
                           });
                 edge.associationExact = associationExact;
                 if (!edge_identity(topology,

+ 0 - 59
Sunrise/src/client/content/activity/scriptable_catalog_type23_placement_links.cpp

@@ -6,11 +6,8 @@
 #include <limits>
 #include <span>
 #include <utility>
-#include <array>
-#include <cstdio>
 #include <vector>
 
-#include "../../../core/logging/log.h"
 #include "../../../middleware/content/packages/tables/type23_placement_identifier_reader.h"
 #include "../../../state/build_data/scriptables/scriptable_catalog.h"
 
@@ -116,61 +113,6 @@ descriptor_object(const catalog::Snapshot& source, const catalog::Descriptor& de
     return count;
 }
 
-/**
- * Records every type-23 device whose placement link did not resolve to one exact placement.
- *
- * A device that resolves to no exact placement is never drawn and has nothing in the world to
- * drive, so its Auth is accepted and does nothing. Nothing surfaced that before: the browser
- * shows such a row as "context only" whenever its owning object has context positions, which
- * hides the failed join behind the object's own count. The builder already counts every way the
- * match can fail, so this reports the counters once and then names the unresolved rows.
- */
-void report_unresolved_links(const catalog::Snapshot& output) noexcept {
-    const auto& diagnostics = output.type23PlacementDiagnostics;
-    std::array<char, 224> line{};
-    int written = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=type23_placement stage=summary links=%zu complete=%d unread=%llu zero_identity=%llu "
-        "multi_identity=%llu zero_active=%llu multi_active=%llu scenario_resolved=%llu",
-        output.type23PlacementLinks.size(),
-        diagnostics.complete ? 1 : 0,
-        static_cast<unsigned long long>(diagnostics.unreadIdentifiers),
-        static_cast<unsigned long long>(diagnostics.zeroIdentityMatches),
-        static_cast<unsigned long long>(diagnostics.multipleIdentityMatches),
-        static_cast<unsigned long long>(diagnostics.zeroActiveCandidates),
-        static_cast<unsigned long long>(diagnostics.multipleActiveCandidates),
-        static_cast<unsigned long long>(diagnostics.scenarioResolvedCandidates));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-    std::size_t reported = 0;
-    for (const catalog::Type23PlacementLink& link : output.type23PlacementLinks) {
-        if (link.join == catalog::ReferenceJoin::exact || reported >= 32) {
-            continue;
-        }
-        ++reported;
-        written = std::snprintf(line.data(),
-                                line.size(),
-                                "ev=type23_placement stage=unresolved slot=%u identifier=%llu "
-                                "identity_matches=%u candidates=%u active=%u complete=%d join=%d",
-                                link.slotRow,
-                                static_cast<unsigned long long>(link.placementIdentifier),
-                                link.identityMatchCount,
-                                link.candidateCount,
-                                link.activeCandidateCount,
-                                link.complete ? 1 : 0,
-                                static_cast<int>(link.join));
-        if (written > 0) {
-            core::log::write(core::log::Channel::client,
-                             core::log::Level::info,
-                             {line.data(), static_cast<std::size_t>(written)});
-        }
-    }
-}
-
 /** Appends one descriptor link and every retained exact identifier match. */
 [[nodiscard]] bool append_link(catalog::Snapshot& output,
                                std::uint32_t descriptorRow,
@@ -340,7 +282,6 @@ bool append_type23_placement_links(catalog::Snapshot& output,
                 return false;
             }
         }
-        report_unresolved_links(output);
         return !cancelled(cancel);
     } catch (...) {
         output.type23PlacementDiagnostics.complete = false;

+ 0 - 1240
Sunrise/src/client/diagnostics/entity_create_probe.cpp

@@ -1,1240 +0,0 @@
-#include "entity_create_probe.h"
-
-#include <Windows.h>
-#include <intrin.h>
-
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-#include <cstring>
-#include <span>
-#include <string_view>
-
-#include "../../core/logging/log.h"
-#include "../hooking/detour.h"
-#include "../patterns/image_scan.h"
-#include "../patterns/signature_text.h"
-
-namespace sunrise::client::diagnostics {
-namespace {
-
-namespace patterns = client::patterns;
-namespace detour = client::hooking::detour;
-
-/**
- * The index allocator the entity creator calls first.
- * Recovered from the mapped-image dump. Its body is unmistakable: it stores -1 into the caller's
- * out-parameter, then asks a pool at `+0xC118` sized `0x2000` for a free index. The frame size is
- * wildcarded so the match carries no position-dependent byte.
- */
-constexpr std::string_view kIndexAllocatorText =
-    "48 89 5C 24 08 48 89 74 24 10 57 48 83 EC ? 48 8B DA C7 02 FF FF FF FF 48 8B F9 "
-    "BA 00 20 00 00";
-/** Compiled pattern bytes of the signature text above. */
-constexpr auto kIndexAllocator =
-    patterns::signature<patterns::signature_length(kIndexAllocatorText)>(kIndexAllocatorText);
-
-/** The allocator answers this in its out-parameter when it has no index to give. */
-constexpr std::int32_t kNoIndex = -1;
-/**
- * Byte offset of the free-slot bitmap inside the manager the allocator is handed.
- * Read out of the allocator's body: it calls the bitmap search with `rcx = manager + 0xC118` and
- * a width of `0x2000`, then clears the bit it was given. A set bit is therefore a FREE slot, and
- * the search answers -1 only when every word is zero.
- */
-constexpr std::size_t kFreeBitmapOffset = 0xC118;
-/** Slots the bitmap covers, from the width the allocator passes. */
-constexpr std::size_t kFreeBitmapBits = 0x2000;
-/** Words in that bitmap. */
-constexpr std::size_t kFreeBitmapWords = kFreeBitmapBits / 32;
-
-/**
- * Counts the free slots the manager currently holds.
- * The exhaustion line alone cannot separate "the host never gave the client any slots" from
- * "the client used everything it was given", and those need opposite fixes.
- * @param pool Manager the allocator was handed.
- * @return Set bits in its free bitmap, or -1 when the bitmap cannot be read.
- */
-[[nodiscard]] std::int64_t free_slot_count(const void* pool) noexcept {
-    if (pool == nullptr) {
-        return -1;
-    }
-    std::int64_t free = 0;
-    __try {
-        const auto* words = reinterpret_cast<const std::uint32_t*>(
-            static_cast<const std::byte*>(pool) + kFreeBitmapOffset);
-        for (std::size_t word = 0; word < kFreeBitmapWords; ++word) {
-            free += static_cast<std::int64_t>(__popcnt(words[word]));
-        }
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        return -1;
-    }
-    return free;
-}
-/** Outcomes reported per run, so a per-frame failure cannot fill the log. */
-constexpr LONG kMaxReports = 200;
-/**
- * Stack frames captured above this probe on each allocation.
- * The allocator itself is generic — one function serves every entity in the game — so its own
- * address says nothing about what is being built. The callers above it are what differ, and six
- * frames is enough to separate "the world is placing an object" from "a weapon spawned a
- * projectile" without unwinding the whole fiber stack.
- */
-constexpr ULONG kTraceFrames = 6;
-/**
- * Allocation traces per run.
- * A raid load builds a few hundred entities, so this holds several bubble loads while still
- * bounding what a long firefight can write.
- */
-constexpr LONG kMaxTraces = 4096;
-/** Traces already spent. */
-volatile LONG g_traces{};
-/**
- * Image offset of the pointer to the game's entity record table.
- * Recovered from the creation path itself, which indexes it as `base + (handle & 0x1FFF) * stride`
- * at `0x4D71F7`: `imul ebx, [rip -> 0x1F93430]` then `add rbx, [rip -> 0x1F93428]`. The mask is the
- * same 13 bits the allocator's bitmap covers, so a record addresses exactly one allocated index.
- */
-constexpr std::uintptr_t kEntityTableBaseRva = 0x1F93428;
-/** Image offset of the record stride that pairs with the table above. */
-constexpr std::uintptr_t kEntityTableStrideRva = 0x1F93430;
-/** Stride the dump reports. Checked at runtime, because a wrong one would read foreign memory. */
-constexpr std::uint32_t kExpectedRecordStride = 224;
-/** Bytes of each record dumped. The whole record, so the type field can be found by comparison. */
-constexpr std::size_t kRecordDumpBytes = kExpectedRecordStride;
-/** Records dumped per run, bounded so a long session cannot fill the sink. */
-constexpr LONG kMaxRecords = 512;
-/** Records already dumped. */
-volatile LONG g_records{};
-/**
- * Record class every live entity carries at `+0x64`.
- * Constant across all 57 records of a run, so it marks a slot the game has actually built rather
- * than one holding whatever the last entity left behind.
- */
-constexpr std::uint32_t kRecordClass = 0x80809783;
-/** Offset of the record class within a record. */
-constexpr std::size_t kRecordClassOffset = 0x64;
-/** Offset of the object's definition hash. Varies per object kind; `0xFFFFFFFF` where absent. */
-constexpr std::size_t kRecordDefinitionOffset = 0x88;
-/** Offset of the instance ordinal that counts copies of one definition. */
-constexpr std::size_t kRecordOrdinalOffset = 0x8C;
-/** Offset of the transform block, which is still unset when a record is first dumped. */
-constexpr std::size_t kRecordTransformOffset = 0xA0;
-/** Dwords of the transform block reported, covering the orientation and position quads. */
-constexpr std::size_t kRecordTransformDwords = 8;
-/** Seconds between censuses. Short enough to catch a bubble soon after it settles. */
-constexpr DWORD kCensusIntervalMs = 15'000;
-/** Small info-level sample remains available when verbose entity tracing is disabled. */
-void report_simulation_pressure() noexcept;
-/**
- * Most recent manager the allocator was handed.
- * The census needs the free bitmap to tell a live record from one an entity left behind, and the
- * allocator is the only place the manager pointer is known.
- */
-void* volatile g_lastPool{};
-/** Entries one census reports, so a fully populated table cannot fill the sink. */
-constexpr LONG kCensusEntryBudget = 2'048;
-/**
- * Distinct record classes counted per census.
- * The census filtered on one class, `kRecordClass`, and so never reported an index above ~1019.
- * An interaction incident then named entity **3539** as its target while the player stood on the
- * Wall of Wishes activation plate -- an object that works -- and the twenty panels that do not
- * work sit at 749..768. Whatever separates them is not visible while the walk only ever admits
- * one class, so every class is counted and sampled now.
- */
-constexpr std::size_t kClassCapacity = 24;
-/**
- * Records dumped per distinct class, so a large class cannot crowd out a small one.
- * Set at 48 this hid the very thing it was built to find: one class holds every real record, so
- * only indices 0..47 were ever dumped and the Wall of Wishes panels at 749..768 fell outside the
- * log entirely. That absence then read as "the player never reached the wall", which was wrong.
- * The share only needs to stop one class starving another, so it sits at the whole budget.
- */
-constexpr LONG kPerClassDump = 2'048;
-/** Cleared to stop the census thread. */
-volatile LONG g_censusRunning{};
-/** Census thread handle. */
-HANDLE g_censusThread{};
-
-/**
- * Index whose record has not been dumped yet.
- * The record is empty when the allocator hands the index out — the creator fills it afterwards — so
- * each index is read one allocation late, when whatever built it has finished.
- */
-volatile LONG g_pendingIndex{-1};
-
-/**
- * Dumps one entity record so the entity can be named rather than counted.
- * Counting proved the pool works and says nothing about what is in it. The record is the only place
- * the client keeps an entity's identity, and every entity in the run shares one creation path, so
- * the bytes here are what separate a wall panel from a projectile.
- * @param index Index whose record to read.
- */
-void report_record(std::int32_t index) noexcept {
-    if (index < 0 || static_cast<std::size_t>(index) >= kFreeBitmapBits
-        || !core::log::accepts(core::log::Channel::client, core::log::Level::debug)
-        || InterlockedIncrement(&g_records) > kMaxRecords) {
-        return;
-    }
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    if (base == 0) {
-        return;
-    }
-    std::array<char, core::log::kLineCapacity> line{};
-    int written = 0;
-    __try {
-        const auto table = *reinterpret_cast<const std::byte* const*>(base + kEntityTableBaseRva);
-        const auto stride = *reinterpret_cast<const std::uint32_t*>(base + kEntityTableStrideRva);
-        // A stride that has moved means this offset no longer names the table, and reading through
-        // it would dump unrelated memory as if it were an entity.
-        if (table == nullptr || stride != kExpectedRecordStride) {
-            return;
-        }
-        const auto* const record = table + static_cast<std::size_t>(index) * stride;
-        written = std::snprintf(line.data(),
-                                line.size(),
-                                "ev=entity_create stage=record idx=%d hex=",
-                                static_cast<int>(index));
-        for (std::size_t offset = 0; offset < kRecordDumpBytes && written > 0
-                                     && static_cast<std::size_t>(written) + 3 < line.size();
-             ++offset) {
-            const int more = std::snprintf(line.data() + written,
-                                           line.size() - static_cast<std::size_t>(written),
-                                           "%02X",
-                                           std::to_integer<unsigned char>(record[offset]));
-            if (more <= 0) {
-                break;
-            }
-            written += more;
-        }
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        return;
-    }
-    if (written <= 0) {
-        return;
-    }
-    const auto length = static_cast<std::size_t>(written) < line.size()
-                            ? static_cast<std::size_t>(written)
-                            : line.size() - 1;
-    core::log::write(core::log::Channel::client, core::log::Level::debug, {line.data(), length});
-}
-/** Resolved `RtlCaptureStackBackTrace`, or null when ntdll would not give it up. */
-USHORT(NTAPI* g_captureBacktrace)(ULONG, ULONG, PVOID*, PULONG){};
-/** Allocations between pool samples. Frequent enough to shape the drain, rare enough to be free. */
-constexpr LONG kSampleInterval = 16;
-/** Bytes in the bitmap, from the width the client's own stocking path passes to its fill. */
-constexpr std::size_t kFreeBitmapBytes = kFreeBitmapBits / 8;
-/**
- * High slots the host keeps for its own entities and never leases to the client.
- * The join grant is `kSlotCount - kDefaultServerReserve` = 7936, so the top 256 indices are the
- * host's. The client's own initialiser frees the whole bitmap because in its intended world it
- * owns every slot; here it does not, and handing it the reserve would let it allocate an index
- * the host also considers its own.
- */
-constexpr std::size_t kServerReserveSlots = 256;
-/** Bytes of the bitmap that stay clear, covering the reserve at the top of the index space. */
-constexpr std::size_t kReserveBytes = kServerReserveSlots / 8;
-/** Bytes of the bitmap that are freed to the client. */
-constexpr std::size_t kClientBytes = kFreeBitmapBytes - kReserveBytes;
-/** Words of the bitmap covering the client's half. The split lands on a word boundary. */
-constexpr std::size_t kClientWords = kClientBytes / sizeof(std::uint32_t);
-static_assert(kClientBytes % sizeof(std::uint32_t) == 0,
-              "the client half must end on a word so a refill never touches the reserve");
-/** Bits per bitmap word. */
-constexpr std::size_t kBitsPerWord = 32;
-/**
- * Address span treated as belonging to the game's image.
- * The dump reports an image size of 0x8A5EA00, so this clears it with room for a larger build while
- * still rejecting a frame that landed in Sunrise's own module or on a foreign allocation.
- */
-constexpr std::uintptr_t kImageSpan = 0x10000000;
-
-/**
- * The allocator's real shape, read from its body rather than guessed.
- * It uses exactly two arguments: `rcx` is the manager whose free-slot bitmap sits at `+0xC118`,
- * and `rdx` is the out-parameter it fills with the allocated index. It returns `rdx` unchanged.
- */
-using IndexAllocator = void*(__fastcall*)(void*, std::int32_t*) noexcept;
-
-detour::Handle g_allocator{};
-volatile LONG g_reports{};
-/** Successful allocations seen, used only to space the samples. */
-volatile LONG g_allocations{};
-/**
- * One manager's record of the indices this probe has watched the allocator hand out.
- *
- * A blanket `memset(bitmap, 0xFF, ...)` is what made the very first stocking work and what made
- * every later one lethal. It frees index 0 upward, and by the time a pool has drained, index 0
- * belongs to a live entity. The allocator picks the lowest set bit, so the next creation lands on
- * top of a live entity and the world stops being a consistent list of them. That is the crash on
- * respawn, the crash on Worldline Zero's ability, and the mainloop stall that ends a Shuro Chi run
- * a few seconds after the room loads.
- *
- * Keeping the set of indices already handed out turns the refill from "free everything" into
- * "free what was never taken", which is the only form of it that is safe to run on a live pool.
- */
-struct PoolRecord {
-    /** Manager this record belongs to, or null while the slot is unused. */
-    void* pool;
-    /** Set bit per index the allocator gave out and the client has not since handed back. */
-    std::array<volatile LONG, kFreeBitmapWords> live;
-    /** Whether this pool has been refilled at least once. */
-    volatile LONG stocked;
-};
-
-/** Managers tracked at once. A world change builds a new one, so several are live per run. */
-constexpr std::size_t kTrackedPoolCapacity = 16;
-/** Per-manager occupancy records, claimed on first sight. */
-std::array<PoolRecord, kTrackedPoolCapacity> g_pools{};
-
-/**
- * Finds the record for one manager, claiming a free slot on first sight.
- * @param pool Manager the allocator was handed.
- * @return Its record, or null when the table is full.
- */
-[[nodiscard]] PoolRecord* find_pool(void* pool) noexcept {
-    for (auto& record : g_pools) {
-        if (record.pool == pool) {
-            return &record;
-        }
-    }
-    for (auto& record : g_pools) {
-        auto* const slot = reinterpret_cast<void* volatile*>(&record.pool);
-        if (InterlockedCompareExchangePointer(slot, pool, nullptr) == nullptr
-            || record.pool == pool) {
-            return &record;
-        }
-    }
-    // Past capacity nothing is tracked, so nothing is refilled either. A missed refill costs this
-    // world's entities; an untracked one corrupts a live pool.
-    return nullptr;
-}
-
-/**
- * Records that one index is now owned by an entity.
- * @param record Manager record, or null when the manager is untracked.
- * @param index Index the allocator produced.
- */
-void mark_live(PoolRecord* record, std::int32_t index) noexcept {
-    if (record == nullptr || index < 0 || static_cast<std::size_t>(index) >= kFreeBitmapBits) {
-        return;
-    }
-    const auto slot = static_cast<std::size_t>(index);
-    (void)InterlockedOr(&record->live[slot / kBitsPerWord],
-                        static_cast<LONG>(1u << (slot % kBitsPerWord)));
-}
-/** Off leaves the probe reporting only, which is what it did before it could write. */
-bool g_stockUnstockedPool{};
-/** Refill a drained pool as well as an unstocked one. Safe now that the refill spares live slots. */
-bool g_restockAlways{};
-
-/**
- * Reports one probe outcome, up to the per-run budget.
- * @param stage Which half answered.
- * @param outcome What it answered.
- * @param detail Free slots left in the pool, or -1 when the bitmap could not be read.
- */
-void report_pair(const char* stage,
-                 const char* outcome,
-                 std::int64_t detail,
-                 std::int64_t allocations) noexcept {
-    const auto level = std::string_view(outcome) == "exhausted"
-        ? core::log::Level::warn : core::log::Level::debug;
-    if (!core::log::accepts(core::log::Channel::client, level)
-        || InterlockedIncrement(&g_reports) > kMaxReports) {
-        return;
-    }
-    std::array<char, core::log::kLineCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=entity_create stage=%s result=%s free=%lld allocs=%lld",
-                                      stage,
-                                      outcome,
-                                      static_cast<long long>(detail),
-                                      static_cast<long long>(allocations));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         level,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/**
- * Reports one refill, naming how many slots it actually handed back.
- * @param outcome Whether the pool answered after the refill.
- * @param free Free slots the bitmap holds now.
- * @param freed Slots this refill put back.
- * @param allocations Successful allocations seen so far.
- */
-void report_stock(const char* outcome,
-                  std::int64_t free,
-                  std::int64_t freed,
-                  std::int64_t allocations) noexcept {
-    if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
-        || InterlockedIncrement(&g_reports) > kMaxReports) {
-        return;
-    }
-    std::array<char, core::log::kLineCapacity> line{};
-    const int written =
-        std::snprintf(line.data(),
-                      line.size(),
-                      "ev=entity_create stage=allocate result=%s free=%lld freed=%lld allocs=%lld",
-                      outcome,
-                      static_cast<long long>(free),
-                      static_cast<long long>(freed),
-                      static_cast<long long>(allocations));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/**
- * Names one allocation and the call sites that asked for it.
- *
- * Counting allocations proved the pool works; it cannot say what is being built, and that is the
- * question a missing Wall of Wishes actually poses. Its panels are one repeated object, so a burst
- * of identical traces landing on consecutive indices as a bubble loads is the wall being created,
- * and the absence of such a burst is the wall never being asked for. Those two need opposite fixes.
- *
- * Addresses are image-relative because the game is rebased every run; an RVA maps straight into the
- * mapped-image dump, where file offset equals RVA.
- * @param pool Manager the index came from, so per-type managers would show as distinct pointers.
- * @param index Index the allocator produced.
- * @param sequence Allocation ordinal within the run.
- */
-void report_allocation(const void* pool, std::int32_t index, LONG sequence) noexcept {
-    if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
-        || InterlockedIncrement(&g_traces) > kMaxTraces) {
-        return;
-    }
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    std::array<char, core::log::kLineCapacity> line{};
-    int written = std::snprintf(line.data(),
-                               line.size(),
-                               "ev=entity_create stage=alloc n=%ld idx=%d pool=0x%llX sites=",
-                               static_cast<long>(sequence),
-                               static_cast<int>(index),
-                               static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(pool)));
-    if (written <= 0) {
-        return;
-    }
-    std::array<void*, kTraceFrames> frames{};
-    // Frame 0 is this probe, which is never interesting, so the capture starts one above it.
-    const USHORT captured = g_captureBacktrace == nullptr
-                                ? 0
-                                : g_captureBacktrace(1, kTraceFrames, frames.data(), nullptr);
-    for (USHORT frame = 0; frame < captured && written > 0
-                           && static_cast<std::size_t>(written) < line.size();
-         ++frame) {
-        const auto site = reinterpret_cast<std::uintptr_t>(frames[frame]);
-        // A frame inside Sunrise's own module is noise here; only the game's code is addressable
-        // in the dump, so anything outside it is printed as a gap rather than a misleading offset.
-        const bool inImage = base != 0 && site >= base && (site - base) < kImageSpan;
-        const int more =
-            std::snprintf(line.data() + written,
-                          line.size() - static_cast<std::size_t>(written),
-                          inImage ? "%s0x%llX" : "%s-",
-                          frame == 0 ? "" : ",",
-                          static_cast<unsigned long long>(inImage ? site - base : 0));
-        if (more <= 0) {
-            break;
-        }
-        written += more;
-    }
-    const auto length = static_cast<std::size_t>(written) < line.size()
-                            ? static_cast<std::size_t>(written)
-                            : line.size() - 1;
-    core::log::write(core::log::Channel::client, core::log::Level::debug, {line.data(), length});
-}
-
-void report(const char* stage, const char* outcome, std::int64_t detail) noexcept {
-    if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
-        || InterlockedIncrement(&g_reports) > kMaxReports) {
-        return;
-    }
-    std::array<char, core::log::kLineCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=entity_create stage=%s result=%s free=%lld",
-                                      stage,
-                                      outcome,
-                                      static_cast<long long>(detail));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/**
- * Mirrors the index allocator and reports whether it produced an index.
- * The out-parameter is the answer: the original writes -1 into it before doing anything, and
- * overwrites it only on success.
- */
-/**
- * Reads the game's entity record table, or reports that it cannot be addressed.
- * @param table Receives the table base.
- * @param stride Receives the record stride.
- * @return True when both were read and the stride still matches this build.
- */
-[[nodiscard]] bool entity_table(const std::byte*& table, std::uint32_t& stride) noexcept {
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    if (base == 0) {
-        return false;
-    }
-    __try {
-        table = *reinterpret_cast<const std::byte* const*>(base + kEntityTableBaseRva);
-        stride = *reinterpret_cast<const std::uint32_t*>(base + kEntityTableStrideRva);
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        return false;
-    }
-    return table != nullptr && stride == kExpectedRecordStride;
-}
-
-/**
- * Reports which of one word's 32 indices already hold an entity.
- *
- * The probe's own record of handed-out indices covers only what came through the hooked allocator,
- * and a census measured that as 58 of 830 — the world's placed objects reach the table by some
- * other path entirely. Trusting that record alone therefore freed 7936 slots while 42 entities
- * were sitting in them, and the client then allocated straight over the top. The game's own record
- * table is the authority on which slots are taken, so occupancy is read from there instead.
- * @param table Entity record table base.
- * @param stride Record stride.
- * @param word Word of the free bitmap being refilled.
- * @return Set bit per index in that word whose record is live.
- */
-[[nodiscard]] LONG occupied_mask(const std::byte* table, std::uint32_t stride, std::size_t word) noexcept {
-    std::uint32_t mask = 0;
-    for (std::size_t bit = 0; bit < kBitsPerWord; ++bit) {
-        const std::size_t index = word * kBitsPerWord + bit;
-        __try {
-            if (*reinterpret_cast<const std::uint32_t*>(table + index * stride
-                                                        + kRecordClassOffset)
-                == kRecordClass) {
-                mask |= 1u << bit;
-            }
-        } __except (EXCEPTION_EXECUTE_HANDLER) {
-            // An unreadable record is treated as taken, which costs a slot rather than an entity.
-            mask |= 1u << bit;
-        }
-    }
-    return static_cast<LONG>(mask);
-}
-
-/**
- * Frees every client slot that no entity holds, leaving the ones that do alone.
- *
- * The client's own initialiser at `0x7FF71DDADB20` fills this bitmap with `0xFF` — every slot free
- * — but only when a role global reads zero; here it reads 3, so the fill never runs and the bitmap
- * is all-zero from the first frame. Every entity creation then fails, which is why no enemy, plate,
- * door or banner ever appeared and why an encounter bubble kicked to orbit. Writing those bytes
- * ourselves is right exactly once, on a pool that is still empty. On a pool that has drained it is
- * catastrophic, because the slots the client is using read as clear too and become free again.
- *
- * So the refill is driven by `record->live` instead of by a constant. A slot is freed only when the
- * bitmap says it is taken AND this probe never watched the allocator hand it out. Two passes,
- * because another thread may claim a slot while the first one runs: the second re-clears anything
- * that became live in between, so no index is ever offered twice.
- * @param pool Manager the allocator was handed.
- * @param record Occupancy record for that manager.
- * @param failure Receives a Windows error, or 1 for a null pool and 2 for a faulting write.
- * @return Slots freed, or -1 when the bitmap could not be written.
- */
-[[nodiscard]] std::int64_t stock_pool(void* pool,
-                                      PoolRecord* record,
-                                      std::uint32_t& failure) noexcept {
-    failure = 0;
-    if (pool == nullptr || record == nullptr) {
-        failure = 1;
-        return -1;
-    }
-    auto* const bitmap = static_cast<std::byte*>(pool) + kFreeBitmapOffset;
-    // The bitmap sits in the game's own allocation, so it carries whatever protection that
-    // allocation was given. Reading it worked, which does not prove it is writable.
-    DWORD previous = 0;
-    if (VirtualProtect(bitmap, kFreeBitmapBytes, PAGE_READWRITE, &previous) == FALSE) {
-        failure = GetLastError();
-        return -1;
-    }
-    const std::byte* table = nullptr;
-    std::uint32_t stride = 0;
-    const bool hasTable = entity_table(table, stride);
-    std::int64_t freed = 0;
-    __try {
-        auto* const words = reinterpret_cast<volatile LONG*>(bitmap);
-        for (std::size_t word = 0; word < kClientWords; ++word) {
-            const LONG available = words[word];
-            // A slot the client has put back is no longer live, so it returns to the pool with the
-            // rest. Without this the record would only ever grow and the refill would fade to a
-            // no-op over a long session.
-            const LONG live = InterlockedAnd(&record->live[word], ~available) & ~available;
-            // The record table is the authority; the probe's own list is kept as a second opinion
-            // for anything created in the window before its record is filled in.
-            const LONG occupied = hasTable ? occupied_mask(table, stride, word) : 0;
-            const LONG missing =
-                static_cast<LONG>(~static_cast<std::uint32_t>(live | available | occupied));
-            if (missing != 0) {
-                (void)InterlockedOr(&words[word], missing);
-                freed += __popcnt(static_cast<unsigned int>(missing));
-            }
-        }
-        for (std::size_t word = 0; word < kClientWords; ++word) {
-            const LONG live = record->live[word];
-            if (live != 0) {
-                (void)InterlockedAnd(&words[word], ~live);
-            }
-        }
-        // The host's reserve at the top of the space stays clear so the client cannot allocate an
-        // index the host also considers its own.
-        for (std::size_t word = kClientWords; word < kFreeBitmapWords; ++word) {
-            (void)InterlockedAnd(&words[word], 0);
-        }
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        failure = 2;
-        freed = -1;
-    }
-    DWORD restored = 0;
-    (void)VirtualProtect(bitmap, kFreeBitmapBytes, previous, &restored);
-    if (freed >= 0) {
-        (void)InterlockedExchange(&record->stocked, 1);
-        if (!hasTable) {
-            // Worth saying out loud: without the table the refill is back to trusting a list that
-            // has been measured as 7% complete, which is how live entities got overwritten.
-            report("allocate", "stock_without_table", freed);
-        }
-    }
-    return freed;
-}
-
-void* __fastcall allocator_body(void* pool, std::int32_t* index) noexcept {
-    const auto call = reinterpret_cast<IndexAllocator>(g_allocator.original);
-    if (call == nullptr) {
-        return nullptr;
-    }
-    void* result = call(pool, index);
-    InterlockedExchangePointer(&g_lastPool, pool);
-    PoolRecord* const record = find_pool(pool);
-    if (index == nullptr || *index != kNoIndex) {
-        // Every index the client takes is recorded before anything else can act on it, because a
-        // refill that does not know about it would offer the same index to a second entity.
-        if (index != nullptr) {
-            mark_live(record, *index);
-        }
-        // Sample the pool as it is spent. A steadily falling count means indices are allocated and
-        // never returned; a count that rises again means the client's own free path does work and
-        // the drain is simply the world being large. Those need opposite fixes, and the exhaustion
-        // line alone cannot tell them apart because it only ever fires at zero.
-        const LONG seen = InterlockedIncrement(&g_allocations);
-        report_allocation(pool, index == nullptr ? kNoIndex : *index, seen);
-        // One allocation behind, so the creator has had time to fill the record being read.
-        report_record(InterlockedExchange(&g_pendingIndex, index == nullptr ? -1 : *index));
-        if ((seen % kSampleInterval) == 0) {
-            // The count is reported beside the free total: if the pool empties while this barely
-            // moves, the bitmap is being cleared by something other than allocation.
-            report_pair("allocate", "sample", free_slot_count(pool), seen);
-        }
-        return result;
-    }
-    const std::int64_t free = free_slot_count(pool);
-    // A pool is refilled the first time it is seen empty, and again on every later drain when the
-    // knob is on. Both are safe now: the refill spares the indices already handed out, so it can
-    // no longer hand one index to two entities the way the old blanket fill did.
-    const bool allowed = g_stockUnstockedPool && record != nullptr
-                         && (g_restockAlways || record->stocked == 0);
-    if (free != 0 || !allowed) {
-        report_pair("allocate", "exhausted", free, g_allocations);
-        return result;
-    }
-    std::uint32_t failure = 0;
-    const std::int64_t freed = stock_pool(pool, record, failure);
-    if (freed < 0) {
-        // Naming the reason matters: a refused write and a faulting page need different fixes.
-        report("allocate", "stock_failed", static_cast<std::int64_t>(failure));
-        return result;
-    }
-    result = call(pool, index);
-    // `freed` is the number that matters. It should fall well short of the whole client half: the
-    // gap is the live entities the old fill used to trample.
-    report_stock(*index == kNoIndex ? "stocked_still_empty" : "stocked",
-                 free_slot_count(pool),
-                 freed,
-                 g_allocations);
-    if (index != nullptr) {
-        mark_live(record, *index);
-    }
-    return result;
-}
-
-/**
- * Image offset of the pointer that reaches the game's entity pool descriptors.
- * From the creation path at `0x4D71B5`: `mov rcx, [rip -> 0x2439C70]` then `add rdx, [rcx]` with
- * the pool ordinal already shifted left by six, so descriptors are 64 bytes apart and their array
- * base is one further dereference in. Within a descriptor, `+0x08` is the pool base and `+0x30`
- * its element size -- `imul eax, [rdx + 0x30]` then `add rcx, [rdx + 8]`.
- */
-constexpr std::uintptr_t kPoolDirectoryRva = 0x2439C70;
-/** Bytes between pool descriptors. */
-constexpr std::size_t kPoolDescriptorStride = 64;
-/** Descriptors probed. The ordinal comes from a handle's high bits, which are six wide. */
-constexpr std::size_t kPoolDescriptorCount = 64;
-/** Offset of a pool's base pointer within its descriptor. */
-constexpr std::size_t kPoolBaseOffset = 0x08;
-/** Offset of a pool's element size within its descriptor. */
-constexpr std::size_t kPoolElementSizeOffset = 0x30;
-/** An element size outside this is not a record, so the descriptor is not one either. */
-constexpr std::uint32_t kMaximumElementSize = 4096;
-/**
- * Pools whose elements match the entity record stride, walked by the census.
- * The directory holds TWO 224-byte pools, ordinals 33 and 35, at stable and distinct bases. The
- * census has only ever read whichever one `kEntityTableBaseRva` points at, so half the records of
- * this shape were never looked at -- and the activation plate that works, entity 3539, is not in
- * the half that was.
- */
-constexpr std::size_t kRecordPoolCapacity = 4;
-/** Bases of the record-shaped pools found in the directory. */
-std::array<const std::byte*, kRecordPoolCapacity> g_recordPools{};
-/** Ordinals of those pools, in the same order. */
-std::array<std::size_t, kRecordPoolCapacity> g_recordPoolOrdinals{};
-/** Record-shaped pools found. */
-std::size_t g_recordPoolCount{};
-
-/**
- * Reports every entity pool the game keeps, not just the one the census walks.
- *
- * The class tally proved the 224-byte table holds exactly one class and 830 records, and that
- * everything read above them is out-of-bounds noise. So the Wall of Wishes activation plate, which
- * an interaction incident named as entity 3539 and which visibly works, cannot be in that table at
- * all -- while the twenty panels that do not work are. Handles carry a pool ordinal in their high
- * bits, which is why one table was never the whole picture.
- */
-void report_pools() noexcept {
-    if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)) {
-        return;
-    }
-    const auto image = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    if (image == 0) {
-        return;
-    }
-    for (std::size_t ordinal = 0; ordinal < kPoolDescriptorCount; ++ordinal) {
-        const std::byte* poolBase = nullptr;
-        std::uint32_t elementSize = 0;
-        __try {
-            const auto* const directory =
-                *reinterpret_cast<const std::byte* const*>(image + kPoolDirectoryRva);
-            if (directory == nullptr) {
-                return;
-            }
-            const auto* const descriptors = *reinterpret_cast<const std::byte* const*>(directory);
-            if (descriptors == nullptr) {
-                return;
-            }
-            const auto* const descriptor = descriptors + ordinal * kPoolDescriptorStride;
-            poolBase = *reinterpret_cast<const std::byte* const*>(descriptor + kPoolBaseOffset);
-            elementSize =
-                *reinterpret_cast<const std::uint32_t*>(descriptor + kPoolElementSizeOffset);
-        } __except (EXCEPTION_EXECUTE_HANDLER) {
-            continue;
-        }
-        if (poolBase == nullptr || elementSize == 0 || elementSize > kMaximumElementSize) {
-            continue;
-        }
-        if (elementSize == kExpectedRecordStride && g_recordPoolCount < kRecordPoolCapacity) {
-            g_recordPoolOrdinals[g_recordPoolCount] = ordinal;
-            g_recordPools[g_recordPoolCount++] = poolBase;
-        }
-        std::array<char, core::log::kLineCapacity> line{};
-        const int written =
-            std::snprintf(line.data(),
-                          line.size(),
-                          "ev=entity_census stage=pool ordinal=%zu base=0x%llX element=%u",
-                          ordinal,
-                          static_cast<unsigned long long>(
-                              reinterpret_cast<std::uintptr_t>(poolBase)),
-                          elementSize);
-        if (written > 0) {
-            core::log::write(core::log::Channel::client,
-                             core::log::Level::debug,
-                             {line.data(), static_cast<std::size_t>(written)});
-        }
-    }
-}
-
-/**
- * Reports the built records of one record-shaped pool other than the cached one.
- *
- * The cached pointer at `kEntityTableBaseRva` names a single pool, and the directory shows two of
- * this shape. An object that works and an object that does not may simply live in different pools,
- * and that is not visible while only one is read.
- * @param poolBase Base of the pool to walk.
- * @param stride Record stride, the same for every pool of this shape.
- * @param ordinal Directory ordinal, reported so the two can be told apart.
- */
-void walk_pool(const std::byte* poolBase, std::uint32_t stride, std::size_t ordinal) noexcept {
-    LONG reported = 0;
-    for (std::size_t index = 0; index < kFreeBitmapBits && reported < kCensusEntryBudget; ++index) {
-        std::array<char, core::log::kLineCapacity> line{};
-        int written = 0;
-        __try {
-            const auto* const record = poolBase + index * stride;
-            const auto recordClass =
-                *reinterpret_cast<const std::uint32_t*>(record + kRecordClassOffset);
-            if (recordClass != kRecordClass) {
-                continue;
-            }
-            written = std::snprintf(
-                line.data(),
-                line.size(),
-                "ev=entity_census stage=entry pool=%zu idx=%zu cls=0x%08X def=0x%08X ord=%u rec=",
-                ordinal,
-                index,
-                recordClass,
-                *reinterpret_cast<const std::uint32_t*>(record + kRecordDefinitionOffset),
-                *reinterpret_cast<const std::uint32_t*>(record + kRecordOrdinalOffset));
-            for (std::size_t offset = 0; offset < kRecordDumpBytes && written > 0
-                                         && static_cast<std::size_t>(written) + 3 < line.size();
-                 ++offset) {
-                const int more = std::snprintf(line.data() + written,
-                                               line.size() - static_cast<std::size_t>(written),
-                                               "%02X",
-                                               std::to_integer<unsigned char>(record[offset]));
-                if (more <= 0) {
-                    break;
-                }
-                written += more;
-            }
-        } __except (EXCEPTION_EXECUTE_HANDLER) {
-            continue;
-        }
-        if (written <= 0) {
-            continue;
-        }
-        ++reported;
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-    std::array<char, core::log::kLineCapacity> tail{};
-    const int written = std::snprintf(tail.data(),
-                                      tail.size(),
-                                      "ev=entity_census stage=pool_end ordinal=%zu records=%ld",
-                                      ordinal,
-                                      static_cast<long>(reported));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::debug,
-                         {tail.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/**
- * Walks the whole entity table and reports every slot the game has built.
- *
- * The per-allocation dump reads a record one allocation after it is handed out, which is early
- * enough that the transform is still its default — every instance of one definition reported the
- * same placement, which cannot be true. A census taken well after a bubble has settled reads the
- * finished records instead, and placement is the field that matters here: a grid of identical
- * co-planar objects is a wall of shootable panels and nothing else is, so this can identify the
- * Wall of Wishes without knowing the game's own name for it.
- */
-void run_census() noexcept {
-    if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)) {
-        return;
-    }
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    if (base == 0) {
-        return;
-    }
-    const std::byte* table = nullptr;
-    std::uint32_t stride = 0;
-    __try {
-        table = *reinterpret_cast<const std::byte* const*>(base + kEntityTableBaseRva);
-        stride = *reinterpret_cast<const std::uint32_t*>(base + kEntityTableStrideRva);
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        return;
-    }
-    if (table == nullptr || stride != kExpectedRecordStride) {
-        return;
-    }
-    // A record keeps its class marker after the entity is gone, so the marker alone cannot
-    // distinguish a live entity from a slot one left behind. The free bitmap can: a slot the
-    // allocator would hand out is not holding anything, whatever its record still says.
-    const auto* freeWords = static_cast<const std::uint32_t*>(nullptr);
-    if (void* const pool = g_lastPool; pool != nullptr) {
-        freeWords = reinterpret_cast<const std::uint32_t*>(static_cast<std::byte*>(pool)
-                                                           + kFreeBitmapOffset);
-    }
-    // First pass counts every class present. A record whose class word is zero or all ones has
-    // never been built, so those are the only two values treated as empty.
-    std::array<std::uint32_t, kClassCapacity> classes{};
-    std::array<LONG, kClassCapacity> classCounts{};
-    std::array<LONG, kClassCapacity> classDumped{};
-    std::size_t classCount = 0;
-    for (std::size_t index = 0; index < kFreeBitmapBits; ++index) {
-        std::uint32_t value = 0;
-        __try {
-            value = *reinterpret_cast<const std::uint32_t*>(table + index * stride
-                                                            + kRecordClassOffset);
-        } __except (EXCEPTION_EXECUTE_HANDLER) {
-            continue;
-        }
-        if (value == 0 || value == 0xFFFFFFFFU) {
-            continue;
-        }
-        std::size_t slot = 0;
-        while (slot < classCount && classes[slot] != value) {
-            ++slot;
-        }
-        if (slot == classCount) {
-            if (classCount == kClassCapacity) {
-                continue;
-            }
-            classes[classCount++] = value;
-        }
-        ++classCounts[slot];
-    }
-    for (std::size_t slot = 0; slot < classCount; ++slot) {
-        std::array<char, core::log::kLineCapacity> head{};
-        const int headWritten = std::snprintf(head.data(),
-                                              head.size(),
-                                              "ev=entity_census stage=class value=0x%08X count=%ld",
-                                              classes[slot],
-                                              static_cast<long>(classCounts[slot]));
-        if (headWritten > 0) {
-            core::log::write(core::log::Channel::client,
-                             core::log::Level::debug,
-                             {head.data(), static_cast<std::size_t>(headWritten)});
-        }
-    }
-    g_recordPoolCount = 0;
-    report_pools();
-    // Name the pool the census has been reading all along, so its ordinal can be matched against
-    // the directory rather than assumed.
-    {
-        std::array<char, core::log::kLineCapacity> line{};
-        const int written = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=entity_census stage=table base=0x%llX stride=%u pools=%zu",
-            static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(table)),
-            stride,
-            g_recordPoolCount);
-        if (written > 0) {
-            core::log::write(core::log::Channel::client,
-                             core::log::Level::debug,
-                             {line.data(), static_cast<std::size_t>(written)});
-        }
-    }
-    // Every record-shaped pool, not just the cached one. A pool the cached pointer already names
-    // is not walked twice.
-    for (std::size_t slot = 0; slot < g_recordPoolCount; ++slot) {
-        if (g_recordPools[slot] == table) {
-            continue;
-        }
-        walk_pool(g_recordPools[slot], stride, g_recordPoolOrdinals[slot]);
-    }
-    LONG live = 0;
-    LONG stale = 0;
-    // Per pass, not per run: a shared budget truncated the one census that mattered.
-    LONG entries = 0;
-    for (std::size_t index = 0; index < kFreeBitmapBits; ++index) {
-        std::array<char, core::log::kLineCapacity> line{};
-        int written = 0;
-        __try {
-            const auto* const record = table + index * stride;
-            const auto recordClass =
-                *reinterpret_cast<const std::uint32_t*>(record + kRecordClassOffset);
-            if (recordClass == 0 || recordClass == 0xFFFFFFFFU) {
-                continue;
-            }
-            std::size_t slot = 0;
-            while (slot < classCount && classes[slot] != recordClass) {
-                ++slot;
-            }
-            const bool spent = slot == classCount || classDumped[slot] >= kPerClassDump;
-            if (!spent) {
-                ++classDumped[slot];
-            }
-            unsigned slotFree = 0;
-            if (freeWords != nullptr
-                && (freeWords[index / kBitsPerWord] & (1u << (index % kBitsPerWord))) != 0) {
-                slotFree = 1;
-                ++stale;
-            } else {
-                ++live;
-            }
-            // The tally above counts every record; only the dump is rationed.
-            if (spent || entries >= kCensusEntryBudget) {
-                continue;
-            }
-            written = std::snprintf(
-                line.data(),
-                line.size(),
-                "ev=entity_census stage=entry idx=%zu cls=0x%08X def=0x%08X ord=%u free=%u rec=",
-                index,
-                recordClass,
-                *reinterpret_cast<const std::uint32_t*>(record + kRecordDefinitionOffset),
-                *reinterpret_cast<const std::uint32_t*>(record + kRecordOrdinalOffset),
-                slotFree);
-            // The whole record, not just the transform block. The block at `+0xA0` decodes as a
-            // clean quaternion but the four dwords after it are not the position — as floats they
-            // are denormals and values in the trillions. Somewhere in these 224 bytes there are
-            // three coordinates, and the way to find them is to scan every aligned offset across a
-            // group for one that varies plausibly. A 5x5 grid of co-planar panels is the Wall of
-            // Wishes and nothing else in the room is shaped like that, so placement identifies it
-            // where counting has not.
-            for (std::size_t offset = 0; offset < kRecordDumpBytes && written > 0
-                                         && static_cast<std::size_t>(written) + 3 < line.size();
-                 ++offset) {
-                const int more = std::snprintf(line.data() + written,
-                                               line.size() - static_cast<std::size_t>(written),
-                                               "%02X",
-                                               std::to_integer<unsigned char>(record[offset]));
-                if (more <= 0) {
-                    break;
-                }
-                written += more;
-            }
-        } __except (EXCEPTION_EXECUTE_HANDLER) {
-            continue;
-        }
-        if (written <= 0) {
-            continue;
-        }
-        ++entries;
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-    std::array<char, core::log::kLineCapacity> tail{};
-    const int written = std::snprintf(tail.data(),
-                                      tail.size(),
-                                      "ev=entity_census stage=end live=%ld stale=%ld allocs=%ld",
-                                      static_cast<long>(live),
-                                      static_cast<long>(stale),
-                                      static_cast<long>(g_allocations));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::debug,
-                         {tail.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/**
- * Runs a census on its own thread so it does not sit inside the game's allocation path.
- * @param unused Thread parameter, unused.
- * @return Always zero.
- */
-DWORD WINAPI census_thread(LPVOID unused) noexcept {
-    (void)unused;
-    while (g_censusRunning != 0) {
-        Sleep(kCensusIntervalMs);
-        if (g_censusRunning == 0) {
-            break;
-        }
-        report_simulation_pressure();
-        run_census();
-    }
-    return 0;
-}
-
-/**
- * Attaches one probe, reporting its own outcome.
- * @param signature Pattern to find.
- * @param name Reported name.
- * @param replacement Probe body.
- * @param handle Receives the trampoline.
- * @return True when the target was found and the detour attached.
- */
-[[nodiscard]] bool attach(std::span<const patterns::PatternByte> signature,
-                          const char* name,
-                          void* replacement,
-                          detour::Handle& handle) noexcept {
-    std::byte* const target = patterns::scan_main_image_unique(signature, name);
-    std::array<char, core::log::kLineCapacity> line{};
-    if (target == nullptr) {
-        const int written = std::snprintf(line.data(),
-                                          line.size(),
-                                          "ev=entity_create stage=attach name=%s result=fail",
-                                          name);
-        if (written > 0) {
-            core::log::write(core::log::Channel::client,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(written)});
-        }
-        return false;
-    }
-    const detour::Spec spec{target, replacement};
-    const bool attached = detour::install(spec, handle);
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=entity_create stage=attach name=%s result=%s",
-                                      name,
-                                      attached ? "ok" : "fail");
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         attached ? core::log::Level::info : core::log::Level::warn,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-    return attached;
-}
-
-} // namespace
-
-/** Reports which half of the client's entity creation refuses. */
-bool install_entity_create_probe(bool stockUnstockedPool, bool restockAlways) noexcept {
-    g_stockUnstockedPool = stockUnstockedPool;
-    g_restockAlways = restockAlways;
-    // Resolved rather than linked: the trace is a diagnostic, and a missing export should cost the
-    // call sites in the log, not the probe that stocks the pool.
-    if (HMODULE const ntdll = GetModuleHandleW(L"ntdll.dll"); ntdll != nullptr) {
-        g_captureBacktrace = reinterpret_cast<decltype(g_captureBacktrace)>(
-            reinterpret_cast<void*>(GetProcAddress(ntdll, "RtlCaptureStackBackTrace")));
-    }
-    const bool allocator = attach(kIndexAllocator,
-                                  "entity_index_allocator",
-                                  reinterpret_cast<void*>(&allocator_body),
-                                  g_allocator);
-    if (allocator) {
-        InterlockedExchange(&g_censusRunning, 1);
-        g_censusThread = CreateThread(nullptr, 0, &census_thread, nullptr, 0, nullptr);
-    }
-    // The initialiser is deliberately NOT hooked. Its fifth argument is passed on the stack
-    // (`mov dword [var_20h], eax` before the call), and a four-argument replacement got that
-    // wrong and black-screened the load. It does not need hooking anyway: the allocator alone
-    // answers the question, because the initialiser only runs when the allocator succeeded.
-    return allocator;
-}
-
-namespace {
-// Original 16CC5D0 only returns the native failure-state pointer. It does not reset
-// the reason. Read it while the creation-failure log is emitted, before 16EE180
-// replaces the more specific reason with generic entity-creation failure (12).
-[[nodiscard]] int native_creation_failure_reason(std::uintptr_t base) noexcept {
-    constexpr std::uintptr_t rva = 0x16CC5D0;
-    constexpr std::array<unsigned char,13> prefix{
-        0x40,0x53,0x48,0x83,0xEC,0x20,0x48,0x8B,0x1D,0x2B,0x57,0x98,0x01};
-    __try {
-        if (base == 0 || std::memcmp(reinterpret_cast<const void*>(base+rva),
-                                     prefix.data(),prefix.size()) != 0) return -1;
-        using Getter = const int* (__fastcall*)();
-        const auto* state = reinterpret_cast<Getter>(base+rva)();
-        return state == nullptr ? -1 : *state;
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return -1; }
-}
-// 170B2B0 allocates from a separate 1024-record global bitmap. Free network
-// indices alone cannot establish room for a simulation record or its state.
-[[nodiscard]] int simulation_record_count(std::uintptr_t base) noexcept {
-    constexpr std::uintptr_t bitmapRva = 0x26BE0E0 + 2607256ULL * 4;
-    __try {
-        if (base == 0) return -1;
-        const auto* words = reinterpret_cast<const std::uint32_t*>(base+bitmapRva);
-        int used = 0;
-        for (std::size_t i=0;i<32;++i) used += static_cast<int>(__popcnt(words[i]));
-        return used;
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return -1; }
-}
-
-struct ReplicationCensus {
-    int authorityRecords{};
-    int detached{};
-    int views{};
-    int enabled{};
-    int pendingDeletes{};
-    int packetViews{};
-    int authorityEpoch{};
-    bool operator==(const ReplicationCensus&) const = default;
-};
-
-// Diagnostic reads only. Native records remain owned by their ordinary deletion
-// and acknowledgement handlers; a census must never free or alter them.
-[[nodiscard]] bool read_replication_census(std::uintptr_t base,
-                                          ReplicationCensus& result) noexcept {
-    __try {
-        if (base == 0) return false;
-        const auto* bitmap = reinterpret_cast<const std::uint32_t*>(base + 0x30B0340);
-        const auto* records = reinterpret_cast<const std::byte*>(base + 0x30B0440);
-        for (unsigned index = 0; index < 1024; ++index) {
-            if ((bitmap[index / 32] & (1U << (index % 32))) == 0) continue;
-            const auto* record = records + index * 112;
-            if (*reinterpret_cast<const std::uintptr_t*>(record + 96) == 0) continue;
-            ++result.authorityRecords;
-            if (*reinterpret_cast<const std::uint32_t*>(record + 4) == 0xFFFFFFFFU)
-                ++result.detached;
-        }
-        const auto* manager = static_cast<const std::byte*>(
-            InterlockedCompareExchangePointer(&g_lastPool, nullptr, nullptr));
-        if (manager == nullptr) return true;
-        result.authorityEpoch = std::to_integer<unsigned char>(manager[53284]);
-        for (unsigned index = 0; index < 31; ++index) {
-            const auto* view = *reinterpret_cast<const std::byte* const*>(manager + 24 + index * 8);
-            if (view == nullptr) continue;
-            if (*reinterpret_cast<const std::uintptr_t*>(view) != base + 0x1C9ADD8) return false;
-            ++result.views;
-            if (view[8] != std::byte{} && view[9] != std::byte{} && view[10] != std::byte{})
-                ++result.enabled;
-            result.pendingDeletes += *reinterpret_cast<const int*>(view + 48);
-            if (*reinterpret_cast<const std::uintptr_t*>(view + 32) != 0) ++result.packetViews;
-        }
-        return true;
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-
-void report_simulation_pressure() noexcept {
-    // This census has one reader thread. Report pressure and its eventual recovery,
-    // so the next mission test can show records being freed before a Bird failure.
-    static int previous = -1;
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    const int used = simulation_record_count(base);
-    if (used >= 0 && used != previous) {
-        core::log::writef(core::log::Channel::client, core::log::Level::info,
-            "ev=simulation_records stage=pressure used=%d previous=%d capacity=1024",
-            used, previous);
-    }
-    previous = used;
-    static ReplicationCensus previousReplication{};
-    static bool haveReplication = false;
-    ReplicationCensus current{};
-    if (read_replication_census(base, current)
-        && (!haveReplication || !(current == previousReplication))) {
-        core::log::writef(core::log::Channel::client, core::log::Level::info,
-            "ev=entity_replication stage=census authority_records=%d authority_capacity=512 "
-            "detached=%d views=%d enabled=%d pending_deletes=%d packet_views=%d authority_epoch=%d",
-            current.authorityRecords, current.detached, current.views, current.enabled,
-            current.pendingDeletes, current.packetViews, current.authorityEpoch);
-        previousReplication = current;
-        haveReplication = true;
-    }
-}
-}
-
-/** The retail Bird line is also used for initializer failure with free indices remaining. */
-void report_entity_create_failure_pool() noexcept {
-    const auto* pool = InterlockedCompareExchangePointer(&g_lastPool, nullptr, nullptr);
-    core::log::writef(core::log::Channel::client, core::log::Level::warn,
-        "ev=entity_create_failure stage=pool free=%lld allocations=%ld",
-        static_cast<long long>(free_slot_count(pool)), static_cast<long>(g_allocations));
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    core::log::writef(core::log::Channel::client, core::log::Level::warn,
-        "ev=entity_create_failure stage=native reason=%d simulation_records=%d capacity=1024 "
-        "reason_11=state_heap reason_14=simulation_records reason_15=authority_records",
-        native_creation_failure_reason(base), simulation_record_count(base));
-}
-
-/** Detaches the entity-creation probes. */
-void uninstall_entity_create_probe() noexcept {
-    InterlockedExchange(&g_censusRunning, 0);
-    if (g_censusThread != nullptr) {
-        // The census only reads, so a shutdown that beats it costs a census, never the process.
-        (void)CloseHandle(g_censusThread);
-        g_censusThread = nullptr;
-    }
-    if (g_allocator.attached) {
-        (void)detour::uninstall(g_allocator);
-    }
-}
-
-} // namespace sunrise::client::diagnostics

+ 0 - 37
Sunrise/src/client/diagnostics/entity_create_probe.h

@@ -1,37 +0,0 @@
-#pragma once
-
-namespace sunrise::client::diagnostics {
-
-/**
- * Reports which half of the client's entity creation refuses.
- * The client logs `failed to create '<type>' entity` and nothing else, and Sunrise's own note at
- * `server/bap/encrypted/transactions/service_outcome_commit.cpp` reads that as "it has no free
- * index". That reading is an assumption, and acting on it once already cost a build-and-run cycle:
- * a lease top-up landed and changed nothing.
- *
- * The creator calls two things in order — an index allocator that answers -1 when it has nothing
- * to give, then an initialiser that answers false when it refuses the entity it was handed. Both
- * end at the same log line, so the line cannot tell them apart. These two detours can: each
- * reports its own outcome, so one run says which half is failing and the guessing stops.
- *
- * Diagnostic only. Neither replacement changes an argument or a result, and both are found with
- * an independent scan rather than through the shared target registry, so a signature that no
- * longer matches this build costs the probe and nothing else.
- * @param stockUnstockedPool Refill a bitmap that is entirely unstocked, which is what the
- *        client's own initialiser would have done had its role global read zero.
- * @param restockAlways Also refill a pool that has drained, not only one never stocked. Needed to
- *        get past an encounter bubble the drained pool would otherwise refuse. Safe: the refill
- *        spares every index the probe watched the allocator hand out, so it cannot re-free one that
- *        is still owned the way the earlier blanket fill did.
- * @return True when the probe attached.
- */
-[[nodiscard]] bool install_entity_create_probe(bool stockUnstockedPool,
-                                               bool restockAlways) noexcept;
-
-/** Reads the current free-slot count at a native entity-creation failure. */
-void report_entity_create_failure_pool() noexcept;
-
-/** Detaches the entity-creation probes. */
-void uninstall_entity_create_probe() noexcept;
-
-} // namespace sunrise::client::diagnostics

+ 0 - 10
Sunrise/src/client/hooks/bootflow/bootflow_hook_lifecycle.cpp

@@ -28,11 +28,6 @@ constexpr std::array kFixes{
     Fix{&stage_orbit_slice_set, &publish_orbit_slice_set},
     Fix{&stage_composition_check, &publish_composition_check},
     Fix{&stage_orbit_handoff, &publish_orbit_handoff},
-    Fix{&stage_loading_cinematics, &publish_loading_cinematics},
-    Fix{&stage_ember_movie_tick, &publish_ember_movie_tick},
-    Fix{&stage_ember_movie_ui, &publish_ember_movie_ui},
-    Fix{&stage_ember_movie_hud, &publish_ember_movie_hud},
-    Fix{&stage_ember_sunburn, &publish_ember_sunburn},
     Fix{&stage_owner_activity_slot, &publish_owner_activity_slot},
     Fix{&stage_region_private, &publish_region_private},
 };
@@ -112,11 +107,6 @@ void uninstall() noexcept {
     uninstall_world_step();
     uninstall_region_private();
     uninstall_owner_activity_slot();
-    uninstall_loading_cinematics();
-    uninstall_ember_sunburn();
-    uninstall_ember_movie_tick();
-    uninstall_ember_movie_hud();
-    uninstall_ember_movie_ui();
     uninstall_orbit_handoff();
     uninstall_composition_check();
     uninstall_orbit_slice_set();

+ 0 - 68
Sunrise/src/client/hooks/bootflow/ember_movie_hud.cpp

@@ -1,68 +0,0 @@
-#include <cstdint>
-#include <cstring>
-#include <intrin.h>
-#include "internal.h"
-#include "../ember_movies/ember_movies.h"
-#include "../../../core/logging/log.h"
-
-namespace sunrise::client::hooks::bootflow {
-namespace {
-hooking::detour::Handle handle{};
-const void* windowDrawReturn{};
-using DrawWidget = void(__fastcall*)(void*, void*, void*);
-
-void __fastcall draw_widget(void* widget, void* context, void* commands) noexcept {
-    const bool presenting = ember_movies::presenting();
-    static thread_local bool reported{};
-    if (!presenting) reported = false;
-    // 13D9060 also submits nested widgets. Only the verified window-list caller
-    // (132C1BD) supplies a full window; never read +310 from a child widget.
-    if (presenting && widget && _ReturnAddress() == windowDrawReturn) {
-        std::int32_t role{};
-        std::memcpy(&role, static_cast<const std::byte*>(widget) + 0x310, sizeof(role));
-        // 131778E assigns role 18 through 13165C0 to the gameplay HUD, including
-        // equipment-specific replacements of the default "hud" window.
-        // Skip its entire cached subtree, not the layer containing subtitles,
-        // menus and cinematic_overlay. Native update/input/visibility stay intact.
-        if (role == 0x12) {
-            if (!reported) {
-                std::uint32_t asset{}, name{};
-                std::memcpy(&asset, static_cast<const std::byte*>(widget) + 0x18, sizeof(asset));
-                std::memcpy(&name, static_cast<const std::byte*>(widget) + 0x24, sizeof(name));
-                core::log::writef(core::log::Channel::client, core::log::Level::info,
-                    "ev=ember_movie result=hud_draw_suppressed role=%d asset=%08X name=%08X",
-                    role, asset, name);
-                reported = true;
-            }
-            return;
-        }
-    }
-    if (auto original = reinterpret_cast<DrawWidget>(handle.original))
-        original(widget, context, commands);
-}
-}
-
-StageResult stage_ember_movie_hud(hooking::detour::Spec& spec) noexcept {
-    if (handle.attached) return StageResult::attached;
-    constexpr auto windowDrawSig = signature<signature_length("4C 8B C7 48 8D 54 24 70 48 8B CE E8 ? ? ? ? 48 8B CF E8 ? ? ? ?")>(
-        "4C 8B C7 48 8D 54 24 70 48 8B CE E8 ? ? ? ? 48 8B CF E8 ? ? ? ?");
-    constexpr auto widgetSig = signature<signature_length("40 57 41 55 41 56 48 83 EC 30 48 8B F9 4D 8B F0 0F B7 49 1E 4C 8B EA")>(
-        "40 57 41 55 41 56 48 83 EC 30 48 8B F9 4D 8B F0 0F B7 49 1E 4C 8B EA");
-    const auto* caller = scan_main_image_unique(windowDrawSig, "ember_movie_hud_window_call");
-    auto* target = scan_main_image_unique(widgetSig, "ember_movie_hud_widget_draw");
-    if (!caller || !target || resolve_relative(caller + 12, caller + 16) != target)
-        return StageResult::unavailable;
-    windowDrawReturn = caller + 16;
-    spec = {target, reinterpret_cast<void*>(&draw_widget)};
-    return StageResult::staged;
-}
-void publish_ember_movie_hud(const hooking::detour::Handle& value) noexcept {
-    handle = value;
-    core::log::write(core::log::Channel::client, core::log::Level::info,
-        value.attached ? "ev=ember_movie result=hud_filter_attached"
-                       : "ev=ember_movie result=hud_filter_attach_failed");
-}
-void uninstall_ember_movie_hud() noexcept {
-    static_cast<void>(hooking::detour::uninstall(handle));
-}
-}

+ 0 - 34
Sunrise/src/client/hooks/bootflow/ember_movie_tick.cpp

@@ -1,34 +0,0 @@
-#include "internal.h"
-#include "bootflow_hook_lifecycle.h"
-#include "../ember_movies/ember_movies.h"
-#include "../../../core/logging/log.h"
-namespace sunrise::client::hooks::bootflow {
-namespace {
-hooking::detour::Handle handle{};
-using Tick = void(__fastcall*)(void*);
-void __fastcall movie_tick(void* decoder) noexcept {
-    if (auto original=reinterpret_cast<Tick>(handle.original)) original(decoder);
-    // Video presentation can suspend the player-camera callback. Observe completion
-    // from the movie player's own frame as well, including its final stopped frame.
-    if (ember_movies::active()) poll_current_slice_set();
-}
-}
-StageResult stage_ember_movie_tick(hooking::detour::Spec& spec) noexcept {
-    if (handle.attached) return StageResult::attached;
-    constexpr auto sig=signature<signature_length("4C 8B DC 55 57 49 8D AB 58 FE FF FF 48 81 EC 98 02 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 50 01 00 00 48 8B F9 48 8B 49 08")>(
-        "4C 8B DC 55 57 49 8D AB 58 FE FF FF 48 81 EC 98 02 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 50 01 00 00 48 8B F9 48 8B 49 08");
-    auto* target=scan_main_image_unique(sig,"ember_movie_frame");
-    if (!target) return StageResult::unavailable;
-    spec={target,reinterpret_cast<void*>(&movie_tick)};return StageResult::staged;
-}
-void publish_ember_movie_tick(const hooking::detour::Handle& value) noexcept {
-    handle=value;
-    ember_movies::frame_ready(value.attached);
-    core::log::write(core::log::Channel::client,core::log::Level::info,
-        value.attached ? "ev=ember_movie result=frame_attached" : "ev=ember_movie result=frame_attach_failed");
-}
-void uninstall_ember_movie_tick() noexcept {
-    ember_movies::frame_ready(false);
-    static_cast<void>(hooking::detour::uninstall(handle));
-}
-}

+ 0 - 48
Sunrise/src/client/hooks/bootflow/ember_movie_ui.cpp

@@ -1,48 +0,0 @@
-#include "internal.h"
-#include "../ember_movies/ember_movies.h"
-#include "../ember_movies/playback_rules.h"
-#include "../../../core/logging/log.h"
-namespace sunrise::client::hooks::bootflow {
-namespace {
-hooking::detour::Handle handle{};
-using SelectState=bool(__fastcall*)(void*,int);
-bool __fastcall select_state(void* ui,int requested) noexcept {
-    // Use the playback window, not the loading branch of E2EAE0.
-    // 1312540/13126E0 map 21h to cinematic_overlay and 22h to loading.
-    // The direct player has no type-6 controller to advance that branch.
-    // E1CD60 owns window/category transitions; all drawing stays native.
-    // Preserve loading/error/menu requests; substitute only normal gameplay.
-    const int selected=ember_movies::movie_ui_state(requested,ember_movies::presenting());
-    auto original=reinterpret_cast<SelectState>(handle.original);
-    if (!original) return false;
-    const bool result=original(ui,selected);
-    static bool wasCinematic{};
-    const bool cinematic=selected!=requested;
-    if (cinematic!=wasCinematic) {
-        core::log::writef(core::log::Channel::client,core::log::Level::info,
-            "ev=ember_movie result=presentation_state requested=%d selected=%d applied=%d category=%d",
-            requested,selected,*reinterpret_cast<int*>(static_cast<std::byte*>(ui)+0x204),
-            *reinterpret_cast<int*>(static_cast<std::byte*>(ui)+0x200));
-        wasCinematic=cinematic;
-    }
-    return result;
-}
-}
-StageResult stage_ember_movie_ui(hooking::detour::Spec& spec) noexcept {
-    if (handle.attached) return StageResult::attached;
-    constexpr auto sig=signature<signature_length("40 55 53 57 41 54 41 55 41 57 48 8D AC 24 08 FD FF FF 48 81 EC F8 03 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 E0 02 00 00 4C 8B E9 8B DA")>(
-        "40 55 53 57 41 54 41 55 41 57 48 8D AC 24 08 FD FF FF 48 81 EC F8 03 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 E0 02 00 00 4C 8B E9 8B DA");
-    auto* target=scan_main_image_unique(sig,"ember_movie_presentation_state");
-    if (!target) return StageResult::unavailable;
-    spec={target,reinterpret_cast<void*>(&select_state)};return StageResult::staged;
-}
-void publish_ember_movie_ui(const hooking::detour::Handle& value) noexcept {
-    handle=value;ember_movies::ui_ready(value.attached);
-    core::log::write(core::log::Channel::client,core::log::Level::info,
-        value.attached ? "ev=ember_movie result=presentation_attached" : "ev=ember_movie result=presentation_attach_failed");
-}
-void uninstall_ember_movie_ui() noexcept {
-    ember_movies::ui_ready(false);
-    static_cast<void>(hooking::detour::uninstall(handle));
-}
-}

+ 0 - 62
Sunrise/src/client/hooks/bootflow/ember_sunburn.cpp

@@ -1,62 +0,0 @@
-#include <Windows.h>
-#include "internal.h"
-#include "../ember_movies/resources.h"
-#include "../ember_movies/sunburn_rules.h"
-#include "../../../core/logging/log.h"
-#include <atomic>
-#include <cstring>
-namespace sunrise::client::hooks::bootflow {
-namespace {
-hooking::detour::Handle hook{};
-using Attach=bool(__fastcall*)(void*,std::uint32_t);
-std::atomic<unsigned> reports{};
-template<class T> T read(const void* pointer) { T value{};std::memcpy(&value,pointer,sizeof(value));return value; }
-std::uint32_t* ember_template(void* component) noexcept {
-    __try {
-        auto* self=static_cast<std::byte*>(component);
-        // Only Ember's type-26 slot 43, whose source and runtime spawn template were captured live.
-        if (read<std::uint32_t>(self)!=0x80B3C0C6U) return nullptr;
-        const auto relative=read<std::int64_t>(self+0x210);
-        if (relative<=0 || relative>0x1000) return nullptr;
-        auto* request=reinterpret_cast<std::uint32_t*>(self+0x210+relative);
-        return ember_movies::ember_burn_source(read<std::uint32_t>(self),read<std::uint32_t>(self+4),
-            read<std::uint64_t>(self+8),*request) ? request : nullptr;
-    } __except(EXCEPTION_EXECUTE_HANDLER) { return nullptr; }
-}
-bool __fastcall attach(void* component,std::uint32_t actor) noexcept {
-    const auto original=reinterpret_cast<Attach>(hook.original);
-    auto* request=ember_template(component);
-    if (!request) return original(component,actor);
-    if (!ember_movies::sunburn_resident()) {
-        if (reports.fetch_add(1)<8) core::log::write(core::log::Channel::client,core::log::Level::warn,
-            "ev=ember_sunburn result=resource_unavailable asset=80B82489");
-        return false;
-    }
-    const auto previous=*request;
-    bool result{};
-    // This is the writable, per-component spawn request, NOT shared package data.
-    // 9F2760 -> 56DE00 consumes the asset at request+0 and creates a tracked child of this actor.
-    // Preserve native duplicate checks, attachment registration and detach behavior.
-    *request=0x80B82489U;
-    __try { result=original(component,actor); }
-    __finally { *request=previous; }
-    if (result && reports.fetch_add(1)<16) core::log::writef(core::log::Channel::client,core::log::Level::info,
-        "ev=ember_sunburn result=attached actor=%08X asset=80B82489",actor);
-    return result;
-}
-}
-StageResult stage_ember_sunburn(hooking::detour::Spec& spec) noexcept {
-    if (hook.attached) return StageResult::attached;
-    constexpr auto sig=signature<signature_length("48 89 5C 24 18 57 48 83 EC 20 8B C2 8B DA 25 FF 1F 00 00 48 8B F9 44 8B C0 8B D0 49 C1 E8 05")>(
-        "48 89 5C 24 18 57 48 83 EC 20 8B C2 8B DA 25 FF 1F 00 00 48 8B F9 44 8B C0 8B D0 49 C1 E8 05");
-    auto* target=scan_main_image_unique(sig,"ember_sunburn_attach");
-    if (!target) return StageResult::unavailable;
-    spec={target,reinterpret_cast<void*>(&attach)};return StageResult::staged;
-}
-void publish_ember_sunburn(const hooking::detour::Handle& value) noexcept {
-    hook=value;
-    core::log::write(core::log::Channel::client,core::log::Level::info,
-        value.attached ? "ev=ember_sunburn result=hook_attached" : "ev=ember_sunburn result=hook_failed");
-}
-void uninstall_ember_sunburn() noexcept { static_cast<void>(hooking::detour::uninstall(hook)); }
-}

+ 0 - 18
Sunrise/src/client/hooks/bootflow/internal.h

@@ -76,24 +76,6 @@ void publish_orbit_handoff(const hooking::detour::Handle& handle) noexcept;
 /** Detaches the orbit handoff release. */
 void uninstall_orbit_handoff() noexcept;
 
-[[nodiscard]] StageResult stage_ember_movie_tick(hooking::detour::Spec& spec) noexcept;
-void publish_ember_movie_tick(const hooking::detour::Handle& handle) noexcept;
-void uninstall_ember_movie_tick() noexcept;
-[[nodiscard]] StageResult stage_ember_movie_ui(hooking::detour::Spec& spec) noexcept;
-void publish_ember_movie_ui(const hooking::detour::Handle& handle) noexcept;
-void uninstall_ember_movie_ui() noexcept;
-[[nodiscard]] StageResult stage_ember_movie_hud(hooking::detour::Spec& spec) noexcept;
-void publish_ember_movie_hud(const hooking::detour::Handle& handle) noexcept;
-void uninstall_ember_movie_hud() noexcept;
-[[nodiscard]] StageResult stage_ember_sunburn(hooking::detour::Spec& spec) noexcept;
-void publish_ember_sunburn(const hooking::detour::Handle& handle) noexcept;
-void uninstall_ember_sunburn() noexcept;
-
-/** LoadingCinematics_Suppressed: travel-only suppression, separate from movie Auth. */
-[[nodiscard]] StageResult stage_loading_cinematics(hooking::detour::Spec& spec) noexcept;
-void publish_loading_cinematics(const hooking::detour::Handle& handle) noexcept;
-void uninstall_loading_cinematics() noexcept;
-
 /**
  * Stages the owner activity slot force. It pins the participation record to the replicated
  * snapshot at `comp + 496` instead of the local one at `comp + 1256`.

+ 0 - 89
Sunrise/src/client/hooks/bootflow/loading_cinematics.cpp

@@ -1,89 +0,0 @@
-#include <cstdint>
-#include <cstring>
-#include <string_view>
-
-#include "../../../core/logging/log.h"
-#include "../../../core/settings/settings.h"
-#include "../../../state/activity_sdk/runtime.h"
-#include "internal.h"
-
-namespace sunrise::client::hooks::bootflow {
-namespace {
-// EF44C0's travel hold calls LoadingCinematics_Suppressed at +30 (C24490 in
-// the captured build). The tiny predicate alone matches two functions.
-constexpr std::string_view kCallerText =
-    "48 89 5C 24 ? 57 48 83 EC ? 48 8B D9 E8 ? ? ? ? 80 3D ? ? ? ? 00 "
-    "48 8B F8 75 ? E8 ? ? ? ? 84 C0 75 ? E8 ? ? ? ? 84 C0 75 ?";
-constexpr auto kCaller = signature<signature_length(kCallerText)>(kCallerText);
-// C294B0 reads the activity index from the native group activity property. The
-// loading flow itself uses this reader at E2EC30 before testing suppression.
-constexpr std::string_view kActivityReaderText =
-    "48 89 5C 24 ? 55 48 8D AC 24 ? ? ? ? 48 81 EC ? ? ? ? 48 8B 05 ? ? ? ? "
-    "48 33 C4 48 89 85 ? ? ? ? 48 8B D9 E8 ? ? ? ? 4C 8B C0 33 D2 8B C2 "
-    "4D 85 C0 74 ? 49 63 40 10 48 69 C8 A0 C8 01 00";
-constexpr auto kActivityReader = signature<signature_length(kActivityReaderText)>(kActivityReaderText);
-constexpr std::uint32_t kEmberDefinition = 0x38F926B2U;
-using ActivityReader = void(__fastcall*)(std::uint16_t*);
-ActivityReader g_activityReader{};
-using Predicate = bool(__fastcall*)();
-hooking::detour::Handle g_handle{};
-
-bool entering_ember() noexcept {
-    if (g_activityReader == nullptr) return false;
-    std::uint16_t index = 0xFFFF;
-    g_activityReader(&index);
-    if (index == 0xFFFF) return false;
-    const auto catalog = state::activity_sdk::snapshot();
-    if (catalog == nullptr) return false;
-    for (const auto& activity : catalog->activities()) {
-        if (activity.activityIndex == index) {
-            return activity.definitionHash == kEmberDefinition;
-        }
-    }
-    return false;
-}
-
-bool __fastcall loading_cinematics_suppressed() noexcept {
-    // Read each time: retained host sessions and saved character activity values
-    // can still name Ember after the client has selected orbit or another mission.
-    if (core::settings::get().client.suppressLoadingCinematics && entering_ember()) {
-        return true;
-    }
-    const auto original = reinterpret_cast<Predicate>(g_handle.original);
-    return original != nullptr && original();
-}
-} // namespace
-
-StageResult stage_loading_cinematics(hooking::detour::Spec& spec) noexcept {
-    if (g_handle.attached) return StageResult::attached;
-    g_activityReader = reinterpret_cast<ActivityReader>(
-        scan_main_image_unique(kActivityReader, "loading_cinematics_activity"));
-    if (g_activityReader == nullptr) return StageResult::unavailable;
-    const auto* caller = scan_main_image_unique(kCaller, "loading_cinematics_hold");
-    if (caller == nullptr) return StageResult::unavailable;
-    auto* target = resolve_relative(caller + 31, caller + 35);
-    // Validate the no-argument boolean wrapper before staging its detour.
-    constexpr unsigned char head[]{0x48, 0x83, 0xEC, 0x28, 0xE8};
-    constexpr unsigned char tail[]{0x84, 0xC0, 0x0F, 0x95, 0xC0, 0x48, 0x83, 0xC4, 0x28, 0xC3};
-    if (target == nullptr || std::memcmp(target, head, sizeof(head)) != 0
-        || std::memcmp(target + 9, tail, sizeof(tail)) != 0) {
-        core::log::write(core::log::Channel::client, core::log::Level::warn,
-                        "ev=bootflow stage=loading_cinematics result=fail reason=predicate_signature");
-        return StageResult::unavailable;
-    }
-    spec = hooking::detour::Spec{target, reinterpret_cast<void*>(&loading_cinematics_suppressed)};
-    return StageResult::staged;
-}
-
-void publish_loading_cinematics(const hooking::detour::Handle& handle) noexcept {
-    g_handle = handle;
-    core::log::write(core::log::Channel::client,
-                    handle.attached ? core::log::Level::info : core::log::Level::warn,
-                    handle.attached ? "ev=bootflow stage=loading_cinematics result=attached"
-                                    : "ev=bootflow stage=loading_cinematics result=fail reason=attach");
-}
-
-void uninstall_loading_cinematics() noexcept {
-    if (g_handle.attached) (void)hooking::detour::uninstall(g_handle);
-}
-} // namespace sunrise::client::hooks::bootflow

+ 1 - 14
Sunrise/src/client/hooks/bootflow/world_step.cpp

@@ -7,10 +7,7 @@
 #include "../../../core/logging/log.h"
 #include "bootflow_hook_lifecycle.h"
 #include "internal.h"
-#include "spawn/probe.h"
-#include "../mission_retirement/mission_retirement.h"
-#include "../ember_movies/ember_movies.h"
-#include "../ember_movies/orbit_return.h"
+#include "spawn/slice_set_sample.h"
 
 namespace sunrise::client::hooks::bootflow {
 namespace {
@@ -63,16 +60,6 @@ void poll_world_step() noexcept {
 /** Publishes the client's current local slice-set index. */
 void poll_current_slice_set() noexcept {
     const std::int32_t index = spawn::sample_current_slice_set();
-    mission_retirement::poll(index);
-    ember_movies::poll(index, read_step());
-    ember_movies::orbit_return::poll(index, read_step());
-    const std::int32_t previous = g_publishedSliceSet.load(std::memory_order_relaxed);
-    // A slice-set change is a world replacement whose transition arms a fresh fade, and a
-    // teleport never passes the off-destination step that re-arms the release. Re-arm here or
-    // the new world stays black behind the spent one-shot.
-    if (index >= 0 && previous >= 0 && index != previous) {
-        rearm_fade_release();
-    }
     g_publishedSliceSet.store(index, std::memory_order_relaxed);
     g_publishedSliceSetTick.store(GetTickCount64(), std::memory_order_release);
 }

+ 5 - 24
Sunrise/src/client/hooks/cine_auth_probe/cine_auth_probe.cpp

@@ -7,8 +7,6 @@
 
 #include "cine_auth_probe.h"
 
-#include <Windows.h>
-
 #include <array>
 #include <atomic>
 #include <cstdarg>
@@ -86,9 +84,8 @@ constexpr std::size_t kCompGenerationOffset = 400;
 constexpr std::size_t kCompStartedOffset = 608;
 /** Deferred flag: an Auth body arrived while the armed gate was closed. */
 constexpr std::size_t kCompDeferredOffset = 609;
-/** Bounded per-window logging survives a long mission without exhausting the intro budget. */
-constexpr unsigned kMaxReports = 32;
-constexpr ULONGLONG kReportWindowMs = 5000;
+/** Process-wide line cap, so a stuck component cannot flood the log. */
+constexpr unsigned kMaxReports = 256;
 
 using ApplyGate = char(__fastcall*)(void*, void*);
 using BodyApply = char(__fastcall*)(void*, const std::byte*);
@@ -118,21 +115,14 @@ std::atomic_bool g_installed{false};
 ArmedCheck g_armedCheck{nullptr};
 // The whole chain runs on the component's owning thread; the counters and flags are diagnostic
 // only, so they need no lock.
-std::array<UpdateState, 16> g_lastUpdates{};
-std::size_t g_nextUpdate{};
-ULONGLONG g_reportWindow{};
+UpdateState g_lastUpdate{};
 unsigned g_reports{0};
 thread_local bool t_inGate{false};
 thread_local bool t_inUpdate{false};
 thread_local bool t_startRan{false};
 
-/** Formats a bounded info-level probe line; normal playtest logs retain it. */
+/** Formats and writes one probe line at debug level, under the shared line cap. */
 void write_line(const char* format, ...) noexcept {
-    const auto now = GetTickCount64();
-    if (now - g_reportWindow >= kReportWindowMs) {
-        g_reportWindow = now;
-        g_reports = 0;
-    }
     if (g_reports >= kMaxReports) {
         return;
     }
@@ -144,7 +134,7 @@ void write_line(const char* format, ...) noexcept {
     va_end(args);
     if (written > 0) {
         core::log::write(core::log::Channel::client,
-                         core::log::Level::info,
+                         core::log::Level::debug,
                          {line.data(), static_cast<std::size_t>(written)});
     }
 }
@@ -280,15 +270,6 @@ char __fastcall update_tick(void* component) noexcept {
     if (comp != nullptr) {
         state.latch = static_cast<int>(comp[kCompStartedOffset]);
     }
-    UpdateState* previous = nullptr;
-    for (auto& entry : g_lastUpdates) {
-        if (entry.component == component) { previous = &entry; break; }
-    }
-    if (previous == nullptr) {
-        previous = &g_lastUpdates[g_nextUpdate++ % g_lastUpdates.size()];
-        *previous = {};
-    }
-    auto& g_lastUpdate = *previous;
     if (state.component != g_lastUpdate.component || state.armed != g_lastUpdate.armed
         || state.deferred != g_lastUpdate.deferred || state.latch != g_lastUpdate.latch) {
         g_lastUpdate = state;

+ 0 - 203
Sunrise/src/client/hooks/ember_movies/ember_movies.cpp

@@ -1,203 +0,0 @@
-#include "ember_movies.h"
-#include "playback_rules.h"
-#include "resources.h"
-#include "orbit_return.h"
-#include <Windows.h>
-#include <array>
-#include <atomic>
-#include <cstdio>
-#include <cstring>
-#include "../../../core/logging/log.h"
-#include "../../patterns/image_scan.h"
-#include "../../patterns/signature_text.h"
-namespace sunrise::client::hooks::ember_movies {
-namespace {
-using namespace patterns;
-using Accessor = void*(__fastcall*)();
-using Operation = void(__fastcall*)(void*);
-using Play = void(__fastcall*)(void*, std::uint32_t, std::uint32_t);
-using Busy = bool(__fastcall*)(void*);
-struct Api { Accessor manager{}, decoder{}; Operation acquire{}, release{}, stop{}; Play play{}; Busy busy{}; } api;
-SRWLOCK lock = SRWLOCK_INIT;
-std::atomic_bool watching{false}, frameReady{false};
-std::atomic_bool presentation{false}, uiReady{false};
-Owner owner{};
-Owner firstCompleted{};
-std::uint64_t key{}, began{};
-unsigned movie{};
-Status state{};
-bool acquired{}, stopRequested{}, attempted{}, escapeHeld{};
-Playback playback{};
-MovieResource resource{};
-void* decoderOwner{};
-int lastDecoderState{-1};
-thread_local bool inPoll{};
-constexpr std::array<std::uint32_t, 2> assets{0x80BCA001U, 0x80BCA003U};
-void report(const char* result, int decoderState = -1) {
-    std::array<char, 240> text{};
-    std::snprintf(text.data(), text.size(),
-        "ev=ember_movie result=%s movie=%u asset=%08X request=%llu decoder_state=%d",
-        result, movie, movie >= 1 && movie <= 2 ? assets[movie-1] : 0,
-        static_cast<unsigned long long>(key), decoderState);
-    core::log::write(core::log::Channel::client, core::log::Level::info, text.data());
-}
-void* call_target(std::byte* code, std::size_t offset) {
-    if (!code || code[offset] != std::byte{0xE8}) return nullptr;
-    std::int32_t relative{}; std::memcpy(&relative, code + offset + 1, 4);
-    return code + offset + 5 + relative;
-}
-bool resolve() {
-    if (attempted) return api.play != nullptr;
-    attempted = true;
-    // Native pre-rendered component start DDB0F0: acquire manager then play config+4C.
-    constexpr auto startSig = signature<signature_length("40 56 48 83 EC 20 48 83 79 30 FF 48 8B F1 0F 85 ? ? ? ? E8 ? ? ? ? 84 C0 0F 85")>(
-        "40 56 48 83 EC 20 48 83 79 30 FF 48 8B F1 0F 85 ? ? ? ? E8 ? ? ? ? 84 C0 0F 85");
-    constexpr auto stopSig = signature<signature_length("40 53 48 83 EC 20 48 83 79 30 FF 48 8B D9 74 22 E8 ? ? ? ? 48 8B C8 E8 ? ? ? ? E8")>(
-        "40 53 48 83 EC 20 48 83 79 30 FF 48 8B D9 74 22 E8 ? ? ? ? 48 8B C8 E8 ? ? ? ? E8");
-    constexpr auto busySig = signature<signature_length("48 83 EC 28 83 79 58 FF 75 ? 8B 0D ? ? ? ? 33 D2 48 89 5C 24 20 32 DB")>(
-        "48 83 EC 28 83 79 58 FF 75 ? 8B 0D ? ? ? ? 33 D2 48 89 5C 24 20 32 DB");
-    auto* start = scan_main_image_unique(startSig, "ember_movie_start");
-    auto* stop = scan_main_image_unique(stopSig, "ember_movie_stop");
-    auto* busy = scan_main_image_unique(busySig, "ember_movie_busy");
-    Api candidate{};
-    candidate.manager = reinterpret_cast<Accessor>(call_target(start, 0x72));
-    candidate.acquire = reinterpret_cast<Operation>(call_target(start, 0x7A));
-    candidate.play = reinterpret_cast<Play>(call_target(start, 0x8E));
-    candidate.stop = reinterpret_cast<Operation>(call_target(stop, 0x18));
-    candidate.release = reinterpret_cast<Operation>(call_target(stop, 0x25));
-    candidate.busy = reinterpret_cast<Busy>(busy);
-    candidate.decoder = reinterpret_cast<Accessor>(call_target(busy, 0x48));
-    if (!candidate.manager || !candidate.acquire || !candidate.play || !candidate.stop
-        || !candidate.release || !candidate.busy || !candidate.decoder) { report("signature_failed"); return false; }
-    api = candidate; return true;
-}
-template<class T> T field(void* pointer, unsigned offset) {
-    T value{}; std::memcpy(&value, static_cast<std::byte*>(pointer)+offset, sizeof(value)); return value;
-}
-void release() {
-    if (acquired) { api.release(api.manager()); acquired=false; }
-}
-void fail(const char* reason) {
-    presentation.store(false);
-    // Stop only our exact decoder asset, never another movie's playback.
-    if (acquired) {
-        auto* decoder=api.decoder();
-        if (decoder && decoder==decoderOwner && field<std::uint32_t>(decoder,0x1B4)==assets[movie-1])
-            api.stop(api.manager());
-        release();
-    }
-    state=Status::failed; watching.store(!resource.release()); report(reason);
-}
-}
-bool request(Owner next, std::uint64_t nextKey, unsigned index, bool stop) noexcept {
-    if (!next.session || !next.generation || !nextKey || index<1 || index>2) return false;
-    AcquireSRWLockExclusive(&lock);
-    bool accepted=false;
-    if (stop) {
-        if (next==owner && index==movie && (state==Status::preparing || state==Status::playing)) {
-            stopRequested=true; accepted=true;
-        }
-    } else if (next==owner && nextKey==key && index==movie) {
-        accepted=state!=Status::failed;
-    } else if (!acquired && (!resource.held() || can_chain_movies(owner,next,firstCompleted,movie,index,state))
-        && state!=Status::queued && state!=Status::preparing && state!=Status::playing) {
-        if (index==1 || !(next==owner)) firstCompleted={};
-        owner=next; key=nextKey; movie=index; state=Status::queued; began=GetTickCount64();
-        stopRequested=false; playback={}; decoderOwner=nullptr; lastDecoderState=-1;
-        escapeHeld=(GetAsyncKeyState(VK_ESCAPE)&0x8000)!=0;
-        watching.store(true); report("queued"); accepted=true;
-    }
-    ReleaseSRWLockExclusive(&lock); return accepted;
-}
-Status status(Owner next, unsigned index) noexcept {
-    AcquireSRWLockShared(&lock);
-    auto value=next==owner && index==movie ? state : Status::absent;
-    ReleaseSRWLockShared(&lock); return value;
-}
-bool active() noexcept { return watching.load() || orbit_return::active(); }
-bool presenting() noexcept { return presentation.load(); }
-void ui_ready(bool ready) noexcept { uiReady.store(ready); }
-void frame_ready(bool ready) noexcept { frameReady.store(ready); }
-void poll(std::int32_t region, std::int32_t step) noexcept {
-    if (inPoll) return;
-    inPoll=true;
-    struct Reset { ~Reset() { inPoll=false; } } reset;
-    AcquireSRWLockExclusive(&lock);
-    if (state!=Status::queued && state!=Status::preparing && state!=Status::playing) {
-        // STM and CNN share textures. Waiting for those textures to disappear
-        // before reporting STM's EOF prevents Lua from ever submitting CNN.
-        const bool betweenMovies=state==Status::complete && movie==1 && region==0 && step==38;
-        if (!betweenMovies) presentation.store(false);
-        watching.store(betweenMovies || !resource.release());
-        ReleaseSRWLockExclusive(&lock); return;
-    }
-    const auto now=GetTickCount64();
-    if (region!=0 || step!=38) { fail("world_changed"); ReleaseSRWLockExclusive(&lock); return; }
-    if (!frameReady.load()) { fail("frame_observer_unavailable"); ReleaseSRWLockExclusive(&lock); return; }
-    if (!uiReady.load()) { fail("movie_presentation_unavailable"); ReleaseSRWLockExclusive(&lock); return; }
-    if (!resolve()) { fail("native_api_unavailable"); ReleaseSRWLockExclusive(&lock); return; }
-    auto* manager=api.manager();
-    auto* decoder=api.decoder();
-    if (!manager || !decoder) { fail("player_unavailable"); ReleaseSRWLockExclusive(&lock); return; }
-    if (state==Status::queued) {
-        if (!resource.begin(assets[movie-1])) {
-            fail("resource_request_failed"); ReleaseSRWLockExclusive(&lock); return;
-        }
-        if (!resource.advance()) {
-            fail("surface_dependency_failed"); ReleaseSRWLockExclusive(&lock); return;
-        }
-        if (resource.state()==3 || resource.state()<0) {
-            fail("resource_load_failed"); ReleaseSRWLockExclusive(&lock); return;
-        }
-        if (!resource.ready()) {
-            if (now-began>30000) fail("resource_ready_timeout");
-            ReleaseSRWLockExclusive(&lock); return;
-        }
-        if (api.busy(manager)) {
-            if (now-began>30000) fail("player_busy_timeout");
-        } else if (!resource.prepare_surfaces()) {
-            if (now-began>30000) fail("surface_registration_timeout");
-        } else {
-            // Same acquire/play pairing as the authored pre-rendered component.
-            report("resource_ready");
-            decoderOwner=decoder; api.acquire(manager); acquired=true;
-            api.play(manager,assets[movie-1],0); state=Status::preparing; began=now;
-            presentation.store(true);
-            report("submitted");
-        }
-        ReleaseSRWLockExclusive(&lock); return;
-    }
-    if (decoder!=decoderOwner) { fail("decoder_owner_changed"); ReleaseSRWLockExclusive(&lock); return; }
-    const auto asset=field<std::uint32_t>(decoder,0x1B4);
-    const int decoderState=field<int>(decoder,0x1B0);
-    if (decoderState!=lastDecoderState) { report("decoder",decoderState); lastDecoderState=decoderState; }
-    const auto observed=playback.observe(asset==assets[movie-1],decoderState,api.busy(manager));
-    if (observed==Status::playing && state!=Status::playing) report("playing",decoderState);
-    // The direct video path has no type-6 source to emit a cinematic-skip incident.
-    // A foreground Escape press asks the original native movie player to stop; completion
-    // still requires the decoder's subsequent stopped/end receipt for this asset.
-    DWORD foregroundProcess{};
-    GetWindowThreadProcessId(GetForegroundWindow(),&foregroundProcess);
-    const bool escapeDown=(GetAsyncKeyState(VK_ESCAPE)&0x8000)!=0;
-    if (observed==Status::playing && foregroundProcess==GetCurrentProcessId()
-        && escapeDown && !escapeHeld) stopRequested=true;
-    escapeHeld=escapeDown;
-    if (stopRequested && asset==assets[movie-1]) {
-        api.stop(manager); stopRequested=false; report("stop_requested",decoderState);
-    }
-    if (observed==Status::complete) {
-        release();
-        if (movie==2) presentation.store(false);
-        state=Status::complete; watching.store(true); report("complete",decoderState);
-        if (movie==1) firstCompleted=owner;
-        else if (firstCompleted==owner) orbit_return::arm(owner);
-        // Final cleanup runs independently on subsequent frames. Native EOF,
-        // not global texture eviction, releases the mission's orbit handoff.
-    }
-    else if (observed==Status::failed) fail("decoder_failed_or_replaced");
-    else state=observed;
-    if ((state==Status::preparing && now-began>30000)
-        || (state==Status::playing && now-began>600000)) fail("playback_timeout");
-    ReleaseSRWLockExclusive(&lock);
-}
-}

+ 0 - 14
Sunrise/src/client/hooks/ember_movies/ember_movies.h

@@ -1,14 +0,0 @@
-#pragma once
-#include <cstdint>
-namespace sunrise::client::hooks::ember_movies {
-// Only Ember's two packaged pre-rendered bookends are admitted by this bridge.
-struct Owner { std::uint64_t session{}, generation{}; bool operator==(const Owner&) const = default; };
-enum class Status : std::uint8_t { absent, queued, preparing, playing, complete, failed };
-bool request(Owner owner, std::uint64_t request, unsigned index, bool stop) noexcept;
-Status status(Owner owner, unsigned index) noexcept;
-bool active() noexcept;
-bool presenting() noexcept;
-void ui_ready(bool ready) noexcept;
-void frame_ready(bool ready) noexcept;
-void poll(std::int32_t region, std::int32_t step) noexcept;
-}

+ 0 - 130
Sunrise/src/client/hooks/ember_movies/orbit_return.cpp

@@ -1,130 +0,0 @@
-#include "orbit_return.h"
-#include "orbit_rules.h"
-#include <Windows.h>
-#include <array>
-#include <atomic>
-#include <cstdio>
-#include <cstring>
-#include "../../patterns/image_scan.h"
-#include "../../patterns/signature_text.h"
-#include "../../../core/logging/log.h"
-#include "../../../server/bap/runtime.h"
-#include "../../../state/activity/runtime.h"
-
-namespace sunrise::client::hooks::ember_movies::orbit_return {
-namespace {
-using namespace patterns;
-using Accessor=void*(__fastcall*)();
-using Construct=void*(__fastcall*)(void*);
-using Clear=void(__fastcall*)();
-using Select=void(__fastcall*)(int,void*);
-using Commit=void(__fastcall*)(int);
-using Predicate=bool(__fastcall*)(int);
-using Activity=void*(__fastcall*)(std::uint16_t*);
-using Lifetime=int(__fastcall*)();
-using Step=void(__fastcall*)(void*,int,int);
-struct Api { Accessor manager{}; Construct construct{}, orbit{}; Clear clear{};
-    Select select{}; Commit commit{}; Predicate mode{}; Activity activity{};
-    Lifetime lifetime{}; Step step{}; } api;
-OrbitReturn progress{};
-Owner owner{};
-std::uint64_t arrival{};
-std::atomic_bool watching{};
-bool attempted{}, available{};
-thread_local bool inPoll{};
-template<class T> T field(const void* pointer,unsigned offset) {
-    T value{};std::memcpy(&value,static_cast<const std::byte*>(pointer)+offset,sizeof(value));return value;
-}
-void* call(std::byte* code,unsigned offset) {
-    if (!code || code[offset]!=std::byte{0xE8}) return nullptr;
-    return code+offset+5+field<std::int32_t>(code,offset+1);
-}
-void report(const char* result) {
-    std::array<char,192> line{};
-    std::snprintf(line.data(),line.size(),"ev=ember_orbit result=%s session=%llu generation=%llu",
-        result,static_cast<unsigned long long>(owner.session),static_cast<unsigned long long>(owner.generation));
-    core::log::write(core::log::Channel::client,core::log::Level::info,line.data());
-}
-bool resolve() {
-    if (attempted) return available;
-    attempted=true;
-    // E19D50's in-world return path uses these exact constructor/clear/select/commit calls.
-    constexpr auto returnSig=signature<signature_length("48 81 EC 58 01 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 84 24 40 01 00 00 B9 03 00 00 00 E8 ? ? ? ? 84 C0 74 7E")>(
-        "48 81 EC 58 01 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 84 24 40 01 00 00 B9 03 00 00 00 E8 ? ? ? ? 84 C0 74 7E");
-    constexpr auto activitySig=signature<signature_length("48 89 5C 24 18 55 48 8D AC 24 F0 FD FF FF 48 81 EC 10 03 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 00 02 00 00 48 8B D9 E8 ? ? ? ? 4C 8B C0 33 D2 8B C2")>(
-        "48 89 5C 24 18 55 48 8D AC 24 F0 FD FF FF 48 81 EC 10 03 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 00 02 00 00 48 8B D9 E8 ? ? ? ? 4C 8B C0 33 D2 8B C2");
-    constexpr auto lifetimeSig=signature<signature_length("48 83 EC 28 E8 ? ? ? ? 48 85 C0 74 0F 0F B6 00 3C FF 74 08 0F BE C0 48 83 C4 28 C3 83 C8 FF 48 83 C4 28 C3")>(
-        "48 83 EC 28 E8 ? ? ? ? 48 85 C0 74 0F 0F B6 00 3C FF 74 08 0F BE C0 48 83 C4 28 C3 83 C8 FF 48 83 C4 28 C3");
-    constexpr auto stepSig=signature<signature_length("48 89 5C 24 20 55 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 D0 BC FF FF B8 30 44 00 00 E8 ? ? ? ? 48 2B E0")>(
-        "48 89 5C 24 20 55 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 D0 BC FF FF B8 30 44 00 00 E8 ? ? ? ? 48 2B E0");
-    auto* native=scan_main_image_unique(returnSig,"ember_orbit_return");
-    api.mode=reinterpret_cast<Predicate>(call(native,0x1E));
-    api.manager=reinterpret_cast<Accessor>(call(native,0x2F));
-    api.construct=reinterpret_cast<Construct>(call(native,0x5B));
-    api.orbit=reinterpret_cast<Construct>(call(native,0x65));
-    api.clear=reinterpret_cast<Clear>(call(native,0x6A));
-    api.select=reinterpret_cast<Select>(call(native,0x7E));
-    api.commit=reinterpret_cast<Commit>(call(native,0x86));
-    api.activity=reinterpret_cast<Activity>(scan_main_image_unique(activitySig,"ember_orbit_activity"));
-    api.lifetime=reinterpret_cast<Lifetime>(scan_main_image_unique(lifetimeSig,"ember_orbit_lifetime"));
-    api.step=reinterpret_cast<Step>(scan_main_image_unique(stepSig,"ember_orbit_cleanup"));
-    available=api.mode && api.manager && api.construct && api.orbit && api.clear && api.select
-        && api.commit && api.activity && api.lifetime && api.step;
-    return available;
-}
-bool select_orbit() {
-    // Native selection is 0x118 bytes plus the constructor's ownership byte at +118.
-    alignas(16) std::array<std::byte,0x120> selection{};
-    api.construct(selection.data());
-    api.orbit(selection.data());
-    // Refuse a missing catalog/default or a non-orbit default before changing selection.
-    if (field<std::uint8_t>(selection.data(),0)!=2 || field<std::uint16_t>(selection.data(),2)!=0)
-        return false;
-    api.clear();
-    api.select(0,selection.data());
-    api.commit(2);
-    return true;
-}
-}
-void arm(Owner next) noexcept {
-    owner=next; arrival=state::activity::world_arrival_revision();
-    progress.arm(GetTickCount64());watching.store(true);report("awaiting_completion");
-}
-bool active() noexcept { return watching.load(); }
-void poll(int region,int step) noexcept {
-    if (!watching.load() || inPoll) return;
-    inPoll=true;
-    struct Reset { ~Reset(){inPoll=false;} } reset;
-    if (!resolve()) { watching.store(false);report("signature_failed");return; }
-    // This runs after the movie lock is released; never take the BAP lock from a movie request.
-    server::bap::CurrentActivityLinkView link{};
-    const bool sameWorld=region==0 && arrival==state::activity::world_arrival_revision();
-    const bool exact=server::bap::current_activity_link_view(region,link)
-        && link.binding.sessionId==owner.session && link.activityClientGeneration==owner.generation
-        && sameWorld;
-    std::uint16_t selected=0xFFFF;
-    auto* manager=api.manager();
-    const bool ready=manager && api.mode(3);
-    if (ready) api.activity(&selected);
-    const bool ember=exact && selected==link.binding.destination.activityIndex;
-    const bool complete=exact && api.lifetime()==6;
-    const auto action=progress.observe(GetTickCount64(),{exact,ember,selected==0,complete,ready,
-        region,step,manager?field<int>(manager,0x3A0):-1,sameWorld});
-    switch(action) {
-    case OrbitAction::select:
-        report("completion_accepted");
-        if (select_orbit()) report("selection_queued");
-        else { progress.stage=OrbitStage::finished;report("orbit_selection_unavailable"); }
-        break;
-    case OrbitAction::cleanup:
-        // The destination must already read back as orbit. Use native deferred cleanup;
-        // it retires the world on its own tick, outside the video decoder callback.
-        api.step(manager,28,309);report("cleanup_requested");break;
-    case OrbitAction::orbitSetup: report("orbit_setup");break;
-    case OrbitAction::canceled: report("canceled_world_or_owner_changed");break;
-    case OrbitAction::timedOut: report("transition_timeout");break;
-    default: break;
-    }
-    watching.store(progress.active());
-}
-}

+ 0 - 7
Sunrise/src/client/hooks/ember_movies/orbit_return.h

@@ -1,7 +0,0 @@
-#pragma once
-#include "ember_movies.h"
-namespace sunrise::client::hooks::ember_movies::orbit_return {
-void arm(Owner owner) noexcept;
-bool active() noexcept;
-void poll(int region, int step) noexcept;
-}

+ 0 - 44
Sunrise/src/client/hooks/ember_movies/orbit_rules.h

@@ -1,44 +0,0 @@
-#pragma once
-#include <cstdint>
-namespace sunrise::client::hooks::ember_movies {
-enum class OrbitAction { none, select, cleanup, orbitSetup, canceled, timedOut };
-enum class OrbitStage { idle, completion, selection, leaving, finished };
-struct OrbitObservation {
-    bool exactOwner{}, emberSelected{}, orbitSelected{}, complete{}, ready{};
-    int region{-1}, step{-1}, pendingStep{-1};
-    bool sameWorld{};
-};
-struct OrbitReturn {
-    OrbitStage stage{OrbitStage::idle};
-    std::uint64_t began{};
-    void arm(std::uint64_t now) { stage=OrbitStage::completion; began=now; }
-    bool active() const { return stage!=OrbitStage::idle && stage!=OrbitStage::finished; }
-    OrbitAction observe(std::uint64_t now, const OrbitObservation& s) {
-        if (!active()) return OrbitAction::none;
-        auto end=[this](OrbitAction a) { stage=OrbitStage::finished; return a; };
-        if (now-began>90000) return end(OrbitAction::timedOut);
-        if (stage==OrbitStage::completion) {
-            if (!s.exactOwner || !s.emberSelected || s.region!=0 || s.step!=38)
-                return end(OrbitAction::canceled);
-            // Return on the first ready frame after completion, with no banner delay.
-            if (!s.complete || !s.ready || s.pendingStep!=38) return OrbitAction::none;
-            stage=OrbitStage::selection;
-            return OrbitAction::select;
-        }
-        if (s.step==29 && s.orbitSelected) return end(OrbitAction::orbitSetup);
-        if (stage==OrbitStage::selection) {
-            // Native selection/commit can initiate cleanup itself. Never override it.
-            if (s.step!=38) { stage=OrbitStage::leaving; return OrbitAction::none; }
-            if (!s.sameWorld || (!s.emberSelected && !s.orbitSelected))
-                return end(OrbitAction::canceled);
-            if (s.orbitSelected && s.ready && s.pendingStep==38) {
-                stage=OrbitStage::leaving;
-                return OrbitAction::cleanup;
-            }
-        } else if (s.step==38 && !s.sameWorld) return end(OrbitAction::canceled);
-        // Cleanup can temporarily have neither an ActivityClient nor a selected activity.
-        // Keep observing that native transition; absence is not an arrival or a failure.
-        return OrbitAction::none;
-    }
-};
-}

+ 0 - 29
Sunrise/src/client/hooks/ember_movies/playback_rules.h

@@ -1,29 +0,0 @@
-#pragma once
-#include "ember_movies.h"
-namespace sunrise::client::hooks::ember_movies {
-// E15D50's gameplay choices -> native cinematic_overlay. Category 4 alone
-// does not distinguish playback from loading: 1312540 maps 21h -> window 26
-// (cinematic_overlay), but 22h -> window 29 (loading) at 13126E0.
-constexpr int movie_ui_state(int requested,bool presenting) noexcept {
-    return presenting && (requested==0x2B || requested==0x2D || requested==0x2F)
-        ? 0x21 : requested;
-}
-constexpr bool can_chain_movies(Owner current,Owner next,Owner firstCompleted,
-                                 unsigned previous,unsigned requested,Status state) noexcept {
-    return current.session && current.generation && current==next && current==firstCompleted
-        && previous==1 && requested==2 && state==Status::complete;
-}
-// A queued request and decoder preparation do not prove rendered playback.
-struct Playback {
-    bool seen{};
-    Status observe(bool exactAsset, int decoderState, bool busy) noexcept {
-        if (!exactAsset) return seen ? Status::failed : Status::preparing;
-        if (decoderState==5) seen=true;
-        if (!busy) {
-            if (decoderState!=0 && decoderState!=6) return Status::failed;
-            return seen ? Status::complete : Status::preparing;
-        }
-        return seen ? Status::playing : Status::preparing;
-    }
-};
-}

+ 0 - 29
Sunrise/src/client/hooks/ember_movies/readiness_rules.h

@@ -1,29 +0,0 @@
-#pragma once
-#include <cstdint>
-#include <array>
-namespace sunrise::client::hooks::ember_movies {
-// Native 426920 selects 1 for ordinary tags, 2 only for type_info & F000 == 2000.
-// The four movie metadata records below are all ordinary tags (100A/103B/1019).
-constexpr std::uint32_t movie_resource_kind = 1;
-constexpr std::array<std::uint32_t,4> movie_metadata(std::uint32_t asset) noexcept {
-    if (asset!=0x80BCA001U && asset!=0x80BCA003U) return {};
-    return {asset,asset-1,asset==0x80BCA001U ? 0x80B9EB33U : 0x80B9EB34U,0x80BCA032U};
-}
-constexpr std::uint32_t movie_stream(std::uint32_t asset) noexcept {
-    return asset==0x80BCA001U ? 0x80BCA034U : asset==0x80BCA003U ? 0x80C7C000U : 0xFFFFFFFFU;
-}
-// Stream datums hold size plus a packed package offset/patch id, not a CPU blob.
-// Native 3591B0 initializes this mapping without allocating the whole media file.
-constexpr bool movie_stream_ready(std::uint32_t size, std::uint32_t typeInfo,
-                                   std::uint64_t location) noexcept {
-    return (size&0xC0000000U)==0xC0000000U && (size&0x3FFFFFFFU)>0
-        && ((typeInfo>>6)&0x3FU)==24 && (typeInfo&0x30000U)==0x10000U
-        && location<=0xFFFFFFFFULL && (location&0xFFFFFF00ULL)!=0;
-}
-constexpr bool movie_resources_ready(int rootState,std::uint32_t asset,bool wrapperResident,
-    std::uint32_t header,bool headerResident,std::uint32_t media) noexcept {
-    return rootState==2 && (asset==0x80BCA001U || asset==0x80BCA003U)
-        && wrapperResident && header==asset-1 && headerResident && media!=0xFFFFFFFFU;
-}
-constexpr bool resource_can_release(int state) noexcept { return state==2 || state==3; }
-}

+ 0 - 236
Sunrise/src/client/hooks/ember_movies/resources.cpp

@@ -1,236 +0,0 @@
-#include "resources.h"
-#include "readiness_rules.h"
-#include "surface_rules.h"
-#include <Windows.h>
-#include <cstring>
-#include "../../patterns/image_scan.h"
-#include "../../patterns/signature_text.h"
-#include "../../../core/logging/log.h"
-namespace sunrise::client::hooks::ember_movies {
-namespace {
-using namespace patterns;
-using Manager=void*(__fastcall*)();
-using Create=std::uint32_t*(__fastcall*)(void*,std::uint32_t*,int,int,int,const char*);
-using Add=void(__fastcall*)(void*,const std::uint32_t*);
-using Submit=void(__fastcall*)(void*,std::uint32_t);
-using State=int(__fastcall*)(void*);
-Manager manager{}; Create create{}; Add add{}; Submit submit{},destroy{}; State status{};
-std::uintptr_t tableGlobal{};
-using PublishSurfaces=void(__fastcall*)();
-PublishSurfaces publishSurfaces{};
-std::uintptr_t surfaceRegistrations{};
-INIT_ONCE init=INIT_ONCE_STATIC_INIT;
-bool available{};
-template<class T> T read(std::uintptr_t at) { T value{};std::memcpy(&value,reinterpret_cast<void*>(at),sizeof(value));return value; }
-void* call(std::byte* code,unsigned offset) {
-    if (!code || code[offset]!=std::byte{0xE8}) return nullptr;
-    return code+offset+5+read<std::int32_t>(reinterpret_cast<std::uintptr_t>(code+offset+1));
-}
-bool resolve_native() {
-    // B46E10 supplies the root lifecycle API. Its startup assets use kind 2;
-    // movies use kind 1, as selected from their package type_info by native 426920.
-    constexpr auto loadSig=signature<signature_length("40 53 56 57 48 81 EC 50 01 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 84 24 40 01 00 00 48 63 DA 48 8B F1")>(
-        "40 53 56 57 48 81 EC 50 01 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 84 24 40 01 00 00 48 63 DA 48 8B F1");
-    // B44020: inspect the completed root, then release it; no global I/O drain needed.
-    constexpr auto endSig=signature<signature_length("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 56 41 57 48 83 EC 20 48 63 EA 4C 8B F1 E8")>(
-        "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 56 41 57 48 83 EC 20 48 63 EA 4C 8B F1 E8");
-    // 1202B00 publishes the registered surface stack; normal world activation
-    // calls it at B5F4CE. Direct movies keep the world, so perform that step here.
-    constexpr auto surfaceSig=signature<signature_length("8B 05 ? ? ? ? 48 8D 15 ? ? ? ? C7 05 ? ? ? ? FF FF FF FF 85 C0 74 0E FF C8 48 98 8B 4C 82 04 89 0D")>(
-        "8B 05 ? ? ? ? 48 8D 15 ? ? ? ? C7 05 ? ? ? ? FF FF FF FF 85 C0 74 0E FF C8 48 98 8B 4C 82 04 89 0D");
-    auto* load=scan_main_image_unique(loadSig,"ember_resource_load");
-    auto* end=scan_main_image_unique(endSig,"ember_resource_release");
-    auto* surfaces=scan_main_image_unique(surfaceSig,"ember_movie_surfaces");
-    auto mgr=reinterpret_cast<Manager>(call(load,0x96));
-    create=reinterpret_cast<Create>(call(load,0xD1));
-    add=reinterpret_cast<Add>(call(load,0x14C));
-    submit=reinterpret_cast<Submit>(call(load,0x157));
-    status=reinterpret_cast<State>(call(end,0x85));
-    destroy=reinterpret_cast<Submit>(call(end,0x9F));
-    if (!mgr || !create || !add || !submit || !status || !destroy || !end || !surfaces
-        || end[0x2E]!=std::byte{0x48} || end[0x2F]!=std::byte{0x8B} || end[0x30]!=std::byte{0x05}) return false;
-    tableGlobal=reinterpret_cast<std::uintptr_t>(end+0x35)+read<std::int32_t>(reinterpret_cast<std::uintptr_t>(end+0x31));
-    surfaceRegistrations=reinterpret_cast<std::uintptr_t>(surfaces+13)
-        +read<std::int32_t>(reinterpret_cast<std::uintptr_t>(surfaces+9));
-    publishSurfaces=reinterpret_cast<PublishSurfaces>(surfaces);
-    manager=mgr; return true;
-}
-BOOL CALLBACK initialize(PINIT_ONCE,void*,void**) { available=resolve_native();return TRUE; }
-bool resolve() { InitOnceExecuteOnce(&init,initialize,nullptr,nullptr);return available; }
-std::uintptr_t row(std::uint32_t handle) {
-    if (handle==0xFFFFFFFFU || !tableGlobal) return 0;
-    const auto high=static_cast<std::uint32_t>(static_cast<std::int32_t>(handle)>>13);
-    const auto pool=(high&0xFFFFU)&((static_cast<std::uint64_t>(high)|0xFFC0000ULL)>>18);
-    const auto head=read<std::uintptr_t>(tableGlobal);
-    if (!head) return 0;
-    const auto table=read<std::uintptr_t>(head);
-    return table ? table+64*pool : 0;
-}
-std::uintptr_t datum(std::uint32_t handle) {
-    const auto pool=row(handle);if (!pool) return 0;
-    const auto storage=read<std::uintptr_t>(pool+8);
-    const auto stride=read<std::uint32_t>(pool+48);
-    if (!storage || !stride) return 0;
-    return storage+stride*static_cast<std::uintptr_t>(handle&0x1FFFU);
-}
-void* blob(std::uint32_t handle,std::uint32_t expectedClass=0) {
-    const auto item=datum(handle);if (!item) return nullptr;
-    // Unloaded package entries contain FEFE free-list markers, not a definition.
-    if (expectedClass && read<std::uint32_t>(item)!=expectedClass) return nullptr;
-    const auto mask=static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(row(handle)+52)));
-    const auto at=item-(read<std::uintptr_t>(item+8)&mask);
-    return reinterpret_cast<void*>(at);
-}
-bool stream_ready(std::uint32_t handle) {
-    const auto item=datum(handle);if (!item) return false;
-    const auto mask=static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(row(handle)+52)));
-    // Never dereference this value: it encodes the file offset and patch id.
-    const auto location=item-(read<std::uintptr_t>(item+8)&mask);
-    return movie_stream_ready(read<std::uint32_t>(item),read<std::uint32_t>(item+4),location);
-}
-int root_state(std::uint32_t handle) {
-    if (handle==0xFFFFFFFFU || !status) return -1;
-    auto* root=blob(handle);return root ? status(root) : -1;
-}
-bool definitions_ready(bool requireBuffers) {
-    for (unsigned i=0;i<movie_surface_definitions.size();++i) {
-        const auto tag=movie_surface_definitions[i];
-        const auto item=datum(tag);
-        if (!item) return false;
-        const auto at=reinterpret_cast<std::uintptr_t>(blob(tag));
-        if (!movie_definition_resident(read<std::uint32_t>(item),read<std::uint32_t>(item+4),at)
-            || !movie_definition_matches(i,read<std::uint8_t>(at),read<std::uint8_t>(at+6))) return false;
-        if (requireBuffers && read<std::uintptr_t>(at+8)==0) return false;
-    }
-    return true;
-}
-}
-bool sunburn_resident() noexcept {
-    if (!resolve()) return false;
-    __try { return blob(0x80B82489U,0x80809C0FU)!=nullptr; }
-    __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-bool MovieResource::begin(std::uint32_t asset) noexcept {
-    if (held()) {
-        if (retiringSurfaces_ || (asset!=0x80BCA001U && asset!=0x80BCA003U)) return false;
-        asset_=asset;return true; // Both movies share one retained resource set.
-    }
-    if ((asset!=0x80BCA001U && asset!=0x80BCA003U) || !resolve()) return false;
-    auto* mgr=manager(); if (!mgr) return false;
-    create(mgr,&root_,8,2,0,"mission_ember_movie");
-    if (!held()) return false;
-    auto* root=blob(root_);if (!root) return false;
-    retiringSurfaces_=false;
-    // This root contains definitions only. The catalog belongs to the dependent
-    // root: retaining it while waiting for its containers to unload is circular.
-    // Native registration 1204163 immediately reads the referenced definition.
-    // A single flat batch cannot order that callback after the definition load.
-    for (const auto tag : movie_surface_definitions) {
-        const std::uint32_t request[]{movie_resource_kind,tag};
-        add(root,request);
-    }
-    submit(mgr,root_);asset_=asset;
-    core::log::writef(core::log::Channel::client,core::log::Level::info,
-        "ev=ember_movie result=resource_requested asset=%08X root=%08X kind=1 definitions=6",asset_,root_);
-    return true;
-}
-bool MovieResource::advance() noexcept {
-    if (!held() || retiringSurfaces_) return false;
-    if (surfaces_!=0xFFFFFFFFU) return true;
-    __try {
-        const auto value=root_state(root_);
-        if (value==3 || value<0) return false;
-        if (value!=2) return true;
-        if (!definitions_ready(false)) return false;
-        auto* mgr=manager();
-        create(mgr,&surfaces_,8,2,0,"mission_ember_movie_surfaces");
-        if (surfaces_==0xFFFFFFFFU) return false;
-        auto* root=blob(surfaces_);if (!root) return false;
-        // Preload both streams' compact mappings and metadata. Native EOF for
-        // STM can then hand off to CNN without releasing their shared textures.
-        for (auto asset : {0x80BCA001U,0x80BCA003U}) {
-            for (auto tag : movie_metadata(asset)) {
-                if (asset==0x80BCA003U && tag==0x80BCA032U) continue;
-                const std::uint32_t request[]{movie_resource_kind,tag};add(root,request);
-            }
-            const std::uint32_t request[]{movie_resource_kind,movie_stream(asset)};add(root,request);
-        }
-        // 1204581 binds each raw buffer to definition+8. Both the buffer callback
-        // and the container registration require the retained definition first.
-        for (auto tag : movie_surface_buffers) {
-            const std::uint32_t request[]{movie_resource_kind,tag};add(root,request);
-        }
-        for (auto tag : movie_surfaces) {
-            const std::uint32_t request[]{movie_resource_kind,tag};add(root,request);
-        }
-        submit(mgr,surfaces_);
-        core::log::writef(core::log::Channel::client,core::log::Level::info,
-            "ev=ember_movie result=surface_dependencies_ready asset=%08X root=%08X buffers=6 containers=6",asset_,surfaces_);
-        return true;
-    } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-int MovieResource::state() const noexcept {
-    if (!held() || !status) return -1;
-    __try {
-        const auto value=root_state(root_);
-        return value!=2 ? value : surfaces_==0xFFFFFFFFU ? 1 : root_state(surfaces_);
-    }
-    __except(EXCEPTION_EXECUTE_HANDLER) { return -1; }
-}
-bool MovieResource::ready() const noexcept {
-    if (state()!=2) return false;
-    __try {
-        auto* wrapper=blob(asset_,0x80808495U);
-        if (!wrapper) return false;
-        const auto header=read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(wrapper)+8);
-        if (header!=asset_-1) return false;
-        auto* info=blob(header,0x80808499U);
-        const auto metadata=movie_metadata(asset_);
-        if (read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(wrapper)+12)!=metadata[2]
-            || !blob(metadata[2],0x80809A88U) || !blob(metadata[3],0x80806B8FU)) return false;
-        const auto media=info ? read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(info)+0x18) : 0xFFFFFFFFU;
-        return movie_resources_ready(2,asset_,true,header,info!=nullptr,media)
-            && media==movie_stream(asset_) && stream_ready(media);
-    } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-bool MovieResource::prepare_surfaces() noexcept {
-    if (state()!=2 || !publishSurfaces || !surfaceRegistrations) return false;
-    __try {
-        if (!definitions_ready(true)) return false;
-        for (unsigned i=0;i<movie_surfaces.size();++i) {
-            auto* container=blob(movie_surfaces[i],0x80806B91U);
-            if (!container || read<std::uint32_t>(reinterpret_cast<std::uintptr_t>(container))
-                !=movie_surface_definitions[i]) return false;
-        }
-        auto rows=read<SurfaceRegistrations>(surfaceRegistrations);
-        if (!movie_surfaces_registered(rows)) return false;
-        if (!movie_surfaces_selected(rows)) publishSurfaces();
-        if (!movie_surfaces_selected(read<SurfaceRegistrations>(surfaceRegistrations))) return false;
-        core::log::writef(core::log::Channel::client,core::log::Level::info,
-            "ev=ember_movie result=surfaces_selected asset=%08X count=6",asset_);
-        return true;
-    } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-bool MovieResource::release() noexcept {
-    if (!held()) return true;
-    __try {
-        if (surfaces_!=0xFFFFFFFFU) {
-            if (!resource_can_release(root_state(surfaces_))) return false;
-            destroy(manager(),surfaces_);surfaces_=0xFFFFFFFFU;retiringSurfaces_=true;
-            core::log::writef(core::log::Channel::client,core::log::Level::info,
-                "ev=ember_movie result=surfaces_retiring asset=%08X",asset_);
-            return false; // Native unregister/release callbacks run asynchronously.
-        }
-        if (retiringSurfaces_) {
-            if (!movie_surfaces_absent(read<SurfaceRegistrations>(surfaceRegistrations))) return false;
-            for (auto tag : movie_surfaces) if (blob(tag,0x80806B91U)) return false;
-            for (auto tag : movie_surface_buffers) if (blob(tag)) return false;
-        }
-        if (!resource_can_release(root_state(root_))) return false;
-        destroy(manager(),root_);
-        core::log::writef(core::log::Channel::client,core::log::Level::info,
-            "ev=ember_movie result=resource_released asset=%08X root=%08X",asset_,root_);
-        root_=0xFFFFFFFFU;asset_=0;retiringSurfaces_=false;return true;
-    } __except(EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-}

+ 0 - 19
Sunrise/src/client/hooks/ember_movies/resources.h

@@ -1,19 +0,0 @@
-#pragma once
-#include <cstdint>
-namespace sunrise::client::hooks::ember_movies {
-bool sunburn_resident() noexcept;
-// Load definitions before the callbacks which dereference them. Release the
-// dependent containers/buffers before releasing the definitions they use.
-class MovieResource {
-    std::uint32_t root_{0xFFFFFFFFU}, surfaces_{0xFFFFFFFFU}, asset_{};
-    bool retiringSurfaces_{};
-public:
-    bool begin(std::uint32_t asset) noexcept;
-    bool advance() noexcept; // nonblocking: start dependent load only after definitions settle
-    int state() const noexcept; // native root: 1 pending, 2 ready, 3 failed
-    bool ready() const noexcept;
-    bool prepare_surfaces() noexcept; // frame-owned publication after the native player is idle
-    bool release() noexcept; // false while pending: never block the frame draining I/O
-    bool held() const noexcept { return root_ != 0xFFFFFFFFU; }
-};
-}

+ 0 - 8
Sunrise/src/client/hooks/ember_movies/sunburn_rules.h

@@ -1,8 +0,0 @@
-#pragma once
-#include <cstdint>
-namespace sunrise::client::hooks::ember_movies {
-constexpr bool ember_burn_source(std::uint32_t tag,std::uint32_t schema,
-    std::uint64_t offset,std::uint32_t asset) noexcept {
-    return tag==0x80B3C0C6U && schema==0x80809540U && offset==0xAC8U && asset==0x80C1D9E0U;
-}
-}

+ 0 - 60
Sunrise/src/client/hooks/ember_movies/surface_rules.h

@@ -1,60 +0,0 @@
-#pragma once
-#include <array>
-#include <cstdint>
-namespace sunrise::client::hooks::ember_movies {
-// 80BCA032 names the six Y/U/V double-buffer surface containers. Loading that
-// list alone does not retain/activate its individual surface containers.
-constexpr std::array<std::uint32_t,6> movie_surfaces{
-    0x80BCA022U,0x80BCA025U,0x80BCA028U,0x80BCA02BU,0x80BCA02EU,0x80BCA031U};
-constexpr std::array<std::uint32_t,6> movie_surface_definitions{
-    0x80BCA021U,0x80BCA024U,0x80BCA026U,0x80BCA029U,0x80BCA02CU,0x80BCA02FU};
-constexpr std::array<std::uint32_t,6> movie_surface_buffers{
-    0x80BCA020U,0x80BCA023U,0x80BCA027U,0x80BCA02AU,0x80BCA02DU,0x80BCA030U};
-// Type-19 definitions expand from eight package bytes to a 16-byte native
-// allocation. The raw buffer loader later fills definition+8 (native 1204581).
-constexpr bool movie_definition_resident(std::uint32_t size, std::uint32_t typeInfo,
-                                         std::uintptr_t pointer) noexcept {
-    return size==0xC0000010U
-        && (typeInfo&0x3FFFFU)==0x44FBU && pointer!=0;
-}
-constexpr bool movie_definition_matches(unsigned index, std::uint8_t slot,
-                                        std::uint8_t format) noexcept {
-    return index<6 && slot==index+1 && format==(index<2 ? 0x7F : 0x23);
-}
-struct SurfaceRegistration {
-    std::uint32_t count{}, entries[3]{}, selected{0xFFFFFFFFU};
-};
-static_assert(sizeof(SurfaceRegistration)==20);
-using SurfaceRegistrations=std::array<SurfaceRegistration,8>;
-constexpr bool movie_surfaces_absent(const SurfaceRegistrations& rows) noexcept {
-    for (const auto& row : rows) {
-        if (row.count>3) return false;
-        for (auto definition : movie_surface_definitions) {
-            if (row.selected==definition) return false;
-            for (unsigned i=0;i<row.count;++i)
-                if (row.entries[i]==definition) return false;
-        }
-    }
-    return true;
-}
-constexpr std::uint32_t selected_surface(const SurfaceRegistration& r) noexcept {
-    return r.count>0 && r.count<=3 ? r.entries[r.count-1] : 0xFFFFFFFFU;
-}
-constexpr bool movie_surfaces_registered(const SurfaceRegistrations& rows) noexcept {
-    for (unsigned i=0;i<rows.size();++i) {
-        if (rows[i].count>3) return false;
-        const auto top=selected_surface(rows[i]);
-        // The native publisher updates all eight slots. Do not change unrelated
-        // pending slot 0/7 selections while preparing an Ember movie.
-        if (i==0 || i==7) { if (rows[i].selected!=top) return false; }
-        else if (top!=movie_surface_definitions[i-1]) return false;
-    }
-    return true;
-}
-constexpr bool movie_surfaces_selected(const SurfaceRegistrations& rows) noexcept {
-    if (!movie_surfaces_registered(rows)) return false;
-    for (unsigned i=1;i<=6;++i)
-        if (rows[i].selected!=movie_surface_definitions[i-1]) return false;
-    return true;
-}
-}

+ 0 - 99
Sunrise/src/client/hooks/harvester_drop/harvester_drop_experiment.cpp

@@ -1,99 +0,0 @@
-#include "harvester_drop_experiment.h"
-
-#include <Windows.h>
-#include <cstddef>
-#include <cstdint>
-#include <cstring>
-
-#include "../../../core/logging/log.h"
-#include "../../../core/settings/settings.h"
-#include "../../hooking/detour.h"
-
-namespace sunrise::client::hooks::harvester_drop {
-namespace {
-hooking::detour::Handle g_hook{};
-std::uintptr_t g_base{};
-using SetStage = std::intptr_t (__fastcall*)(void*, int);
-using SetChannel = std::intptr_t (__fastcall*)(std::uint32_t, const std::uint32_t*, const float*);
-// 80FE22CB source+0x1F0 names the model channel; FNV-1("doors").
-constexpr std::uint32_t kDoorsChannel = 0x80296344;
-
-template<class T> T read(std::uintptr_t address) noexcept {
-    T value{};
-    std::memcpy(&value, reinterpret_cast<const void*>(address), sizeof(value));
-    return value;
-}
-
-// Resolve the source exactly as 1029330 does. The runtime component is a separate
-// allocation: offsets into an entity's source slab are not runtime pointers.
-bool is_harvester_delivery(std::uintptr_t runtime) noexcept {
-    __try {
-        const auto handle = read<std::uint32_t>(runtime);
-        if (handle == 0xffffffff) return false;
-        const auto high = static_cast<std::uint32_t>(static_cast<std::int32_t>(handle) >> 13);
-        const auto pool = (high & 0xffffU) & ((static_cast<std::uint64_t>(high) | 0xffc0000ULL) >> 18);
-        const auto table = read<std::uintptr_t>(read<std::uintptr_t>(g_base + 0x2439C70));
-        const auto record = table + pool * 64;
-        const auto item = read<std::uintptr_t>(record + 8)
-            + read<std::uint32_t>(record + 48) * static_cast<std::uintptr_t>(handle & 0x1fff);
-        const auto mask = static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(record + 52)));
-        const auto slab = item - (read<std::uintptr_t>(item + 8) & mask);
-        const auto source = slab + read<std::uintptr_t>(runtime + 8);
-        // The native reference resolves the delivery RESOURCE, not the owning
-        // actor's source slab. Its root is a resource header (size/offsets), so
-        // checking slab+4 for the actor tag rejected every real delivery.
-        return read<std::uint32_t>(source) == 0x80C0E5D2
-            && read<std::uint32_t>(source + 4) == 0x8080670A;
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-
-// The live capture proved all four delivery action bindings are empty. Drive the
-// separately authored model channel instead, using the native named-scalar route
-// used by actor command A9BFA0. Match only this delivery resource, never other AI.
-void set_doors(void* self, int stage) noexcept {
-    const auto runtime = reinterpret_cast<std::uintptr_t>(self);
-    if (!self || (stage != 0 && stage != 1) || !is_harvester_delivery(runtime)) return;
-    __try {
-        const auto entity = read<std::uint32_t>(runtime + 44);
-        const auto priorStage = read<std::uint8_t>(runtime + 480);
-        if (entity == 0xffffffff || priorStage == stage) return;
-        const float value = stage == 1 ? 1.0f : 0.0f;
-        alignas(16) const float values[4]{value, value, value, value};
-        reinterpret_cast<SetChannel>(g_base + 0x576420)(entity, &kDoorsChannel, values);
-        core::log::writef(core::log::Channel::client, core::log::Level::info,
-            "ev=harvester_doors stage=%d previous=%u entity=%08x channel=%08x value=%.1f result=requested",
-            stage, static_cast<unsigned>(priorStage), entity, kDoorsChannel, static_cast<double>(value));
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        core::log::write(core::log::Channel::client, core::log::Level::error,
-            "ev=harvester_doors result=exception");
-    }
-}
-
-std::intptr_t __fastcall set_stage(void* self, int stage) noexcept {
-    set_doors(self, stage);
-    return reinterpret_cast<SetStage>(g_hook.original)(self, stage);
-}
-}
-
-bool install() noexcept {
-    if (g_hook.attached) return install_exit();
-    if (!core::settings::get().server.activation.missionScripting) return true;
-    g_base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    constexpr unsigned char stagePrefix[]{0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,
-        0x48,0x63,0xda,0x48,0x8b,0xf9,0x88,0x99,0xe0,0x01,0x00,0x00};
-    constexpr unsigned char channelPrefix[]{0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,
-        0x55,0x57,0x41,0x56,0x48,0x8d,0xac,0x24,0x70,0xf4,0xff,0xff,0x48,0x81,0xec,0x90,0x0c,0x00,0x00};
-    if (!g_base || std::memcmp(reinterpret_cast<void*>(g_base + 0x102A5B0), stagePrefix, sizeof(stagePrefix))
-        || std::memcmp(reinterpret_cast<void*>(g_base + 0x576420), channelPrefix, sizeof(channelPrefix))) {
-        core::log::write(core::log::Channel::client, core::log::Level::warn,
-            "ev=harvester_doors stage=install result=build_mismatch");
-        return false;
-    }
-    const bool ok = hooking::detour::install({reinterpret_cast<void*>(g_base + 0x102A5B0),
-        reinterpret_cast<void*>(&set_stage)}, g_hook);
-    core::log::writef(core::log::Channel::client, core::log::Level::info,
-        "ev=harvester_doors stage=install result=%s", ok ? "ok" : "failed");
-    const bool exitOk = install_exit();
-    return ok && exitOk;
-}
-}

+ 0 - 7
Sunrise/src/client/hooks/harvester_drop/harvester_drop_experiment.h

@@ -1,7 +0,0 @@
-#pragma once
-
-namespace sunrise::client::hooks::harvester_drop {
-// Experimental model doors channel: open during native delivery, close on completion.
-[[nodiscard]] bool install_exit() noexcept;
-bool install() noexcept;
-}

+ 0 - 135
Sunrise/src/client/hooks/harvester_drop/harvester_exit.cpp

@@ -1,135 +0,0 @@
-#include "harvester_drop_experiment.h"
-#include <Windows.h>
-#include <atomic>
-#include <cstdint>
-#include <cstring>
-#include "../../../core/logging/log.h"
-#include "../../hooking/detour.h"
-
-namespace sunrise::client::hooks::harvester_drop {
-namespace {
-std::uintptr_t base{};
-hooking::detour::Handle lookupHook{}, startHook{}, stopHook{};
-using Lookup = bool (__fastcall*)(std::uint32_t, const std::uint32_t*, const std::uint32_t*, int*, int*);
-using Start = std::intptr_t (__fastcall*)(void*, int);
-using Stop = bool (__fastcall*)(void*, int);
-std::atomic<unsigned> lookupReports{};
-constexpr std::uint32_t none = 0x811c9dc5;
-template<class T> T read(std::uintptr_t at) noexcept {
-    T v{}; std::memcpy(&v, reinterpret_cast<const void*>(at), sizeof(v)); return v;
-}
-std::uintptr_t resolve(std::uint32_t handle, std::uintptr_t offset = 0) noexcept {
-    if (handle == 0xffffffff) return 0;
-    const auto high = static_cast<std::uint32_t>(static_cast<std::int32_t>(handle) >> 13);
-    const auto pool = (high & 0xffffU) & ((static_cast<std::uint64_t>(high) | 0xffc0000ULL) >> 18);
-    const auto table = read<std::uintptr_t>(read<std::uintptr_t>(base + 0x2439c70));
-    const auto record = table + pool * 64;
-    const auto item = read<std::uintptr_t>(record + 8)
-        + read<std::uint32_t>(record + 48) * static_cast<std::uintptr_t>(handle & 0x1fff);
-    const auto mask = static_cast<std::uintptr_t>(static_cast<std::intptr_t>(read<std::int32_t>(record + 52)));
-    return item - (read<std::uintptr_t>(item + 8) & mask) + offset;
-}
-bool exit_name(std::uint32_t name) noexcept {
-    return name == 0x7b0d3643 || name == 0x7d0d39a9 || name == 0x820d414b;
-}
-// A889E0's exact native table: group rows are 32 bytes; variant names are u32.
-// Resolve the owner from the live actor metadata rather than inventing a group hash.
-// This extension is deliberately limited to an unnamed Harvester exit request.
-bool find_group(std::uint32_t actor, std::uint32_t variant, std::uint32_t& group) noexcept {
-    __try {
-        if (actor == 0xffffffff || !exit_name(variant)) return false;
-        const auto record = read<std::uintptr_t>(base + 0x1f9d7f8)
-            + read<std::uint32_t>(base + 0x1f9d800) * static_cast<std::uintptr_t>(actor & 0x1fff);
-        const auto entity = read<std::uint32_t>(record + 76);
-        if (entity == 0xffffffff) return false;
-        const auto entityRecord = read<std::uintptr_t>(base + 0x1f93428)
-            + read<std::uint32_t>(base + 0x1f93430) * static_cast<std::uintptr_t>(entity & 0x1fff);
-        const auto source = resolve(read<std::uint32_t>(entityRecord + 76));
-        if (!source || read<std::uint32_t>(source + 4) != 0x80fe22fc) return false;
-        const auto metadata = resolve(read<std::uint32_t>(record + 44));
-        if (!metadata) return false;
-        const auto count = read<std::int64_t>(metadata + 32);
-        if (count <= 0 || count > 256) return false;
-        const auto rows = metadata + 56 + read<std::uintptr_t>(metadata + 40);
-        unsigned matches = 0;
-        for (std::int64_t i = 0; i < count; ++i) {
-            const auto row = rows + 32 * i;
-            const auto variants = read<std::int64_t>(row + 16);
-            if (variants < 0 || variants > 256) return false;
-            const auto names = row + 40 + read<std::uintptr_t>(row + 24);
-            for (std::int64_t j = 0; j < variants; ++j) {
-                if (read<std::uint32_t>(names + 4 * j) != variant) continue;
-                group = read<std::uint32_t>(row + 12);
-                ++matches;
-            }
-        }
-        return matches == 1;
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-bool __fastcall lookup(std::uint32_t actor, const std::uint32_t* group,
-    const std::uint32_t* variant, int* groupIndex, int* variantIndex) noexcept {
-    const auto original = reinterpret_cast<Lookup>(lookupHook.original);
-    if (!group || !variant || *group != none || !exit_name(*variant))
-        return original(actor, group, variant, groupIndex, variantIndex);
-    std::uint32_t resolved = none;
-    const bool found = find_group(actor, *variant, resolved);
-    const bool ok = original(actor, found ? &resolved : group, variant, groupIndex, variantIndex);
-    if (lookupReports.fetch_add(1, std::memory_order_relaxed) < 16)
-        core::log::writef(core::log::Channel::client, core::log::Level::info,
-            "ev=harvester_exit stage=resolve actor=%08x group=%08x action=%08x unique=%u result=%s",
-            actor, resolved, *variant, found ? 1U : 0U, ok ? "resolved" : "unresolved");
-    return ok;
-}
-std::uint32_t action_name(void* self, int index) noexcept {
-    __try {
-        const auto runtime = reinterpret_cast<std::uintptr_t>(self);
-        if (!runtime || index < 0 || index >= read<std::int64_t>(runtime + 1264)) return none;
-        const auto source = resolve(read<std::uint32_t>(runtime), read<std::uintptr_t>(runtime + 8));
-        if (!source || read<std::uint32_t>(source) != 0x80fe21ce
-            || read<std::uint32_t>(source + 4) != 0x808067ec) return none;
-        return read<std::uint32_t>(source + 816 + read<std::uintptr_t>(source + 784) + 80 * index);
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return none; }
-}
-std::intptr_t __fastcall start(void* self, int index) noexcept {
-    const auto name = action_name(self, index);
-    const auto result = reinterpret_cast<Start>(startHook.original)(self, index);
-    if (name != none)
-        core::log::writef(core::log::Channel::client, core::log::Level::info,
-            "ev=harvester_action stage=start action=%08x index=%d active=%d",
-            name, index, read<std::int32_t>(reinterpret_cast<std::uintptr_t>(self) + 1484));
-    return result;
-}
-bool __fastcall stop(void* self, int index) noexcept {
-    const auto name = action_name(self, index);
-    const bool result = reinterpret_cast<Stop>(stopHook.original)(self, index);
-    if (name != none)
-        core::log::writef(core::log::Channel::client, core::log::Level::info,
-            "ev=harvester_action stage=stop action=%08x index=%d result=%u", name, index, result ? 1U : 0U);
-    return result;
-}
-}
-bool install_exit() noexcept {
-    if (lookupHook.attached && startHook.attached && stopHook.attached) return true;
-    base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    // Exact loaded-build prologues; no patching on an unrecognized executable.
-    constexpr unsigned char lookupPrefix[]{0x48,0x83,0xec,0x38,0x4c,0x8b,0xd2,0x81,0xe1,0xff,0x1f,0x00,0x00};
-    constexpr unsigned char startPrefix[]{0x48,0x89,0x5c,0x24,0x20,0x55,0x56,0x57,0x41,0x55,0x41,0x57,0x48,0x8d,0x6c,0x24,0xc9,0x48,0x81,0xec};
-    constexpr unsigned char stopPrefix[]{0x48,0x89,0x74,0x24,0x18,0x57,0x48,0x83,0xec,0x70,0x44,0x8b,0x11,0x48,0x8b,0xf9,0x48,0x8b,0x05,0x99};
-    if (!base || std::memcmp(reinterpret_cast<void*>(base + 0xa054c0), lookupPrefix, sizeof(lookupPrefix))
-        || std::memcmp(reinterpret_cast<void*>(base + 0x1023f00), startPrefix, sizeof(startPrefix))
-        || std::memcmp(reinterpret_cast<void*>(base + 0x10245c0), stopPrefix, sizeof(stopPrefix))) {
-        core::log::write(core::log::Channel::client, core::log::Level::warn,
-            "ev=harvester_exit stage=install result=build_mismatch");
-        return false;
-    }
-    const bool a = lookupHook.attached || hooking::detour::install(
-        {reinterpret_cast<void*>(base + 0xa054c0), reinterpret_cast<void*>(&lookup)}, lookupHook);
-    const bool b = startHook.attached || hooking::detour::install(
-        {reinterpret_cast<void*>(base + 0x1023f00), reinterpret_cast<void*>(&start)}, startHook);
-    const bool c = stopHook.attached || hooking::detour::install(
-        {reinterpret_cast<void*>(base + 0x10245c0), reinterpret_cast<void*>(&stop)}, stopHook);
-    core::log::writef(core::log::Channel::client, core::log::Level::info,
-        "ev=harvester_exit stage=install lookup=%u start=%u stop=%u", a ? 1U : 0U, b ? 1U : 0U, c ? 1U : 0U);
-    return a && b && c;
-}
-}

+ 0 - 107
Sunrise/src/client/hooks/mission_retirement/mission_retirement.cpp

@@ -1,107 +0,0 @@
-#include "mission_retirement.h"
-#include <Windows.h>
-#include <array>
-#include <cstring>
-#include <cstdio>
-#include "../../../core/logging/log.h"
-#include "../../patterns/image_scan.h"
-#include "../../patterns/signature_text.h"
-
-namespace sunrise::client::hooks::mission_retirement {
-namespace {
-constexpr std::size_t kCapacity = 96;
-SRWLOCK g_lock = SRWLOCK_INIT;
-RequestId g_id{};
-Progress g_progress{};
-std::array<std::uint32_t, kCapacity> g_keys{};
-std::size_t g_count{};
-ULONGLONG g_started{};
-const std::uint16_t* g_registryHandle{};
-using Resolve = const std::uintptr_t*(__fastcall*)(std::uint16_t);
-Resolve g_resolve{};
-bool g_resolved{};
-
-void report(const char* line) noexcept {
-    core::log::write(core::log::Channel::client, core::log::Level::info, line);
-}
-void resolve() noexcept {
-    if (g_resolved) return;
-    g_resolved = true;
-    // 4E2580's clear routine supplies the registry handle and read-only pool accessor.
-    // Resolve those operands; do not call the clearing routine.
-    constexpr std::string_view text =
-        "48 83 EC 28 0F B7 0D ? ? ? ? 66 85 C9 74 ? E8 ? ? ? ? "
-        "48 8B 08 48 85 C9 74 ? 33 C0 33 D2 89 41 08 41 B8 24 01 00 00";
-    constexpr auto sig = patterns::signature<patterns::signature_length(text)>(text);
-    auto* p = patterns::scan_main_image_unique(sig, "mission_retirement_registry");
-    if (!p) return;
-    g_registryHandle = reinterpret_cast<const std::uint16_t*>(patterns::resolve_relative(p + 7, p + 11));
-    g_resolve = reinterpret_cast<Resolve>(patterns::resolve_relative(p + 17, p + 21));
-}
-bool sample(std::uintptr_t& owner, std::size_t& matching, std::uint32_t& count) noexcept {
-    __try {
-        if (!g_registryHandle || !g_resolve || !*g_registryHandle) return false;
-        owner = *g_resolve(*g_registryHandle);
-        if (!owner) return false;
-        count = *reinterpret_cast<const std::uint32_t*>(owner + 8);
-        if (count == 0 || count > 128) return false; // An empty/destroyed world is not retirement.
-        matching = 0;
-        for (std::uint32_t i = 0; i < count; ++i) {
-            const auto key = *reinterpret_cast<const std::uint32_t*>(owner + 20 + 24 * i);
-            for (std::size_t j = 0; j < g_count; ++j)
-                if (key == g_keys[j]) { ++matching; break; }
-        }
-        return owner == *g_resolve(*g_registryHandle)
-            && count == *reinterpret_cast<const std::uint32_t*>(owner + 8);
-    } __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
-}
-}
-Status prepare(RequestId id, std::int32_t sourceRegion,
-    std::span<const std::uint32_t> keys) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    if (!(g_id == id)) {
-        g_id = id; g_progress.owner = 0; g_count = 0; g_progress.sourceRegion = sourceRegion;
-        g_started = GetTickCount64();
-        g_progress.value = keys.empty() || keys.size() > kCapacity || sourceRegion < 0
-            ? Status::failed : Status::baselinePending;
-        if (g_progress.value != Status::failed) {
-            g_count = keys.size();
-            std::memcpy(g_keys.data(), keys.data(), keys.size_bytes());
-        }
-        std::array<char, 192> line{};
-        std::snprintf(line.data(), line.size(),
-            "ev=mission_retirement result=requested source=%d keys=%zu status=%u revision=%llu",
-            sourceRegion, keys.size(), static_cast<unsigned>(g_progress.value),
-            static_cast<unsigned long long>(id.revision));
-        report(line.data());
-    }
-    const auto result = g_progress.value;
-    ReleaseSRWLockExclusive(&g_lock);
-    return result;
-}
-Status status(RequestId id) noexcept {
-    AcquireSRWLockShared(&g_lock);
-    const auto result = g_id == id ? g_progress.value : Status::absent;
-    ReleaseSRWLockShared(&g_lock);
-    return result;
-}
-void poll(std::int32_t currentRegion) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    if (g_progress.value == Status::baselinePending || g_progress.value == Status::retiring) {
-        resolve();
-        std::uintptr_t owner{}; std::size_t matching{}; std::uint32_t count{};
-        if (GetTickCount64() - g_started > 120000) {
-            g_progress.value = g_progress.owner ? Status::failedRetiring : Status::failed;
-            report("ev=mission_retirement result=failed reason=native_cleanup_timeout");
-        } else if (sample(owner, matching, count)) {
-            const auto before = g_progress.value;
-            g_progress.observe(currentRegion, owner, count, matching);
-            if (before != g_progress.value)
-                report(g_progress.value == Status::complete
-                    ? "ev=mission_retirement result=native_cleanup_complete"
-                    : "ev=mission_retirement result=baseline_observed");
-        }
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-}
-}

+ 0 - 31
Sunrise/src/client/hooks/mission_retirement/mission_retirement.h

@@ -1,31 +0,0 @@
-#pragma once
-#include <cstdint>
-#include <span>
-
-namespace sunrise::client::hooks::mission_retirement {
-struct RequestId {
-    std::uint64_t session{}, binding{}, revision{};
-    bool operator==(const RequestId&) const = default;
-};
-enum class Status { absent, baselinePending, retiring, complete, failed, failedRetiring };
-struct Progress {
-    Status value{Status::absent};
-    std::int32_t sourceRegion{-1};
-    std::uintptr_t owner{};
-    void observe(std::int32_t region, std::uintptr_t observedOwner,
-        std::uint32_t groupCount, std::size_t matchingKeys) noexcept {
-        if (region != sourceRegion || !observedOwner || !groupCount || groupCount > 128) return;
-        if (value == Status::baselinePending && matchingKeys > 0) {
-            owner = observedOwner; value = Status::retiring;
-        } else if (value == Status::retiring && owner == observedOwner && matchingKeys == 0) {
-            value = Status::complete;
-        }
-    }
-};
-// Keys remain at their wire ordinals. The client observes them before and after removal.
-Status prepare(RequestId id, std::int32_t sourceRegion,
-    std::span<const std::uint32_t> keys) noexcept;
-Status status(RequestId id) noexcept;
-// Called only from the existing native frame observation point; never changes game memory.
-void poll(std::int32_t currentRegion) noexcept;
-}

+ 1 - 67
Sunrise/src/client/hooks/retail_log/retail_log_enqueue_observer.cpp

@@ -8,7 +8,7 @@
 
 #include "../../../core/logging/log.h"
 #include "../../targets/game.h"
-#include "../../diagnostics/entity_create_probe.h"
+#include "../network/investment/internal.h"
 
 namespace sunrise::client::hooks::retail_log {
 namespace {
@@ -112,72 +112,6 @@ void capture_line(std::int32_t siteId, const char* text) noexcept {
     core::log::write(core::log::Channel::client, core::log::Level::info, {line.data(), length});
 }
 
-/** Text whose emitting call site is worth locating in the image. */
-constexpr std::string_view kTracedText = "failed to create";
-/** Call sites named per run, so a repeating line cannot flood the sink. */
-constexpr std::size_t kMaxCallSiteReports = 64;
-
-/** Reports already spent. */
-volatile LONG g_callSiteReports{};
-
-/**
- * Names the image offset of the code that emitted one line.
- * The packed executable cannot be disassembled on disk, so a dump of the mapped image is the only
- * readable copy, and an offset from the load base is what addresses it. The retail text itself
- * carries no address, and the site id is assigned by the game's own registration rather than by
- * position, so nothing else here says which function produced a line. `_ReturnAddress` inside the
- * funnel is the emitting call site, which is exactly the function to disassemble.
- * @param returnAddress Return address captured in the funnel.
- * @param text Already-formatted native line.
- */
-void report_call_site(const void* returnAddress, const char* text) noexcept {
-    const bool entityFailure = std::string_view(text).find("networking:simulation:entity:")
-                               != std::string_view::npos;
-    const auto level = entityFailure ? core::log::Level::warn : core::log::Level::debug;
-    if (returnAddress == nullptr
-        || !core::log::accepts(core::log::Channel::client, level)) {
-        return;
-    }
-    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
-    const auto site = reinterpret_cast<std::uintptr_t>(returnAddress);
-    if (base == 0 || site < base) {
-        return;
-    }
-    if (InterlockedIncrement(&g_callSiteReports) > static_cast<LONG>(kMaxCallSiteReports)) {
-        return;
-    }
-    std::array<char, kEventCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=retail_site stage=caller rva=0x%llX va=0x%llX text=%s",
-                                      static_cast<unsigned long long>(site - base),
-                                      static_cast<unsigned long long>(site),
-                                      text);
-    if (written > 0) {
-        const auto length = static_cast<std::size_t>(written) < line.size()
-                                ? static_cast<std::size_t>(written)
-                                : line.size() - 1;
-        core::log::write(core::log::Channel::client, level,
-                         {line.data(), length});
-    }
-    if (entityFailure) {
-        diagnostics::report_entity_create_failure_pool();
-        // Bird's retail line omits the entity identity and failure boundary. Capture
-        // the callers at the failure, while they still exist, at normal log settings.
-        // This observes the failure; it does not retry or suppress native creation.
-        std::array<void*, 12> frames{};
-        const auto count = RtlCaptureStackBackTrace(1, static_cast<DWORD>(frames.size()),
-                                                    frames.data(), nullptr);
-        for (USHORT index = 0; index < count; ++index) {
-            const auto address = reinterpret_cast<std::uintptr_t>(frames[index]);
-            core::log::writef(core::log::Channel::client, level,
-                "ev=entity_create_failure stage=stack frame=%u va=0x%llX image_base=0x%llX",
-                static_cast<unsigned>(index), static_cast<unsigned long long>(address),
-                static_cast<unsigned long long>(base));
-        }
-    }
-}
-
 /**
  * Mirrors the single funnel every retail log line passes through.
  * @param siteId Registered site id.

+ 0 - 3
Sunrise/src/client/runtime/client_hook_activation.cpp

@@ -24,7 +24,6 @@
 #include "../hooks/cursor/runtime.h"
 #include "../hooks/graphics/graphics_hook_lifecycle.h"
 #include "../hooks/hitch_probe/hitch_probe.h"
-#include "../hooks/harvester_drop/harvester_drop_experiment.h"
 #include "../hooks/inactivity/inactivity_override.h"
 #include "../hooks/infinite_ammo/infinite_ammo.h"
 #include "../hooks/membership_probe/membership_probe.h"
@@ -175,8 +174,6 @@ void clear_game_targets() noexcept {
                                  : "ev=activate stage=package_keys result=fail");
     // Diagnostic capture reports its own outcome and never demotes this stage.
     (void)hooks::retail_log::install();
-    (void)hooks::vendor_banner::install();
-    (void)hooks::harvester_drop::install();
     (void)hooks::assert_handler::install();
     // Read-only. At a hitch it dumps every in-flight job record from the watchdog snapshot,
     // which names the job and thread the in-world freeze blocks on.

+ 0 - 12
Sunrise/src/core/settings/client/client_settings_parser.cpp

@@ -15,8 +15,6 @@ bool Parser::client_settings(client::Settings& output) noexcept {
     bool hasRevealLoreBooks = false;
     bool hasRegionPrivate = false;
     bool hasSkipOrbitCinematicWait = false;
-    bool hasSuppressLoadingCinematics = false;
-    bool hasSuppressPeerRelay = false;
     bool hasPinReplicatedRecord = false;
     if (consume('}')) {
         return true;
@@ -61,16 +59,6 @@ bool Parser::client_settings(client::Settings& output) noexcept {
                 return false;
             }
             hasSkipOrbitCinematicWait = true;
-        } else if (key == "suppress_loading_cinematics") {
-            if (hasSuppressLoadingCinematics || !boolean(candidate.suppressLoadingCinematics)) {
-                return false;
-            }
-            hasSuppressLoadingCinematics = true;
-        } else if (key == "suppress_peer_relay") {
-            if (hasSuppressPeerRelay || !boolean(candidate.suppressPeerRelay)) {
-                return false;
-            }
-            hasSuppressPeerRelay = true;
         } else if (key == "pin_replicated_record") {
             if (hasPinReplicatedRecord || !boolean(candidate.pinReplicatedRecord)) {
                 return false;

+ 0 - 2
Sunrise/src/core/settings/client/definition.h

@@ -32,8 +32,6 @@ struct Settings {
      * suppresses the orbit-side entry cinematic.
      */
     bool skipOrbitCinematicWait{false};
-    /** Force LoadingCinematics_Suppressed only for 1AU; other activities use the native predicate. */
-    bool suppressLoadingCinematics{false};
     /**
      * Pins the participation record to the replicated snapshot at `comp + 496`.
      * The msg-5 spawn hold reaches no other record.

+ 0 - 1
Sunrise/src/middleware/bap/activity_message/activity_incident_parser.cpp

@@ -132,7 +132,6 @@ Verdict validate(std::span<const std::byte> payload, Incident& parsed) noexcept
     if (!read_bytes(reader, std::span(parsed.payload).first(parsed.payloadLength))) {
         return Verdict::truncated;
     }
-    parsed.hasPayload = parsed.payloadLength != 0;
     parsed.consumedBits = static_cast<std::uint32_t>(payload.size() * encoding::kBitsPerByte
                                                      - reader.remaining_bits());
     return Verdict::accepted;

+ 1 - 4
Sunrise/src/middleware/bap/activity_message/activity_replicate_membership_encoder.cpp

@@ -28,10 +28,7 @@ bool encode_replicate_membership(const MembershipSnapshot& snapshot,
                          && writer.write(1, 1) && write_region_block(writer, snapshot)
                          && writer.write(1, 1) && writer.write(occupied_member_mask(snapshot), 32)
                          && writer.write(1, 1) && writer.write(occupied_member_mask(snapshot), 32)
-                         // Field 6 drives native view +178 (entity send enabled). An absent
-                         // delta preserves zero and strands every deletion awaiting this host.
-                         && writer.write(1, 1)
-                         && writer.write(entity_replication_member_mask(snapshot), 32)
+                         && writer.write(1, 1) && writer.write(active_view_mask(snapshot), 32)
                          && writer.write(0, 1) && writer.write(0, 1);
     std::size_t encodedSize = 0;
     const std::size_t meaningfulBits = meaningful_bit_count(snapshot);

+ 0 - 616
Sunrise/src/middleware/bap/activity_message/activity_scriptable_auth_fixed_body_codec.cpp

@@ -22,63 +22,6 @@ constexpr std::uint64_t kUnusedAuxiliary = 0;
 /** Type-31 reserves the largest generation as its unset sentinel. */
 constexpr std::uint64_t kReservedGeneration = std::numeric_limits<std::uint64_t>::max();
 
-/**
- * Writes the 353-bit 0x808099C4 child with no timer: epoch -1, all other fields 0.
- * Epoch 0 arms the client's goal-timer pin and the directive banner never hides.
- */
-[[nodiscard]] bool write_neutral_timed_state(bits::Writer& writer) noexcept {
-    if (!writer.write(0, kBoolWidth)) {
-        return false;
-    }
-    for (std::size_t index = 0; index < 4; ++index) {
-        if (!writer.write(0, kWideIntegerWidth)) {
-            return false;
-        }
-    }
-    return writer.write(kNoTimerEpoch, kWideIntegerWidth) && writer.write(0, kReal32Width);
-}
-
-/** Writes one neutral 239-bit authored HUD marker subrecord. */
-[[nodiscard]] bool write_directive_marker(bits::Writer& writer, const Type2LaneClientRef& target) noexcept {
-    // 100A6E0 copies .0 into the native target; .2's first hash must be the
-    // absent FNV sentinel or the client attempts a second authored-name lookup.
-    return writer.write(target.registryKey, 32)
-           && writer.write(static_cast<std::uint8_t>(target.slotType), kClientRefTypeWidth)
-           && writer.write(static_cast<std::uint32_t>(target.slotIndex + kClientRefIndexBias), kClientRefIndexWidth)
-           && write_absent_client_ref(writer) && writer.write(kClientRefAbsentKey, 32)
-           && writer.write(0, 32) && writer.write(0, 32) && writer.write(0, 32)
-           && writer.write(0, kBoolWidth);
-}
-
-/** Writes one of the three complete type-68 state lanes. */
-[[nodiscard]] bool write_directive_entry(bits::Writer& writer,
-                                         std::uint32_t nameHash,
-                                         std::int32_t elementIndex,
-                                         std::int8_t state,
-                                         const Type2LaneClientRef& target = {}) noexcept {
-    if (!writer.write(nameHash, 32)
-        || !writer.write(std::bit_cast<std::uint32_t>(elementIndex) + kSigned32Bias, 32)
-        || !writer.write(static_cast<std::uint32_t>(state) + 1U, kDirectiveStateWidth)
-        || !write_neutral_timed_state(writer)) {
-        return false;
-    }
-    for (std::size_t index = 0; index < 4; ++index) {
-        if (!writer.write(kSigned32Bias, 32)) {
-            return false;
-        }
-    }
-    if (!writer.write(1, kDirectiveStateWidth) || !write_absent_client_ref(writer)
-        || !writer.write(target.slotIndex >= 0 ? 3U : 1U, kDirectiveAuxStateWidth)) {
-        return false;
-    }
-    for (std::size_t index = 0; index < 4; ++index) {
-        if (!write_directive_marker(writer, index == 0 ? target : Type2LaneClientRef{})) {
-            return false;
-        }
-    }
-    return true;
-}
-
 /** @return The reflected channel ordinal, or the channel count for an invalid enum value. */
 [[nodiscard]] constexpr std::size_t channel_index(Type23Channel channel) noexcept {
     const auto index = static_cast<std::size_t>(channel);
@@ -457,563 +400,4 @@ bool validate_type35_body(std::span<const std::byte> input, std::size_t bitCount
     return bitCount == kType35BitCount && decode_type35_body(input, body);
 }
 
-/** Finds the next sequence revision while reserving 0xFF as inactive. */
-bool next_type5_revision(const Type5RevisionGuard& guard, std::uint8_t& next) noexcept {
-    next = 1U;
-    if (!guard.hasLast) {
-        return true;
-    }
-    if (guard.last >= kSequenceInactiveRevision - 1U) {
-        return false;
-    }
-    next = static_cast<std::uint8_t>(guard.last + 1U);
-    return next != kSequenceInactiveRevision;
-}
-
-/** Encodes the canonical sequence body: one revision and 129 unset ClientRefs. */
-bool encode_type5(const Type5Preset& preset,
-                  const Type5RevisionGuard& guard,
-                  std::span<std::byte> output,
-                  std::size_t& written) noexcept {
-    written = 0;
-    if (preset.revision == kSequenceInactiveRevision
-        || (guard.hasLast && preset.revision <= guard.last) || output.size() < kType5ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType5ByteCount));
-    bool encoded = writer.write(0, kWideIntegerWidth) && writer.write(0, kWideIntegerWidth)
-                   && writer.write(preset.revision, 8U);
-    for (std::size_t index = 0; encoded && index < 4U; ++index) {
-        encoded = writer.write(0, kSigned32Width);
-    }
-    for (std::size_t index = 0; encoded && index < kSequenceReferenceCount; ++index) {
-        encoded = write_absent_client_ref(writer);
-    }
-    encoded = encoded && write_absent_client_ref(writer);
-    return encoded && writer.bit_count() == kType5BitCount && writer.finish(written)
-           && written == kType5ByteCount;
-}
-
-/** Accepts only the canonical closed type-5 body emitted by encode_type5. */
-bool validate_type5_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    if (bitCount != kType5BitCount || input.size() != kType5ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t first = 0;
-    std::uint64_t second = 0;
-    std::uint64_t revision = 0;
-    if (!reader.read(kWideIntegerWidth, first) || !reader.read(kWideIntegerWidth, second)
-        || !reader.read(8U, revision) || first != 0 || second != 0
-        || revision == kSequenceInactiveRevision) {
-        return false;
-    }
-    for (std::size_t index = 0; index < 4U; ++index) {
-        std::uint64_t value = 0;
-        if (!reader.read(kSigned32Width, value) || value != 0) {
-            return false;
-        }
-    }
-    for (std::size_t index = 0; index < kSequenceReferenceCount + 1U; ++index) {
-        if (!read_absent_client_ref(reader)) {
-            return false;
-        }
-    }
-    return finish_padding(reader);
-}
-
-/** Finds the next cinematic generation without wrapping. */
-bool next_type6_generation(const Type6GenerationGuard& guard, std::uint32_t& next) noexcept {
-    next = 1U;
-    if (!guard.hasLast) {
-        return true;
-    }
-    if (guard.last == (std::numeric_limits<std::uint32_t>::max)()) {
-        return false;
-    }
-    next = guard.last + 1U;
-    return next != 0;
-}
-
-/** Encodes the canonical cinematic start/stop body with empty participant arrays. */
-bool encode_type6(const Type6Preset& preset,
-                  const Type6GenerationGuard& guard,
-                  std::span<std::byte> output,
-                  std::size_t& written) noexcept {
-    written = 0;
-    if (preset.generation == 0 || (guard.hasLast && preset.generation <= guard.last)
-        || output.size() < kType6ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType6ByteCount));
-    bool encoded = writer.write(0, kWideIntegerWidth) && writer.write(0, kWideIntegerWidth)
-                   && writer.write(preset.generation, kSigned32Width)
-                   && writer.write(preset.active ? 1U : 0U, kBoolWidth)
-                   && writer.write(0, kBoolWidth)
-                   && write_absent_client_ref(writer)
-                   // Bias three stores the native wildcard value -1 as wire value two.
-                   && writer.write(2U, 6U)
-                   // Both arrays are dynamically sized. Zero counts have no element payload.
-                   && writer.write(0, 5U) && writer.write(0, 3U) && writer.write(0, kSigned32Width);
-    return encoded && writer.bit_count() == kType6BitCount && writer.finish(written)
-           && written == kType6ByteCount;
-}
-
-/** Accepts only the canonical closed type-6 body emitted by encode_type6. */
-bool validate_type6_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    if (bitCount != kType6BitCount || input.size() != kType6ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t first = 0;
-    std::uint64_t second = 0;
-    std::uint64_t generation = 0;
-    std::uint64_t active = 0;
-    std::uint64_t force = 0;
-    std::uint64_t role = 0;
-    std::uint64_t count = 0;
-    if (!reader.read(kWideIntegerWidth, first) || !reader.read(kWideIntegerWidth, second)
-        || !reader.read(kSigned32Width, generation) || !reader.read(kBoolWidth, active)
-        || !reader.read(kBoolWidth, force) || !read_absent_client_ref(reader)
-        || !reader.read(6U, role) || !reader.read(5U, count) || first != 0 || second != 0
-        || generation == 0 || force != 0 || role != 2U || count != 0) {
-        return false;
-    }
-    if (!reader.read(3U, count) || count != 0) {
-        return false;
-    }
-    std::uint64_t trailing = 0;
-    return reader.read(kSigned32Width, trailing) && trailing == 0 && finish_padding(reader);
-}
-
-/** Advances the exact signed type-3 generation without wrapping its terminal value. */
-bool next_type3_generation(const Type3GenerationGuard& guard, std::int32_t& next) noexcept {
-    return next_positive_generation(guard.hasLast, guard.last, next);
-}
-
-/** Encodes the fixed 24-objective reset body and its monotonic generation. */
-bool encode_type3(const Type3Body& body,
-                  const Type3GenerationGuard& guard,
-                  std::span<std::byte> output,
-                  std::size_t& written) noexcept {
-    written = 0;
-    if (body.generation <= 0 || (guard.hasLast && body.generation <= guard.last)
-        || output.size() < kType3ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType3ByteCount));
-    bool encoded = writer.write(1U, kBoolWidth);
-    for (const std::int8_t value : body.objectiveRevisions) {
-        if (value < 0) {
-            return false;
-        }
-        encoded = encoded && writer.write(1U, kBoolWidth)
-                  && writer.write(static_cast<std::uint8_t>(value), kType3ValueWidth);
-    }
-    encoded = encoded && writer.write(1U, kBoolWidth)
-              && writer.write(static_cast<std::uint32_t>(body.generation), kType3GenerationWidth);
-    return encoded && writer.bit_count() == kType3BitCount && writer.finish(written)
-           && written == kType3ByteCount;
-}
-
-/** Decodes one complete fixed-width type-3 objective reset body. */
-bool decode_type3_body(std::span<const std::byte> input, Type3Body& body) noexcept {
-    body = {};
-    if (input.size() != kType3ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t arrayPresent = 0;
-    if (!reader.read(kBoolWidth, arrayPresent) || arrayPresent == 0) {
-        return false;
-    }
-    for (std::int8_t& value : body.objectiveRevisions) {
-        std::uint64_t present = 0;
-        std::uint64_t wire = 0;
-        if (!reader.read(kBoolWidth, present)
-            || (present != 0 && !reader.read(kType3ValueWidth, wire))) {
-            return false;
-        }
-        value = present != 0 ? static_cast<std::int8_t>(wire) : 0;
-    }
-    std::uint64_t present = 0;
-    std::uint64_t generation = 0;
-    if (!reader.read(kBoolWidth, present)
-        || (present != 0 && !reader.read(kType3GenerationWidth, generation))
-        || !finish_padding(reader)) {
-        return false;
-    }
-    body.generation = present != 0 ? static_cast<std::int32_t>(generation) : 0;
-    return body.generation > 0;
-}
-
-/** Validates the exact type-3 bit count and canonical fixed-width body. */
-bool validate_type3_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    Type3Body body{};
-    return bitCount == kType3BitCount && decode_type3_body(input, body);
-}
-
-/** Finds the next positive task generation without wrapping. */
-bool next_type38_generation(const Type38GenerationGuard& guard, std::int32_t& next) noexcept {
-    return next_positive_generation(guard.hasLast, guard.last, next);
-}
-
-/** Encodes one exact signed-int32 task generation. */
-bool encode_type38(const Type38Preset& preset,
-                   const Type38GenerationGuard& guard,
-                   std::span<std::byte> output,
-                   std::size_t& written) noexcept {
-    written = 0;
-    if (preset.generation <= 0 || (guard.hasLast && preset.generation <= guard.last)
-        || output.size() < kType38ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType38ByteCount));
-    const std::uint32_t wire = std::bit_cast<std::uint32_t>(preset.generation) + kSigned32Bias;
-    return writer.write(wire, kSigned32Width) && writer.bit_count() == kType38BitCount
-           && writer.finish(written) && written == kType38ByteCount;
-}
-
-/** Decodes one exact signed-int32 task generation. */
-bool decode_type38_body(std::span<const std::byte> input, std::int32_t& generation) noexcept {
-    if (input.size() != kType38ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t wire = 0;
-    if (!reader.read(kSigned32Width, wire) || !finish_padding(reader)) {
-        return false;
-    }
-    generation = std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(wire) - kSigned32Bias);
-    return generation > 0;
-}
-
-/** Checks one packed type-38 task generation. */
-bool validate_type38_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    std::int32_t generation = 0;
-    return bitCount == kType38BitCount && decode_type38_body(input, generation);
-}
-
-/** Finds the next positive dialogue fire sequence without wrapping. */
-bool next_type53_sequence(const Type53SequenceGuard& guard,
-                          std::uint16_t cueIndex,
-                          std::int32_t& next) noexcept {
-    next = 0;
-    if (cueIndex >= guard.last.size() || guard.last[cueIndex] < 0
-        || guard.last[cueIndex] == (std::numeric_limits<std::int32_t>::max)()) {
-        return false;
-    }
-    next = guard.last[cueIndex] + 1;
-    return next > 0;
-}
-
-/** Encodes one dialogue pulse with the schema's optional world field present only for its cue. */
-bool encode_type53(const Type53Preset& preset,
-                   const Type53SequenceGuard& guard,
-                   std::span<std::byte> output,
-                   std::size_t& written) noexcept {
-    written = 0;
-    if (preset.cueIndex >= kType53EntryCount || preset.sequence <= 0
-        || preset.sequence <= guard.last[preset.cueIndex] || output.size() < kType53ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType53ByteCount));
-    bool encoded = write_absent_client_ref(writer);
-    for (std::size_t index = 0; encoded && index < kType53EntryCount; ++index) {
-        const bool selected = index == preset.cueIndex;
-        const std::int32_t sequence = selected ? preset.sequence : guard.last[index];
-        const std::uint32_t wireSequence = std::bit_cast<std::uint32_t>(sequence) + kSigned32Bias;
-        const std::int8_t mode = selected ? kDialogueFireMode : kDialogueInactiveMode;
-        encoded =
-            writer.write(0, kWideIntegerWidth) && writer.write(selected ? 1U : 0U, kBoolWidth);
-        if (encoded && selected) {
-            encoded = writer.write(kDialogueActiveWorld, kWideIntegerWidth);
-        }
-        encoded =
-            encoded && write_absent_client_ref(writer) && writer.write(wireSequence, kSigned32Width)
-            && writer.write(static_cast<std::uint32_t>(static_cast<std::int32_t>(mode) + kModeBias),
-                            kModeWidth);
-    }
-    return encoded && writer.bit_count() == kType53BitCount && writer.finish(written)
-           && written == kType53ByteCount;
-}
-
-/** Validates the closed pulse form rather than accepting arbitrary type-53 references. */
-bool validate_type53_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    if (bitCount != kType53BitCount || input.size() != kType53ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    if (!read_absent_client_ref(reader)) {
-        return false;
-    }
-    std::size_t activeCount = 0;
-    for (std::size_t index = 0; index < kType53EntryCount; ++index) {
-        std::uint64_t tick = 0;
-        std::uint64_t hasWorld = 0;
-        std::uint64_t world = 0;
-        std::uint64_t wireSequence = 0;
-        std::uint64_t wireMode = 0;
-        if (!reader.read(kWideIntegerWidth, tick) || !reader.read(kBoolWidth, hasWorld)
-            || (hasWorld != 0 && !reader.read(kWideIntegerWidth, world))
-            || !read_absent_client_ref(reader) || !reader.read(kSigned32Width, wireSequence)
-            || !reader.read(kModeWidth, wireMode)) {
-            return false;
-        }
-        const std::int32_t sequence =
-            std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(wireSequence) - kSigned32Bias);
-        const std::int32_t mode = static_cast<std::int32_t>(wireMode) - kModeBias;
-        if (tick != 0) {
-            return false;
-        }
-        if (hasWorld != 0) {
-            if (hasWorld != 1 || world != kDialogueActiveWorld || sequence <= 0
-                || mode != kDialogueFireMode || ++activeCount != 1) {
-                return false;
-            }
-        } else if (mode != kDialogueInactiveMode) {
-            return false;
-        }
-    }
-    return activeCount == 1 && finish_padding(reader);
-}
-
-/** Encodes one complete three-lane type-68 directive state. */
-bool encode_type68(const Type68Preset& preset,
-                   std::span<std::byte> output,
-                   std::size_t& written) noexcept {
-    written = 0;
-    if ((preset.audience.slotIndex >= 0 && (preset.audience.slotType != 70
-            || preset.audience.registryKey == 0 || preset.audience.registryKey == kClientRefAbsentKey))
-        || output.size() < kType68ByteCount || preset.state < 0 || preset.state > 2
-        || (preset.visible
-            && (preset.nameHash == 0 || preset.nameHash == kClientRefAbsentKey
-                || preset.elementIndex < 0))
-        || (preset.navpoint.slotIndex >= 0 && (!preset.visible || preset.state != 0
-            || preset.navpoint.slotType != 47 || preset.navpoint.registryKey == 0
-            || preset.navpoint.registryKey == kClientRefAbsentKey))) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType68ByteCount));
-    bool encoded = (writer.write(preset.audience.registryKey, 32)
-        && writer.write(static_cast<std::uint32_t>(preset.audience.slotType) + 1U, 7)
-        && writer.write(static_cast<std::uint32_t>(preset.audience.slotIndex) + kSigned16Bias, 16)) && write_absent_client_ref(writer);
-    for (std::size_t index = 0; encoded && index < kType68EntryCount; ++index) {
-        encoded =
-            index == 0 && preset.visible
-                ? write_directive_entry(writer, preset.nameHash, preset.elementIndex, preset.state, preset.navpoint)
-                : write_directive_entry(writer, kClientRefAbsentKey, 0, -1);
-    }
-    encoded = encoded && writer.write(preset.visible ? 1U : 0U, kDirectiveActiveIndexWidth);
-    return encoded && writer.bit_count() == kType68BitCount && writer.finish(written)
-           && written == kType68ByteCount;
-}
-
-/** Validates the crash-bearing selector fields and the exact type-68 body shape. */
-bool validate_type68_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    if (bitCount != kType68BitCount || input.size() != kType68ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    auto skip = [&reader](std::size_t count) noexcept {
-        std::uint64_t discarded = 0;
-        while (count != 0) {
-            const auto width = static_cast<std::uint8_t>((std::min)(count, std::size_t{64}));
-            if (!reader.read(width, discarded)) {
-                return false;
-            }
-            count -= width;
-        }
-        return true;
-    };
-    if (!skip(110)) {
-        return false;
-    }
-    std::array<std::uint32_t, kType68EntryCount> names{};
-    std::array<std::int32_t, kType68EntryCount> elements{};
-    std::array<std::int32_t, kType68EntryCount> states{};
-    for (std::size_t index = 0; index < kType68EntryCount; ++index) {
-        std::uint64_t name = 0;
-        std::uint64_t element = 0;
-        std::uint64_t state = 0;
-        if (!reader.read(32, name) || !reader.read(32, element)
-            || !reader.read(kDirectiveStateWidth, state) || !skip(1'497)) {
-            return false;
-        }
-        names[index] = static_cast<std::uint32_t>(name);
-        elements[index] =
-            std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(element) - kSigned32Bias);
-        states[index] = static_cast<std::int32_t>(state) - 1;
-    }
-    std::uint64_t activeWire = 0;
-    if (!reader.read(kDirectiveActiveIndexWidth, activeWire)) {
-        return false;
-    }
-    const std::int32_t active = static_cast<std::int32_t>(activeWire) - 1;
-    if (active < -1 || active >= static_cast<std::int32_t>(kType68EntryCount)) {
-        return false;
-    }
-    if (active >= 0) {
-        const std::size_t selected = static_cast<std::size_t>(active);
-        if (names[selected] == 0 || names[selected] == kClientRefAbsentKey || elements[selected] < 0
-            || states[selected] < 0 || states[selected] > 2) {
-            return false;
-        }
-    }
-    return finish_padding(reader);
-}
-
-/** Encodes the no-filter type-70 encounter engagement state. */
-bool encode_type70(const Type70Preset& preset,
-                   std::span<std::byte> output,
-                   std::size_t& written) noexcept {
-    written = 0;
-    if (preset.flags > 0x1FU || output.size() < kType70ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType70ByteCount));
-    const auto revision = static_cast<std::uint16_t>(static_cast<std::int32_t>(preset.revision)
-                                                     + static_cast<std::int32_t>(kSigned16Bias));
-    const bool encoded = writer.write(preset.flags, 5U)
-                         // Both dynamic collections are absent; no element payload follows.
-                         && writer.write(0, kBoolWidth) && writer.write(revision, 16U)
-                         && writer.write(0, kBoolWidth);
-    return encoded && writer.bit_count() == kType70BitCount && writer.finish(written)
-           && written == kType70ByteCount;
-}
-
-/** Validates the exact no-filter type-70 form while preserving all five flag bits. */
-bool validate_type70_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    if (bitCount != kType70BitCount || input.size() != kType70ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t flags = 0;
-    std::uint64_t playerListPresent = 0;
-    std::uint64_t revision = 0;
-    std::uint64_t definitionListPresent = 0;
-    return reader.read(5U, flags) && flags <= 0x1FU && reader.read(kBoolWidth, playerListPresent)
-           && playerListPresent == 0 && reader.read(16U, revision)
-           && reader.read(kBoolWidth, definitionListPresent) && definitionListPresent == 0
-           && finish_padding(reader);
-}
-
-bool next_type42_generation(const Type42GenerationGuard& guard, std::int32_t& next) noexcept {
-    return next_positive_generation(guard.hasLast, guard.last, next);
-}
-
-/** The client treats the FNV-1 basis as "no performance", so neither it nor zero may be sent. */
-[[nodiscard]] bool valid_type42_name(std::uint32_t nameHash) noexcept {
-    return nameHash != 0 && nameHash != kClientRefAbsentKey;
-}
-
-/** Refuses a generation that does not rise, because the client starts the state only once. */
-bool encode_type42(const Type42Preset& preset,
-                   const Type42GenerationGuard& guard,
-                   std::span<std::byte> output,
-                   std::size_t& written) noexcept {
-    written = 0;
-    if (!valid_type42_name(preset.nameHash) || preset.generation <= 0
-        || (guard.hasLast && preset.generation <= guard.last) || output.size() < kType42ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType42ByteCount));
-    const std::uint32_t generation =
-        std::bit_cast<std::uint32_t>(preset.generation) + kSigned32Bias;
-    const bool encoded = writer.write(kEnabled, kBoolWidth) && writer.write(preset.nameHash, 32U)
-                         && writer.write(preset.value, 32U)
-                         && writer.write(generation, kSigned32Width)
-                         // The row table is absent; the client keeps its own rows.
-                         && writer.write(0, kBoolWidth);
-    return encoded && writer.bit_count() == kType42BitCount && writer.finish(written)
-           && written == kType42ByteCount;
-}
-
-/** Accepts only the command group present, a real state name, and an absent row table. */
-bool validate_type42_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    if (bitCount != kType42BitCount || input.size() != kType42ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t commandPresent = 0;
-    std::uint64_t nameHash = 0;
-    std::uint64_t value = 0;
-    std::uint64_t generation = 0;
-    std::uint64_t rowsPresent = 0;
-    return reader.read(kBoolWidth, commandPresent) && commandPresent == kEnabled
-           && reader.read(32U, nameHash) && valid_type42_name(static_cast<std::uint32_t>(nameHash))
-           && reader.read(32U, value) && reader.read(kSigned32Width, generation)
-           && generation > kSigned32Bias && reader.read(kBoolWidth, rowsPresent) && rowsPresent == 0
-           && finish_padding(reader);
-}
-
-/** @return True when the area reference names a real slot the client evaluator can resolve. */
-[[nodiscard]] bool valid_type71_area(const Type71Body& body) noexcept {
-    return body.areaRegistryKey != 0 && body.areaRegistryKey != kClientRefAbsentKey
-           && body.areaSlotType < (std::uint8_t{1} << kClientRefTypeWidth) - 1U
-           && body.areaSlotIndex < kClientRefIndexBias;
-}
-
-/** Encodes the fixed 183-bit type-71 body. */
-bool encode_type71(const Type71Body& body,
-                   std::span<std::byte> output,
-                   std::size_t& written) noexcept {
-    written = 0;
-    if (!valid_type71_area(body) || !std::isfinite(body.leaveSeconds) || body.leaveSeconds < 0.0F
-        || output.size() < kType71ByteCount) {
-        return false;
-    }
-    bits::Writer writer(output.first(kType71ByteCount));
-    const std::uint32_t state = std::bit_cast<std::uint32_t>(body.state) + kSigned32Bias;
-    const bool encoded =
-        writer.write(state, kSigned32Width) && writer.write(body.playerIdentity, kWideIntegerWidth)
-        && writer.write(body.areaRegistryKey, 32)
-        && writer.write(std::uint32_t{body.areaSlotType} + 1U, kClientRefTypeWidth)
-        && writer.write(std::uint32_t{body.areaSlotIndex} + kClientRefIndexBias,
-                        kClientRefIndexWidth)
-        && writer.write(std::bit_cast<std::uint32_t>(body.leaveSeconds), kReal32Width);
-    return encoded && writer.bit_count() == kType71BitCount && writer.finish(written)
-           && written == kType71ByteCount;
-}
-
-/** Decodes one complete type-71 body. */
-bool decode_type71_body(std::span<const std::byte> input, Type71Body& body) noexcept {
-    body = {};
-    if (input.size() != kType71ByteCount) {
-        return false;
-    }
-    bits::Reader reader(input);
-    std::uint64_t state = 0;
-    std::uint64_t identity = 0;
-    std::uint64_t key = 0;
-    std::uint64_t type = 0;
-    std::uint64_t index = 0;
-    std::uint64_t seconds = 0;
-    if (!reader.read(kSigned32Width, state) || !reader.read(kWideIntegerWidth, identity)
-        || !reader.read(32, key) || !reader.read(kClientRefTypeWidth, type)
-        || !reader.read(kClientRefIndexWidth, index) || !reader.read(kReal32Width, seconds)
-        || !finish_padding(reader) || type == 0 || index < kClientRefIndexBias) {
-        return false;
-    }
-    Type71Body parsed{};
-    parsed.state = std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(state) - kSigned32Bias);
-    parsed.playerIdentity = identity;
-    parsed.areaRegistryKey = static_cast<std::uint32_t>(key);
-    parsed.areaSlotType = static_cast<std::uint8_t>(type - 1U);
-    parsed.areaSlotIndex = static_cast<std::uint16_t>(index - kClientRefIndexBias);
-    parsed.leaveSeconds = std::bit_cast<float>(static_cast<std::uint32_t>(seconds));
-    if (!valid_type71_area(parsed) || !std::isfinite(parsed.leaveSeconds)
-        || parsed.leaveSeconds < 0.0F) {
-        return false;
-    }
-    body = parsed;
-    return true;
-}
-
-/** Validates the exact type-71 body shape and its present area reference. */
-bool validate_type71_body(std::span<const std::byte> input, std::size_t bitCount) noexcept {
-    Type71Body body{};
-    return bitCount == kType71BitCount && decode_type71_body(input, body);
-}
-
 } // namespace sunrise::middleware::bap::activity_message::scriptable_auth

+ 39 - 12
Sunrise/src/middleware/bap/activity_message/activity_scriptable_auth_mission_body_codec.cpp

@@ -49,9 +49,16 @@ constexpr std::uint8_t kDirectiveActiveIndexWidth = 3;
     return writer.write(kNoTimerEpoch, kWideIntegerWidth) && writer.write(0, kReal32Width);
 }
 
-/** Writes one neutral 239-bit authored HUD marker subrecord. */
-[[nodiscard]] bool write_neutral_directive_marker(bits::Writer& writer) noexcept {
-    return write_absent_client_ref(writer) && write_absent_client_ref(writer) && writer.write(0, 32)
+/** Writes one 239-bit authored HUD marker subrecord, targeting the given slot or none. */
+[[nodiscard]] bool write_directive_marker(bits::Writer& writer,
+                                          const Type2LaneClientRef& target) noexcept {
+    // The client copies .0 into its target. The first hash of .2 must be the absent key, or the
+    // client runs a second authored-name lookup.
+    return writer.write(target.registryKey, 32)
+           && writer.write(static_cast<std::uint8_t>(target.slotType), kClientRefTypeWidth)
+           && writer.write(static_cast<std::uint32_t>(target.slotIndex + kClientRefIndexBias),
+                           kClientRefIndexWidth)
+           && write_absent_client_ref(writer) && writer.write(kClientRefAbsentKey, 32)
            && writer.write(0, 32) && writer.write(0, 32) && writer.write(0, 32)
            && writer.write(0, kBoolWidth);
 }
@@ -60,7 +67,8 @@ constexpr std::uint8_t kDirectiveActiveIndexWidth = 3;
 [[nodiscard]] bool write_directive_entry(bits::Writer& writer,
                                          std::uint32_t nameHash,
                                          std::int32_t elementIndex,
-                                         std::int8_t state) noexcept {
+                                         std::int8_t state,
+                                         const Type2LaneClientRef& target = {}) noexcept {
     if (!writer.write(nameHash, 32)
         || !writer.write(std::bit_cast<std::uint32_t>(elementIndex) + kSigned32Bias, 32)
         || !writer.write(static_cast<std::uint32_t>(state) + 1U, kDirectiveStateWidth)
@@ -73,11 +81,11 @@ constexpr std::uint8_t kDirectiveActiveIndexWidth = 3;
         }
     }
     if (!writer.write(1, kDirectiveStateWidth) || !write_absent_client_ref(writer)
-        || !writer.write(1, kDirectiveAuxStateWidth)) {
+        || !writer.write(target.slotIndex >= 0 ? 3U : 1U, kDirectiveAuxStateWidth)) {
         return false;
     }
     for (std::size_t index = 0; index < 4; ++index) {
-        if (!write_neutral_directive_marker(writer)) {
+        if (!write_directive_marker(writer, index == 0 ? target : Type2LaneClientRef{})) {
             return false;
         }
     }
@@ -413,19 +421,38 @@ bool encode_type68(const Type68Preset& preset,
                    std::span<std::byte> output,
                    std::size_t& written) noexcept {
     written = 0;
-    if (output.size() < kType68ByteCount || preset.state < 0 || preset.state > 2
+    const auto valid_reference = [](const Type2LaneClientRef& reference, std::int8_t slotType) {
+        return reference.slotIndex < 0
+               || (reference.slotType == slotType && reference.registryKey != 0
+                   && reference.registryKey != kClientRefAbsentKey);
+    };
+    // A navpoint rides only on a visible entry in the enter state.
+    const bool navpointAllowed = preset.navpoint.slotIndex < 0 || (preset.visible && preset.state == 0);
+    if (!valid_reference(preset.audience, kType70SlotType)
+        || !valid_reference(preset.navpoint, kType47SlotType) || !navpointAllowed
+        || output.size() < kType68ByteCount || preset.state < 0 || preset.state > 2
         || (preset.visible
             && (preset.nameHash == 0 || preset.nameHash == kClientRefAbsentKey
                 || preset.elementIndex < 0))) {
         return false;
     }
     bits::Writer writer(output.first(kType68ByteCount));
-    bool encoded = write_absent_client_ref(writer) && write_absent_client_ref(writer);
+    // The audience ClientRef stays unset when no sensor was named: type 0, index -1.
+    bool encoded =
+        writer.write(preset.audience.registryKey, 32)
+        && writer.write(static_cast<std::uint32_t>(preset.audience.slotType) + 1U,
+                        kClientRefTypeWidth)
+        && writer.write(static_cast<std::uint32_t>(preset.audience.slotIndex) + kSigned16Bias,
+                        kClientRefIndexWidth)
+        && write_absent_client_ref(writer);
     for (std::size_t index = 0; encoded && index < kType68EntryCount; ++index) {
-        encoded =
-            index == 0 && preset.visible
-                ? write_directive_entry(writer, preset.nameHash, preset.elementIndex, preset.state)
-                : write_directive_entry(writer, kClientRefAbsentKey, 0, -1);
+        encoded = index == 0 && preset.visible
+                      ? write_directive_entry(writer,
+                                              preset.nameHash,
+                                              preset.elementIndex,
+                                              preset.state,
+                                              preset.navpoint)
+                      : write_directive_entry(writer, kClientRefAbsentKey, 0, -1);
     }
     encoded = encoded && writer.write(preset.visible ? 1U : 0U, kDirectiveActiveIndexWidth);
     return encoded && writer.bit_count() == kType68BitCount && writer.finish(written)

+ 92 - 58
Sunrise/src/middleware/bap/activity_message/activity_sense_update_decoder.cpp

@@ -26,6 +26,18 @@ constexpr std::uint32_t kSquadReals = 0x80807ECDU;
 constexpr std::uint32_t kObjectiveBlock = 0x80807F07U, kObjectiveTasks = 0x80807F08U;
 constexpr std::uint32_t kCombatant = 0x80807DA2U, kCombatantAtoms = 0x80807F6EU;
 constexpr std::uint32_t kCombatantKeyed = 0x80807DA3U, kCombatantLanes = 0x80807DA4U;
+/** Type-13 participation root and its nested blocks, in body order. */
+constexpr std::uint32_t kPlayer = 0x80804F2FU, kPlayerState = 0x808094E4U;
+constexpr std::uint32_t kPlayerScalars = 0x80804F39U, kPlayerTail = 0x80804F35U;
+constexpr std::uint32_t kPlayerIdentity = 0x808094DDU, kPlayerKeys = 0x808094DFU;
+constexpr std::uint32_t kPlayerPairs = 0x808094E3U, kPlayerFlags = 0x80804F31U;
+constexpr std::uint32_t kPlayerOptional = 0x80804F34U;
+/** Type-20 damage monitor root: health, shield, echoed revision. */
+constexpr std::uint32_t kDamage = 0x80809562U;
+/** Registered object replies: the interaction latch and the ownership key. */
+constexpr std::uint32_t kInteractionReply = 0x80804FB7U, kOwnershipReply = 0x80809ACCU;
+/** Counted lists inside the participation body. */
+constexpr std::uint64_t kPlayerScalarCountMaximum = 4, kPlayerKeyCountMaximum = 32;
 /** Quantization ceilings the combatant reals carry in their own descriptors. */
 constexpr std::uint32_t kSpatialMaximumBits = 0x45000000U, kUnitMaximumBits = 0x3F800000U;
 
@@ -514,17 +526,16 @@ real_value(std::uint64_t raw, std::uint8_t width, std::uint32_t maximumBits) noe
         if (!reader.read(1, exists)) return NativeStatus::malformed;
         if (!exists) continue;
         if (!reader.read(32, schema)) return NativeStatus::malformed;
-        // F32220 publishes the interaction latch and revision through this registered child.
-        if (schema == 0x80804FB7U) {
-            if (!read_bool(reader, values, 0x80804FB7U, 0, false, index)
-                || !read_signed(reader, values, 0x80804FB7U, 1, 32, 32,
+        if (schema == kInteractionReply) {
+            // The interaction latch and the revision that set it.
+            if (!read_bool(reader, values, kInteractionReply, 0, false, index)
+                || !read_signed(reader, values, kInteractionReply, 1, 32, 32,
                     (std::numeric_limits<std::int32_t>::min)(), false, index))
                 return NativeStatus::malformed;
-        } else if (schema == 0x80809ACCU) {
-            // Runtime flags 0x21 mean exact + custom codec, not optional (0x02).
-            // Native 9F0750 always writes the packed owner key, including zero when unheld.
-            if (!read_bool(reader, values, 0x80809ACCU, 0, false, index)
-                || !read_unsigned(reader, values, 0x80809ACCU, 1, 64, false, index))
+        } else if (schema == kOwnershipReply) {
+            // The owner key is always written, zero when nobody holds the object.
+            if (!read_bool(reader, values, kOwnershipReply, 0, false, index)
+                || !read_unsigned(reader, values, kOwnershipReply, 1, 64, false, index))
                 return NativeStatus::malformed;
         } else return NativeStatus::unsupported;
     }
@@ -583,51 +594,76 @@ real_value(std::uint64_t raw, std::uint8_t width, std::uint32_t maximumBits) noe
     }
     return NativeStatus::complete;
 }
-/** Type13 participation: authored field tables80804F2F/808094E4. In particular,
- * F9CC60 publishes .0.5 from a dead player with a valid Ghost, not a missing actor
- * during region loading. Decode the complete body before accepting that evidence. */
+/**
+ * Decodes the type-13 participation body. The whole body is read before its Ghost flag counts,
+ * because a player mid-load publishes a body with the flag but without a settled actor.
+ */
 [[nodiscard]] NativeStatus decode_player(Reader& reader, Values& values) noexcept {
     constexpr auto signedBias = (std::numeric_limits<std::int32_t>::min)();
-    if (!read_signed(reader,values,0x808094E4,0,32,32,signedBias,true)
-        || !read_unsigned(reader,values,0x808094E4,1,32,true)) return NativeStatus::malformed;
-    for (unsigned i=2;i<=5;++i)
-        if (!read_bool(reader,values,0x808094E4,i,false)) return NativeStatus::malformed;
-    if (!read_signed(reader,values,0x808094E4,6,3,8,1,false)
-        || !read_signed(reader,values,0x808094E4,7,2,8,1,false)) return NativeStatus::malformed;
-    std::uint64_t count{};
-    if (!reader.read(3,count)) return NativeStatus::malformed;
-    if (count>4) return NativeStatus::unsafeCount;
-    for (unsigned i=0;i<count;++i)
-        if (!read_unsigned(reader,values,0x80804F39,0,32,false,i)
-            || !read_real(reader,values,0x80804F39,1,32,false,i)) return NativeStatus::malformed;
-    if (!read_real(reader,values,0x80804F35,1,32,false)
-        || !read_signed(reader,values,0x80804F35,2,5,32,1,false)
-        || !read_signed(reader,values,0x80804F35,3,32,32,signedBias,true)) return NativeStatus::malformed;
-    for (unsigned i=4;i<=6;++i)
-        if (!read_bool(reader,values,0x80804F35,i,false)) return NativeStatus::malformed;
-    if (!read_unsigned(reader,values,0x808094DD,0,64,true)
-        || !read_unsigned(reader,values,0x808094DD,1,5,false)
-        || !read_signed(reader,values,0x808094DD,2,6,8,3,false)) return NativeStatus::malformed;
-    if (!reader.read(6,count)) return NativeStatus::malformed;
-    if (count>32) return NativeStatus::unsafeCount;
-    for (unsigned i=0;i<count;++i)
-        if (!read_unsigned(reader,values,0x808094DF,0,32,false,i)) return NativeStatus::malformed;
-    if (!reader.read(6,count)) return NativeStatus::malformed;
-    if (count>32) return NativeStatus::unsafeCount;
-    for (unsigned i=0;i<count;++i)
-        if (!read_unsigned(reader,values,0x808094E3,0,32,true,i)
-            || !read_unsigned(reader,values,0x808094E3,1,64,true,i)) return NativeStatus::malformed;
-    if (!read_bool(reader,values,0x80804F31,0,false)
-        || !read_unsigned(reader,values,0x80804F31,1,4,false)
-        || !read_unsigned(reader,values,0x80804F31,2,16,true)
-        || !read_unsigned(reader,values,0x80804F31,3,16,true)) return NativeStatus::malformed;
-    bool exists{};
-    if (!present(reader,true,exists)) return NativeStatus::malformed;
-    if (exists && !read_unsigned(reader,values,0x80804F34,0,32,false)) return NativeStatus::malformed;
-    return read_bool(reader,values,0x80804F31,5,false)
-        && read_signed(reader,values,0x80804F31,6,8,8,128,false)
-        && read_signed(reader,values,0x80804F2F,4,32,32,signedBias,false)
-        ? NativeStatus::complete : NativeStatus::malformed;
+    if (!read_signed(reader, values, kPlayerState, 0, 32, 32, signedBias, true)
+        || !read_unsigned(reader, values, kPlayerState, 1, 32, true)) {
+        return NativeStatus::malformed;
+    }
+    for (std::uint16_t ordinal = 2; ordinal <= 5; ++ordinal) {
+        if (!read_bool(reader, values, kPlayerState, ordinal, false)) return NativeStatus::malformed;
+    }
+    if (!read_signed(reader, values, kPlayerState, 6, 3, 8, 1, false)
+        || !read_signed(reader, values, kPlayerState, 7, 2, 8, 1, false)) {
+        return NativeStatus::malformed;
+    }
+    std::uint64_t count = 0;
+    if (!reader.read(3, count)) return NativeStatus::malformed;
+    if (count > kPlayerScalarCountMaximum) return NativeStatus::unsafeCount;
+    for (unsigned index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kPlayerScalars, 0, 32, false, index)
+            || !read_real(reader, values, kPlayerScalars, 1, 32, false, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_real(reader, values, kPlayerTail, 1, 32, false)
+        || !read_signed(reader, values, kPlayerTail, 2, 5, 32, 1, false)
+        || !read_signed(reader, values, kPlayerTail, 3, 32, 32, signedBias, true)) {
+        return NativeStatus::malformed;
+    }
+    for (std::uint16_t ordinal = 4; ordinal <= 6; ++ordinal) {
+        if (!read_bool(reader, values, kPlayerTail, ordinal, false)) return NativeStatus::malformed;
+    }
+    if (!read_unsigned(reader, values, kPlayerIdentity, 0, 64, true)
+        || !read_unsigned(reader, values, kPlayerIdentity, 1, 5, false)
+        || !read_signed(reader, values, kPlayerIdentity, 2, 6, 8, 3, false)) {
+        return NativeStatus::malformed;
+    }
+    if (!reader.read(6, count)) return NativeStatus::malformed;
+    if (count > kPlayerKeyCountMaximum) return NativeStatus::unsafeCount;
+    for (unsigned index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kPlayerKeys, 0, 32, false, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!reader.read(6, count)) return NativeStatus::malformed;
+    if (count > kPlayerKeyCountMaximum) return NativeStatus::unsafeCount;
+    for (unsigned index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kPlayerPairs, 0, 32, true, index)
+            || !read_unsigned(reader, values, kPlayerPairs, 1, 64, true, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_bool(reader, values, kPlayerFlags, 0, false)
+        || !read_unsigned(reader, values, kPlayerFlags, 1, 4, false)
+        || !read_unsigned(reader, values, kPlayerFlags, 2, 16, true)
+        || !read_unsigned(reader, values, kPlayerFlags, 3, 16, true)) {
+        return NativeStatus::malformed;
+    }
+    bool exists = false;
+    if (!present(reader, true, exists)) return NativeStatus::malformed;
+    if (exists && !read_unsigned(reader, values, kPlayerOptional, 0, 32, false)) {
+        return NativeStatus::malformed;
+    }
+    return read_bool(reader, values, kPlayerFlags, 5, false)
+                   && read_signed(reader, values, kPlayerFlags, 6, 8, 8, 128, false)
+                   && read_signed(reader, values, kPlayer, 4, 32, 32, signedBias, false)
+               ? NativeStatus::complete
+               : NativeStatus::malformed;
 }
 
 /** Decodes one root body of the given schema; the status names how far it got. */
@@ -641,19 +677,17 @@ decode_body(std::uint32_t schema, Reader& reader, Values& values) noexcept {
         return NativeStatus::complete;
     }
     switch (schema) {
-    case 0x80804F2F:
+    case kPlayer:
         return decode_player(reader, values);
-    case 0x80809562U:
-        // Reflected flags 0x21: mandatory raw floats (optional is bit 0x02).
-        // Native A774B0 publishes health/shield/revision as a fixed 12-byte state.
+    case kDamage:
+        // Three mandatory fields: health, shield, then the echoed Auth revision.
         return read_real(reader, values, schema, 0, 32, false)
             && read_real(reader, values, schema, 1, 32, false)
             && read_signed(reader, values, schema, 2, 32, 32,
                 (std::numeric_limits<std::int32_t>::min)(), false)
             ? NativeStatus::complete : NativeStatus::malformed;
     case kGhostLink:
-        // Native E4A590 publishes all three fields: active, elapsed/duration,
-        // and the accepted Auth generation. None has an optional presence bit.
+        // Three mandatory fields: active, elapsed over duration, accepted Auth generation.
         return read_bool(reader, values, kGhostLink, 0, false)
                        && read_real(reader, values, kGhostLink, 1, 32, false)
                        && read_signed(reader, values, kGhostLink, 2, 32, 32,

+ 23 - 14
Sunrise/src/middleware/bap/activity_message/activity_sensor_auth_bodies.cpp

@@ -30,6 +30,12 @@ constexpr std::size_t kConfigurationBits = 35;
 constexpr std::size_t kPackageBits = 7;
 constexpr std::size_t kQueueBits = 12;
 constexpr std::size_t kSpawnKeyBits = 32 * 32 + 1 + 32;
+/** Optional participation revive delay, a 16-bit half float: 30 s under darkness, else 3 s. */
+constexpr std::uint8_t kHalfWidth = 16;
+constexpr std::uint32_t kDarknessReviveDelayHalf = 0x4F80U;
+constexpr std::uint32_t kDefaultReviveDelayHalf = 0x4200U;
+/** A packed region is bubble times eight plus the state ordinal. */
+constexpr std::uint32_t kStatesPerBubble = 8;
 /**
  * Empty map-generator body, schema `0x80805007`.
  * Two 475-bit records, a u32, then fixed arrays of 32 and 64 u8. The fixed array lengths apply
@@ -115,18 +121,18 @@ constexpr std::size_t kSpawnKeyCount = 32;
            && writer.write(1, kPresenceWidth) && writer.write(snapshot.playerKey, 64)
            && writer.write(0, 5) && writer.write(3, 6) && writer.write(0, 6)
            && writer.write(0, 6)
-           // +736 bypasses DC3770's late spawn-location hold, NOT the HUD timer.
-           // The visible countdown is initialized by 12E9330 from +740 (half)
-           // into player+64, then displayed by 1677E60 via 12EE500/12EE590.
+           // Byte 736 skips the late spawn-location hold. Byte 737 holds the spawn while the
+           // client loads. The revive delay stays authored unless a darkness policy is set.
            && writer.write(1, kPresenceWidth)
            && writer.write(snapshot.awaitClientSync ? kAwaitingClientSync : 0U, 4)
-           && writer.write(0, kPresenceWidth) // +738: preserve authored revive delay.
-           && writer.write(snapshot.hasDarknessPolicy, kPresenceWidth)
+           && writer.write(0, kPresenceWidth)
+           && writer.write(snapshot.hasDarknessPolicy ? 1U : 0U, kPresenceWidth)
            && (!snapshot.hasDarknessPolicy
-               || writer.write(snapshot.darknessEnabled ? 0x4F80U : 0x4200U, 16)) // half 30 / 3
-           && writer.write(0, kPresenceWidth) // Optional spawn override absent.
-           && writer.write(0, kPresenceWidth) && writer.write(128, 8)
-           && writer.write(kSignedZero, 32);
+               || writer.write(snapshot.darknessEnabled ? kDarknessReviveDelayHalf
+                                                        : kDefaultReviveDelayHalf,
+                               kHalfWidth))
+           && writer.write(0, kPresenceWidth) && writer.write(0, kPresenceWidth)
+           && writer.write(128, 8) && writer.write(kSignedZero, 32);
 }
 
 /**
@@ -141,10 +147,13 @@ constexpr std::size_t kSpawnKeyCount = 32;
     bool encoded = writer.write(std::uint32_t{snapshot.lifetime} + kLifetimeBias, kLifetimeWidth)
                    && writer.write(1, 3) && writer.write(0, kPresenceWidth)
                    && writer.write(kSignedZero, 32) && writer.write(kEmptyNameHash, 32)
-                   && writer.write(snapshot.hasDarknessPolicy
-                           ? (snapshot.darknessEnabled && snapshot.hasRegion
-                                  ? kSignedZero + snapshot.region / 8U : kSignedMinusOne)
-                           : kSignedZero, 32) && writer.write(1, 6)
+                   // Under a darkness policy the lifetime names the bubble, or -1 when disabled.
+                   && writer.write(!snapshot.hasDarknessPolicy ? kSignedZero
+                                   : snapshot.darknessEnabled && snapshot.hasRegion
+                                       ? kSignedZero + snapshot.region / kStatesPerBubble
+                                       : kSignedMinusOne,
+                                   32)
+                   && writer.write(1, 6)
                    && writer.write(kWaitingSwitchKey, 32) && writer.write(1, kPresenceWidth)
                    && writer.write(kWaitingSwitchClass, 32) && writer.write(kSignedZero, 32)
                    && writer.write(kSignedZero, 32);
@@ -224,7 +233,7 @@ auth_body_bits(const Snapshot& snapshot, std::uint8_t slotType, bool carriesPlay
     if (slotType == kSlotTypeParticipation) {
         return carriesPlayerKey
                    ? kParticipationBits + (snapshot.hasRegion ? kParticipationRegionBits : 0)
-                         + (snapshot.hasDarknessPolicy ? 16U : 0U)
+                         + (snapshot.hasDarknessPolicy ? kHalfWidth : 0U)
                    : 0;
     }
     if (slotType == kSlotTypeLifetime) {

+ 9 - 6
Sunrise/src/middleware/bap/activity_message/activity_squad_auth_body_codec.cpp

@@ -31,6 +31,9 @@ constexpr std::uint8_t kModeWidth = 3;
 constexpr std::uint32_t kModeBias = 1;
 /** A present name hash is one exact unsigned 32-bit value. */
 constexpr std::uint8_t kNameHashWidth = 32;
+/** Logical active one becomes wire two after the schema bias; logical zero becomes one. */
+constexpr std::uint32_t kActiveWireValue = 2;
+constexpr std::uint32_t kInactiveWireValue = 1;
 /** Fields zero through two precede the requested-count block. */
 constexpr std::size_t kLeadingAbsentFieldCount = 3;
 /** One unused dynamic field separates requested counts from generation. */
@@ -123,13 +126,13 @@ constexpr std::size_t kAfterSpawnReferenceAbsentFieldCount = 5;
     }
     // A name the host does not own is the no-name value, never zero: `sub_7FF7421A9720`
     // compares this field against it and walks the member collection when it differs.
-    // Native 4E9550 consumes generation .6: .18=0 destroys owned actors through
-    // 4EC1A0 -> 56A8F0; .18=1 only kills them through 4EC410 -> 3BD720.
-    // A zero-request retirement/reset must release entities, not retain dead actors.
-    const bool hasRequests = std::any_of(preset.requestedCounts.begin(), preset.requestedCounts.end(),
-        [](std::int32_t count) { return count > 0; });
+    // Active reads inactive when nothing is requested, so the client destroys the owned actors
+    // instead of only killing them.
+    const bool hasRequests = std::any_of(preset.requestedCounts.begin(),
+                                         preset.requestedCounts.end(),
+                                         [](std::int32_t count) { return count > 0; });
     if (!write_absent(writer, kAfterSpawnReferenceAbsentFieldCount)
-        || !writer.write(hasRequests ? 2U : 1U, kActiveWidth)
+        || !writer.write(hasRequests ? kActiveWireValue : kInactiveWireValue, kActiveWidth)
         || !writer.write(static_cast<std::uint32_t>(mode) + kModeBias, kModeWidth)
         || !writer.write(1, kPresenceWidth)
         || !writer.write(preset.nameHash.value_or(kEmptyNameHash), kNameHashWidth)) {

+ 80 - 0
Sunrise/src/middleware/bap/activity_message/auth_fields.h

@@ -0,0 +1,80 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+#include "../../encoding/bit_writer.h"
+
+namespace sunrise::middleware::bap::activity_message::auth_fields {
+
+/** One fixed-width wire field. The row that builds it states what the value asserts. */
+struct Field final {
+    std::uint64_t value{};
+    std::uint8_t width{};
+};
+
+/** Presence bit that precedes every optional schema field. */
+inline constexpr std::uint8_t kPresenceWidth = 1;
+/** A schema boolean is one bit. */
+inline constexpr std::uint8_t kBoolWidth = 1;
+/** Signed 32-bit schema fields store zero at the middle of the unsigned wire range. */
+inline constexpr std::uint32_t kSigned32Bias = 0x80000000U;
+/** Generations and revisions are 31-bit counters that the client accepts only while positive. */
+inline constexpr std::uint8_t kCounterWidth = 31;
+inline constexpr std::uint32_t kMaximumCounter = 0x7FFFFFFFU;
+/** A nested ClientRef is a 32-bit registry key, a 7-bit biased slot type and a 16-bit biased index.
+ */
+inline constexpr std::uint8_t kClientRefKeyWidth = 32;
+inline constexpr std::uint8_t kClientRefTypeWidth = 7;
+inline constexpr std::uint8_t kClientRefIndexWidth = 16;
+inline constexpr std::uint32_t kClientRefTypeBias = 1;
+inline constexpr std::uint32_t kClientRefIndexBias = 32'768;
+inline constexpr std::uint16_t kMaximumClientRefIndex = 32'767;
+/** The unset ClientRef carries the no-name hash, type zero and index -1. */
+inline constexpr std::uint32_t kClientRefAbsentKey = 0x811C9DC5U;
+inline constexpr std::size_t kClientRefBits =
+    kClientRefKeyWidth + kClientRefTypeWidth + kClientRefIndexWidth;
+
+/** Writes the fields in order. @return False when the writer runs out of room. */
+[[nodiscard]] inline bool write_fields(encoding::bits::Writer& writer,
+                                       std::span<const Field> fields) noexcept {
+    for (const Field& field : fields) {
+        if (!writer.write(field.value, field.width)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Writes one present ClientRef naming an exact slot. */
+[[nodiscard]] inline bool write_client_ref(encoding::bits::Writer& writer,
+                                           std::uint32_t registryKey,
+                                           std::uint32_t slotType,
+                                           std::uint16_t slotIndex) noexcept {
+    return writer.write(registryKey, kClientRefKeyWidth)
+           && writer.write(slotType + kClientRefTypeBias, kClientRefTypeWidth)
+           && writer.write(slotIndex + kClientRefIndexBias, kClientRefIndexWidth);
+}
+
+/** Writes the unset ClientRef. */
+[[nodiscard]] inline bool write_absent_client_ref(encoding::bits::Writer& writer) noexcept {
+    return writer.write(kClientRefAbsentKey, kClientRefKeyWidth)
+           && writer.write(0, kClientRefTypeWidth)
+           && writer.write(kClientRefIndexBias - 1U, kClientRefIndexWidth);
+}
+
+/** @return True when the writer holds exactly the expected bits and closes into the byte count. */
+[[nodiscard]] inline bool finish_exact(encoding::bits::Writer& writer,
+                                       std::size_t bits,
+                                       std::size_t bytes,
+                                       std::size_t& written) noexcept {
+    return writer.bit_count() == bits && writer.finish(written) && written == bytes;
+}
+
+/** @return True when a body's byte count is the one its bit count needs. */
+[[nodiscard]] constexpr bool bytes_match_bits(std::size_t bytes, std::size_t bits) noexcept {
+    return bytes == (bits + 7U) / 8U;
+}
+
+} // namespace sunrise::middleware::bap::activity_message::auth_fields

+ 0 - 40
Sunrise/src/middleware/bap/activity_message/authority_cleanup.h

@@ -1,40 +0,0 @@
-#pragma once
-
-#include "activity_host_control.h"
-#include "entity_authority.h"
-
-namespace sunrise::middleware::bap::activity_message::authority_cleanup {
-
-// Only the requesting client's explicit abandoned mask is eligible. Never infer
-// an all-slots reset from elapsed time, pressure, or a region's numeric range.
-[[nodiscard]] inline bool prepare(std::uint32_t messageType,
-                                  std::span<const std::byte> payload,
-                                  std::uint8_t currentEpoch,
-                                  host_control::PurgeAuthorityBody& output) noexcept {
-    host_control::PurgeAuthorityBody candidate{};
-    // Native 170B030 compares the promoted byte with current + 1; zero
-    // cannot wrap past 255. Never send a repeated epoch or guess a reset.
-    if (currentEpoch == 255) return false;
-    candidate.epoch = static_cast<std::uint8_t>(currentEpoch + 1U);
-    if (messageType == entity_authority::kRequestPurgeMessageType) {
-        entity_authority::PurgeRequest request{};
-        if (!entity_authority::parse_request_purge(payload, request)) return false;
-        candidate.slots = request.mask;
-        candidate.reason = static_cast<std::int8_t>(request.reason);
-    } else if (messageType == entity_authority::kAbandonMessageType) {
-        entity_authority::Release release{};
-        if (!entity_authority::parse_abandon(payload, release)) return false;
-        candidate.slots = release.mask;
-        candidate.reason = static_cast<std::int8_t>(release.reason);
-    } else {
-        // Msg33 can relinquish authority over live objects inside the occupied
-        // bubble. It is not a request to delete those objects.
-        return false;
-    }
-    bool any = false;
-    for (const auto byte : candidate.slots) any |= byte != std::byte{};
-    if (!any) return false;
-    output = candidate;
-    return true;
-}
-}

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

@@ -10,7 +10,7 @@ namespace sunrise::middleware::bap::activity_message::cinematic_incident {
 inline constexpr std::uint32_t kStartedTarget = 5'239;
 /** Global SObject row emitted for start failure or when the runtime stops owning the resource. */
 inline constexpr std::uint32_t kTerminatedTarget = 1'685;
-/** FNV-1(cinematic_skip), posted by native 1069BE0 through 106A530. */
+/** Global SObject row emitted when the player asks to skip a cinematic. */
 inline constexpr std::uint32_t kSkipTarget = 3'338;
 /** Effective type-17 incident metadata schema selected by the three cinematic SObject rows. */
 inline constexpr std::uint32_t kSchema = 0x808087BFU;

+ 0 - 55
Sunrise/src/middleware/bap/activity_message/combatant_action_auth.h

@@ -1,55 +0,0 @@
-#pragma once
-#include <array>
-#include <cstdint>
-#include <span>
-#include "../../encoding/bit_reader.h"
-#include "../../encoding/bit_writer.h"
-namespace sunrise::middleware::bap::activity_message::combatant_action {
-inline constexpr std::uint32_t kSchema = 0x80807DA1;
-inline constexpr std::size_t kBits = 254, kBytes = 32;
-struct Request {
-    std::uint32_t generation{}, revision{}, group{}, action{};
-};
-// One native kind-9 custom action, with no spatial target. Keep the spawn
-// generation while advancing the program revision; completion uses Sense .3.
-// A97800 -> AB4030 -> A054C0 resolves the authored group/action pair.
-[[nodiscard]] inline bool encode(const Request& request, std::span<std::byte> output) noexcept {
-    if (output.size() != kBytes || request.generation == 0 || request.generation > 0x7fffffff
-        || request.revision == 0 || request.revision > 0x7fffffff || request.action == 0 || request.action == 0x811c9dc5) return false;
-    std::array<std::byte, kBytes> bytes{};
-    encoding::bits::Writer w(bytes);
-    std::size_t written{};
-    const bool ok = w.write(1, 1) && w.write(request.generation, 31)
-        && w.write(1, 2) && w.write(1, 3) && w.write(1, 1)
-        && w.write(0, 2) && w.write(1, 1) // .4/.5 absent; .6 present
-        && w.write(request.revision, 31) && w.write(0, 6) && w.write(1, 6)
-        && w.write(1, 1) && w.write(10, 4) && w.write(1, 2) // kind 9, native completion
-        && w.write(request.group, 32) && w.write(request.action, 32)
-        && w.write(0x811c9dc5, 32) // no additional identity
-        && w.write(0x811c9dc5, 32) && w.write(0, 7) && w.write(32767, 16)
-        && w.write(0, 3) && w.write(127, 8) // no target, mode/marker -1
-        && w.write(0, 1) && w.bit_count() == kBits // .7 absent
-        && w.finish(written) && written == kBytes;
-    if (!ok) return false;
-    for (std::size_t i = 0; i < bytes.size(); ++i) output[i] = bytes[i];
-    return true;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (bits != kBits || input.size() != kBytes) return false;
-    encoding::bits::Reader r(input);
-    std::uint64_t v{}, generation{}, revision{};
-    return r.read(1, v) && v == 1 && r.read(31, generation) && generation > 0
-        && r.read(2, v) && v == 1 && r.read(3, v) && v == 1
-        && r.read(1, v) && v == 1 && r.read(2, v) && v == 0
-        && r.read(1, v) && v == 1 && r.read(31, revision) && revision > 0
-        && r.read(6, v) && v == 0 && r.read(6, v) && v == 1
-        && r.read(1, v) && v == 1 && r.read(4, v) && v == 10
-        && r.read(2, v) && v == 1 && r.read(32, v)
-        && r.read(32, v) && v != 0 && v != 0x811c9dc5
-        && r.read(32, v) && v == 0x811c9dc5
-        && r.read(32, v) && v == 0x811c9dc5
-        && r.read(7, v) && v == 0 && r.read(16, v) && v == 32767
-        && r.read(3, v) && v == 0 && r.read(8, v) && v == 127
-        && r.read(1, v) && v == 0 && r.read(2, v) && v == 0;
-}
-}

+ 268 - 0
Sunrise/src/middleware/bap/activity_message/combatant_auth.h

@@ -0,0 +1,268 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+#include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+#include "scriptable_auth_body.h"
+#include "squad_auth_body.h"
+
+// Type-2 combatant Auth bodies that drive one named actor: a movement path, a custom action, a
+// passenger delivery and a retirement. All four share the root prefix in fields .0 to .3.
+
+namespace sunrise::middleware::bap::activity_message::combatant_auth {
+
+namespace fields = auth_fields;
+
+inline constexpr std::uint32_t kSchema = scriptable_auth::kType2Schema;
+/** Root .1 mode and .2 marker are written as one; the client keeps the placement it has. */
+inline constexpr std::uint8_t kRootModeWidth = 2;
+inline constexpr std::uint8_t kRootMarkerWidth = 3;
+inline constexpr std::uint32_t kRootModeValue = 1;
+inline constexpr std::uint32_t kRootMarkerValue = 1;
+/** Field .6 program header: two 6-bit values written as 0 then 1. Meaning unverified. */
+inline constexpr std::uint8_t kProgramHeaderWidth = 6;
+inline constexpr std::uint32_t kProgramHeaderFirst = 0;
+inline constexpr std::uint32_t kProgramHeaderSecond = 1;
+/** Field .6 program kind, biased by one on the wire: 3 follows a path, 9 runs an action. */
+inline constexpr std::uint8_t kProgramKindWidth = 4;
+inline constexpr std::uint32_t kProgramKindBias = 1;
+inline constexpr std::uint32_t kPathProgramKind = 3;
+inline constexpr std::uint32_t kActionProgramKind = 9;
+/** Field .6 completion selector: logical zero with bias one, the native completion. */
+inline constexpr std::uint8_t kCompletionWidth = 2;
+inline constexpr std::uint32_t kCompletionValue = 1;
+/** A path program names a type-58 authored path and one of its two markers. */
+inline constexpr std::uint32_t kPathSlotType = 58;
+inline constexpr std::uint32_t kPathComponentClass = 0x80807D9BU;
+inline constexpr std::uint8_t kPathMarkerWidth = 8;
+/** Marker 0 is the path start, marker 1 its destination. */
+inline constexpr std::uint32_t kPathDestinationMarker = 1;
+/** An action program names an authored group and action hash, with no spatial target. */
+inline constexpr std::uint8_t kActionTargetModeWidth = 3;
+inline constexpr std::uint8_t kActionTargetMarkerWidth = 8;
+/** The 8-bit target marker stores -1 with a bias of 128. */
+inline constexpr std::uint32_t kActionNoTargetMarker = 127;
+/** Field .7 delivery manifest: a 4-bit squad count, then one type-1 squad ClientRef each. */
+inline constexpr std::uint8_t kManifestCountWidth = 4;
+inline constexpr std::size_t kMaximumManifestSquads = 8;
+/** Bits one manifest squad adds: its ClientRef. */
+inline constexpr std::size_t kManifestSquadBits = fields::kClientRefBits;
+
+/** Fixed bit and byte counts of each body. */
+inline constexpr std::size_t kPathBits = 156;
+inline constexpr std::size_t kPathBytes = 20;
+inline constexpr std::size_t kActionBits = 254;
+inline constexpr std::size_t kActionBytes = 32;
+inline constexpr std::size_t kDeliveryBits = 132;
+inline constexpr std::size_t kDeliveryBytes = 17;
+inline constexpr std::size_t kDeliveryMaximumBytes =
+    (kDeliveryBits + kManifestSquadBits * (kMaximumManifestSquads - 1) + 7) / 8;
+inline constexpr std::size_t kRetireBits = 77;
+inline constexpr std::size_t kRetireBytes = 10;
+
+/** One squad the delivery manifest names. */
+struct SquadReference final {
+    std::uint32_t registryKey{};
+    std::uint16_t squadIndex{};
+};
+
+struct PathRequest final {
+    std::uint32_t generation{};
+    std::uint32_t revision{};
+    std::uint32_t registryKey{};
+    std::uint16_t pathIndex{};
+};
+
+struct ActionRequest final {
+    std::uint32_t generation{};
+    std::uint32_t revision{};
+    std::uint32_t group{};
+    std::uint32_t action{};
+};
+
+/** @return True when a 31-bit counter is positive and representable. */
+[[nodiscard]] constexpr bool valid_counter(std::uint32_t value) noexcept {
+    return value != 0 && value <= fields::kMaximumCounter;
+}
+
+/** Writes root fields .0 to .3: the spawn generation, mode one, marker one and the enabled bit. */
+[[nodiscard]] inline bool
+write_root(encoding::bits::Writer& writer, std::uint32_t generation, bool enabled) noexcept {
+    const std::array<fields::Field, 5> root{{
+        {1, fields::kPresenceWidth},
+        {generation, fields::kCounterWidth},
+        {kRootModeValue, kRootModeWidth},
+        {kRootMarkerValue, kRootMarkerWidth},
+        {enabled ? 1U : 0U, fields::kBoolWidth},
+    }};
+    return fields::write_fields(writer, root);
+}
+
+/** Writes the .6 program header up to and including its kind and completion selector. */
+[[nodiscard]] inline bool write_program_header(encoding::bits::Writer& writer,
+                                               std::uint32_t revision,
+                                               std::uint32_t kind) noexcept {
+    const std::array<fields::Field, 9> header{{
+        {0, fields::kPresenceWidth}, // .4 absent
+        {0, fields::kPresenceWidth}, // .5 absent
+        {1, fields::kPresenceWidth}, // .6 present
+        {revision, fields::kCounterWidth},
+        {kProgramHeaderFirst, kProgramHeaderWidth},
+        {kProgramHeaderSecond, kProgramHeaderWidth},
+        {1, fields::kPresenceWidth},
+        {kind + kProgramKindBias, kProgramKindWidth},
+        {kCompletionValue, kCompletionWidth},
+    }};
+    return fields::write_fields(writer, header);
+}
+
+/**
+ * Encodes one movement program. The spawn generation stays, the program revision advances.
+ * @param output Exactly kPathBytes.
+ * @return False on an out-of-range counter or index, or a size mismatch.
+ */
+[[nodiscard]] inline bool encode_path(const PathRequest& request,
+                                      std::span<std::byte> output) noexcept {
+    if (output.size() != kPathBytes || !valid_counter(request.generation)
+        || !valid_counter(request.revision) || request.registryKey == 0
+        || request.pathIndex > fields::kMaximumClientRefIndex) {
+        return false;
+    }
+    encoding::bits::Writer writer(output);
+    std::size_t written = 0;
+    const std::array<fields::Field, 3> tail{{
+        {kPathDestinationMarker, kPathMarkerWidth},
+        {1, fields::kBoolWidth},     // follow the authored curve
+        {0, fields::kPresenceWidth}, // .7 absent
+    }};
+    return write_root(writer, request.generation, true)
+           && write_program_header(writer, request.revision, kPathProgramKind)
+           && fields::write_client_ref(
+               writer, request.registryKey, kPathSlotType, request.pathIndex)
+           && fields::write_fields(writer, tail)
+           && fields::finish_exact(writer, kPathBits, kPathBytes, written);
+}
+
+/**
+ * Encodes one custom action program with no spatial target.
+ * @param output Exactly kActionBytes.
+ * @return False on an out-of-range counter, a zero or no-name action, or a size mismatch.
+ */
+[[nodiscard]] inline bool encode_action(const ActionRequest& request,
+                                        std::span<std::byte> output) noexcept {
+    if (output.size() != kActionBytes || !valid_counter(request.generation)
+        || !valid_counter(request.revision) || request.action == 0
+        || request.action == fields::kClientRefAbsentKey) {
+        return false;
+    }
+    encoding::bits::Writer writer(output);
+    std::size_t written = 0;
+    const std::array<fields::Field, 3> identities{{
+        {request.group, 32},
+        {request.action, 32},
+        {fields::kClientRefAbsentKey, 32}, // no additional identity
+    }};
+    const std::array<fields::Field, 3> tail{{
+        {0, kActionTargetModeWidth},
+        {kActionNoTargetMarker, kActionTargetMarkerWidth},
+        {0, fields::kPresenceWidth}, // .7 absent
+    }};
+    return write_root(writer, request.generation, true)
+           && write_program_header(writer, request.revision, kActionProgramKind)
+           && fields::write_fields(writer, identities) && fields::write_absent_client_ref(writer)
+           && fields::write_fields(writer, tail)
+           && fields::finish_exact(writer, kActionBits, kActionBytes, written);
+}
+
+/**
+ * Encodes one delivery manifest: the reserved squads the actor carries. Field .6 stays absent
+ * so a delivery never replaces the movement program.
+ * @param output At least the manifest's byte count.
+ * @param written Receives the byte count. @param bits Receives the meaningful bit count.
+ * @return False on an empty, oversized or repeated manifest, or an out-of-range counter.
+ */
+[[nodiscard]] inline bool encode_delivery(std::uint32_t generation,
+                                          std::uint32_t revision,
+                                          std::span<const SquadReference> squads,
+                                          std::span<std::byte> output,
+                                          std::size_t& written,
+                                          std::size_t& bits) noexcept {
+    written = 0;
+    bits = 0;
+    if (!valid_counter(generation) || !valid_counter(revision) || squads.empty()
+        || squads.size() > kMaximumManifestSquads) {
+        return false;
+    }
+    for (std::size_t index = 0; index < squads.size(); ++index) {
+        if (squads[index].registryKey == 0
+            || squads[index].squadIndex > fields::kMaximumClientRefIndex) {
+            return false;
+        }
+        for (std::size_t prior = 0; prior < index; ++prior) {
+            if (squads[index].registryKey == squads[prior].registryKey
+                && squads[index].squadIndex == squads[prior].squadIndex) {
+                return false;
+            }
+        }
+    }
+    const std::size_t expectedBits = kDeliveryBits + kManifestSquadBits * (squads.size() - 1);
+    const std::size_t expectedBytes = (expectedBits + 7) / 8;
+    if (output.size() < expectedBytes) {
+        return false;
+    }
+    encoding::bits::Writer writer(output.first(expectedBytes));
+    const std::array<fields::Field, 5> manifestHeader{{
+        {0, fields::kPresenceWidth}, // .4 absent
+        {0, fields::kPresenceWidth}, // .5 absent
+        {0, fields::kPresenceWidth}, // .6 absent
+        {1, fields::kPresenceWidth}, // .7 present
+        {squads.size(), kManifestCountWidth},
+    }};
+    if (!write_root(writer, generation, true) || !fields::write_fields(writer, manifestHeader)) {
+        return false;
+    }
+    for (const SquadReference& squad : squads) {
+        if (!fields::write_client_ref(writer,
+                                      squad.registryKey,
+                                      static_cast<std::uint32_t>(squad_auth::kSlotType),
+                                      squad.squadIndex)) {
+            return false;
+        }
+    }
+    if (!writer.write(revision, fields::kCounterWidth)
+        || !fields::finish_exact(writer, expectedBits, expectedBytes, written)) {
+        return false;
+    }
+    bits = expectedBits;
+    return true;
+}
+
+/**
+ * Encodes one retirement: root .3 disabled, an empty delivery manifest and the delivery
+ * revision set to the generation. The client retires the actor on this new generation.
+ * @param output Exactly kRetireBytes.
+ */
+[[nodiscard]] inline bool encode_retire(std::uint32_t generation,
+                                        std::span<std::byte> output) noexcept {
+    if (output.size() != kRetireBytes || !valid_counter(generation)) {
+        return false;
+    }
+    encoding::bits::Writer writer(output);
+    std::size_t written = 0;
+    const std::array<fields::Field, 6> tail{{
+        {0, fields::kPresenceWidth}, // .4 absent
+        {0, fields::kPresenceWidth}, // .5 absent
+        {0, fields::kPresenceWidth}, // .6 absent
+        {1, fields::kPresenceWidth}, // .7 present
+        {0, kManifestCountWidth},    // no passengers
+        {generation, fields::kCounterWidth},
+    }};
+    return write_root(writer, generation, false) && fields::write_fields(writer, tail)
+           && fields::finish_exact(writer, kRetireBits, kRetireBytes, written);
+}
+
+} // namespace sunrise::middleware::bap::activity_message::combatant_auth

+ 0 - 69
Sunrise/src/middleware/bap/activity_message/combatant_delivery_auth.h

@@ -1,69 +0,0 @@
-#pragma once
-#include <array>
-#include <cstdint>
-#include <span>
-#include "../../encoding/bit_reader.h"
-#include "../../encoding/bit_writer.h"
-namespace sunrise::middleware::bap::activity_message::combatant_delivery {
-inline constexpr std::uint32_t kSchema = 0x80807DA1;
-inline constexpr std::size_t kBits = 132, kBytes = 17;
-struct Request {
-    std::uint32_t generation{}, revision{}, registryKey{};
-    std::uint16_t squadIndex{};
-};
-// AB6600 dispatches Auth .7 to the actor's 80807DB4 interface. Harvester component
-// 8080670A implements it at 1029330: create passengers in seats, finish after detach.
-// The reference MUST be a squad (type 1); its counts must be reserved beforehand.
-// Auth .6 remains absent so delivery cannot replace the movement program.
-struct SquadReference { std::uint32_t registryKey{}; std::uint16_t squadIndex{}; };
-inline constexpr std::size_t kMaxSquads = 8;
-inline constexpr std::size_t kMaxBytes = (kBits + 55 * (kMaxSquads - 1) + 7) / 8;
-[[nodiscard]] inline bool encode_many(std::uint32_t generation, std::uint32_t revision,
-    std::span<const SquadReference> squads, std::span<std::byte> output,
-    std::size_t& written, std::size_t& bits) noexcept {
-    written = bits = 0;
-    if (!generation || generation > 0x7fffffff || !revision || revision > 0x7fffffff
-        || squads.empty() || squads.size() > kMaxSquads) return false;
-    for (std::size_t i = 0; i < squads.size(); ++i) {
-        if (!squads[i].registryKey || squads[i].squadIndex > 32767) return false;
-        for (std::size_t j = 0; j < i; ++j)
-            if (squads[i].registryKey == squads[j].registryKey && squads[i].squadIndex == squads[j].squadIndex)
-                return false;
-    }
-    std::array<std::byte, kMaxBytes> b{};
-    encoding::bits::Writer w(b);
-    if (!(w.write(1,1) && w.write(generation,31)
-        && w.write(1,2) && w.write(1,3) && w.write(1,1)
-        && w.write(0,3) && w.write(1,1) && w.write(squads.size(),4))) return false;
-    for (const auto& squad : squads)
-        if (!(w.write(squad.registryKey,32) && w.write(2,7) && w.write(squad.squadIndex + 32768U,16))) return false;
-    if (!w.write(revision,31)) return false;
-    const auto usedBits = w.bit_count(); std::size_t used{};
-    if (!w.finish(used) || output.size() < used) return false;
-    for (std::size_t i = 0; i < used; ++i) output[i] = b[i];
-    written = used; bits = usedBits; return true;
-}
-[[nodiscard]] inline bool encode(const Request& q, std::span<std::byte> output) noexcept {
-    const std::array<SquadReference,1> squads{{{q.registryKey,q.squadIndex}}};
-    std::size_t written{}, bits{};
-    return output.size() == kBytes && encode_many(q.generation,q.revision,squads,output,written,bits);
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> b, std::size_t bits) noexcept {
-    if (bits < kBits || b.size() != (bits+7)/8 || b.size() > kMaxBytes) return false;
-    encoding::bits::Reader r(b); std::uint64_t v{}, count{};
-    if (!(r.read(1,v)&&v==1 && r.read(31,v)&&v>0
-        && r.read(2,v)&&v==1 && r.read(3,v)&&v==1 && r.read(1,v)&&v==1
-        && r.read(3,v)&&v==0 && r.read(1,v)&&v==1 && r.read(4,count)&&count>0&&count<=kMaxSquads
-        && bits == kBits + 55*(count-1))) return false;
-    std::array<SquadReference,kMaxSquads> seen{};
-    for (std::size_t i=0; i<count; ++i) {
-        if (!r.read(32,v) || !v) return false;
-        seen[i].registryKey=static_cast<std::uint32_t>(v);
-        if (!(r.read(7,v)&&v==2 && r.read(16,v)&&v>=32768)) return false;
-        seen[i].squadIndex=static_cast<std::uint16_t>(v-32768);
-        for (std::size_t j=0;j<i;++j)
-            if (seen[i].registryKey==seen[j].registryKey && seen[i].squadIndex==seen[j].squadIndex) return false;
-    }
-    return r.read(31,v)&&v>0 && r.read(static_cast<std::uint8_t>(b.size()*8-bits),v)&&v==0;
-}
-}

+ 0 - 56
Sunrise/src/middleware/bap/activity_message/combatant_path_auth.h

@@ -1,56 +0,0 @@
-#pragma once
-#include <array>
-#include <cstdint>
-#include <span>
-#include "../../encoding/bit_reader.h"
-#include "../../encoding/bit_writer.h"
-namespace sunrise::middleware::bap::activity_message::combatant_path {
-inline constexpr std::uint32_t kSchema = 0x80807DA1;
-inline constexpr std::size_t kBits = 156, kBytes = 20;
-struct Request {
-    std::uint32_t generation{}, revision{}, registryKey{};
-    std::uint16_t pathIndex{};
-};
-// AB6340 creates the authored member. Auth .6 runs one kind-3 movement command:
-// A97BB0 -> 4FFEC0 resolves a type-58 authored path; native AI follows its curve.
-// Auth .7 is a squad-reference delivery request, NOT a movement path list.
-// Preserve spawn generation while advancing the action-program revision. The authored
-// entry/exit curves have two markers: marker 0 is the START, marker 1 the destination.
-// AB0C30 resolves the marker index, not an action index or a duration in seconds.
-[[nodiscard]] inline bool encode(const Request& request, std::span<std::byte> output) noexcept {
-    if (output.size() != kBytes || request.generation == 0 || request.generation > 0x7fffffff
-        || request.revision == 0 || request.revision > 0x7fffffff || request.registryKey == 0
-        || request.pathIndex > 32767) return false;
-    std::array<std::byte, kBytes> bytes{};
-    encoding::bits::Writer w(bytes);
-    std::size_t written{};
-    const bool ok = w.write(1, 1) && w.write(request.generation, 31)
-        && w.write(1, 2) && w.write(1, 3) && w.write(1, 1)
-        && w.write(0, 2) && w.write(1, 1) // .4/.5 absent; .6 present
-        && w.write(request.revision, 31) && w.write(0, 6) && w.write(1, 6)
-        && w.write(1, 1) && w.write(4, 4) && w.write(1, 2) // kind 3, completion 0
-        && w.write(request.registryKey, 32) && w.write(59, 7)
-        && w.write(request.pathIndex + 32768U, 16)
-        && w.write(1, 8) && w.write(1, 1) // destination marker; follow curve
-        && w.write(0, 1) && w.bit_count() == kBits // .7 absent
-        && w.finish(written) && written == kBytes;
-    if (!ok) return false;
-    for (std::size_t i = 0; i < bytes.size(); ++i) output[i] = bytes[i];
-    return true;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (bits != kBits || input.size() != kBytes) return false;
-    encoding::bits::Reader r(input);
-    std::uint64_t v{}, generation{}, registry{}, index{}, revision{};
-    return r.read(1, v) && v == 1 && r.read(31, generation) && generation > 0
-        && r.read(2, v) && v == 1 && r.read(3, v) && v == 1
-        && r.read(1, v) && v == 1 && r.read(2, v) && v == 0
-        && r.read(1, v) && v == 1 && r.read(31, revision) && revision > 0
-        && r.read(6, v) && v == 0 && r.read(6, v) && v == 1
-        && r.read(1, v) && v == 1 && r.read(4, v) && v == 4
-        && r.read(2, v) && v == 1 && r.read(32, registry) && registry != 0
-        && r.read(7, v) && v == 59 && r.read(16, index) && index >= 32768
-        && r.read(8, v) && v == 1 && r.read(1, v) && v == 1
-        && r.read(1, v) && v == 0 && r.read(4, v) && v == 0;
-}
-}

+ 0 - 33
Sunrise/src/middleware/bap/activity_message/combatant_retire_auth.h

@@ -1,33 +0,0 @@
-#pragma once
-#include <array>
-#include <cstdint>
-#include <span>
-#include "../../encoding/bit_reader.h"
-#include "../../encoding/bit_writer.h"
-namespace sunrise::middleware::bap::activity_message::combatant_retire {
-inline constexpr std::uint32_t kSchema = 0x80807DA1;
-inline constexpr std::size_t kBits = 77, kBytes = 10;
-// The live Auth callback AB6E20 -> AB71E0 retires only on a NEW generation.
-// Enabled=false alone only prevents creation in AB7350; it does not remove the actor.
-// Clear delivery .7 in the same update: AB7130 must not replay old cargo after reset.
-// Root .1 mode0 takes native detach + entity destruction (AB89A0 -> 56A8F0).
-[[nodiscard]] inline bool encode(std::uint32_t generation, std::span<std::byte> output) noexcept {
-    if (output.size() != kBytes || !generation || generation > 0x7fffffff) return false;
-    std::array<std::byte, kBytes> b{};
-    encoding::bits::Writer w(b); std::size_t written{};
-    const bool ok = w.write(1,1) && w.write(generation,31)
-        && w.write(1,2) && w.write(1,3) && w.write(0,1) && w.write(0,3) && w.write(1,1)
-        && w.write(0,4) && w.write(generation,31)
-        && w.bit_count() == kBits && w.finish(written) && written == kBytes;
-    if (ok) for (std::size_t i=0;i<kBytes;++i) output[i]=b[i];
-    return ok;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> b, std::size_t bits) noexcept {
-    if (b.size()!=kBytes || bits!=kBits) return false;
-    encoding::bits::Reader r(b); std::uint64_t v{};
-    return r.read(1,v)&&v==1 && r.read(31,v)&&v>0 && r.read(2,v)&&v==1
-        && r.read(3,v)&&v==1 && r.read(1,v)&&v==0 && r.read(3,v)&&v==0
-        && r.read(1,v)&&v==1 && r.read(4,v)&&v==0 && r.read(31,v)&&v>0
-        && r.read(3,v)&&v==0;
-}
-}

+ 47 - 0
Sunrise/src/middleware/bap/activity_message/damage_monitor_auth.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+#include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+
+// Type-20 damage monitor. The body binds the monitor to one type-4 object and carries a revision
+// the client echoes with the object's health and shield fractions.
+
+namespace sunrise::middleware::bap::activity_message::damage_monitor {
+
+namespace fields = auth_fields;
+
+inline constexpr std::uint8_t kSlotType = 20;
+inline constexpr std::uint32_t kComponentClass = 0x80809560U;
+inline constexpr std::uint32_t kAuthSchema = 0x80809563U;
+inline constexpr std::uint32_t kSenseSchema = 0x80809562U;
+/** The watched object is a type-4 object slot. */
+inline constexpr std::uint32_t kTargetSlotType = 4;
+inline constexpr std::size_t kBits = fields::kClientRefBits + 32;
+inline constexpr std::size_t kBytes = (kBits + 7) / 8;
+
+/**
+ * Encodes the monitor body.
+ * @param revision Positive revision; a change re-binds the monitor.
+ * @param written Receives kBytes.
+ */
+[[nodiscard]] inline bool encode(std::uint32_t registryKey,
+                                 std::uint16_t slotIndex,
+                                 std::int32_t revision,
+                                 std::span<std::byte> output,
+                                 std::size_t& written) noexcept {
+    written = 0;
+    if (registryKey == 0 || revision <= 0 || slotIndex > fields::kMaximumClientRefIndex
+        || output.size() < kBytes) {
+        return false;
+    }
+    encoding::bits::Writer writer(output.first(kBytes));
+    return fields::write_client_ref(writer, registryKey, kTargetSlotType, slotIndex)
+           && writer.write(static_cast<std::uint32_t>(revision) + fields::kSigned32Bias, 32)
+           && fields::finish_exact(writer, kBits, kBytes, written);
+}
+
+} // namespace sunrise::middleware::bap::activity_message::damage_monitor

+ 89 - 38
Sunrise/src/middleware/bap/activity_message/darkness_zone_auth.h

@@ -1,47 +1,98 @@
 #pragma once
+
 #include <array>
+#include <cstddef>
 #include <cstdint>
 #include <span>
+
 #include "../../encoding/bit_reader.h"
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+
+// Type-35 hard-wipe globals sensor. Field .0 enables the darkness restriction; the timer block
+// selects the native wipe countdown, published in whole seconds with the elapsed value clamped.
+
 namespace sunrise::middleware::bap::activity_message::darkness_zone {
-inline constexpr std::uint32_t kSchema=0x808099BF, kClass=0x808099BD;
-inline constexpr std::uint8_t kSlotType=35;
-inline constexpr std::size_t kBits=359, kBytes=45;
-// E4C110 reads .0; E4C0A0 additionally compares lifetime +12 with the current bubble.
-// .3=0 selects the native wipe countdown (5011E0/BD4860). The host publishes
-// whole-second steps with the timer clamped at the current elapsed value. Native
-// 4C8FF0/4C8E90 then expose remaining and elapsed without needing a guessed client
-// clock epoch. Native durations use673200 ticks/second (35F080/35FC40).
-[[nodiscard]] inline bool encode(bool enabled, std::span<std::byte> output,
-    int wipeSeconds=-1) noexcept {
-    if (output.size()!=kBytes || wipeSeconds < -1 || wipeSeconds>3 || (!enabled && wipeSeconds>=0)) return false;
-    const bool wipe=wipeSeconds>=0;
-    std::array<std::byte,kBytes> b{}; encoding::bits::Writer w(b); std::size_t used{};
-    if (!(w.write(enabled,1)&&w.write(0,1)&&w.write(1,2)&&w.write(wipe?1:0,2)&&w.write(wipe,1))) return false;
-    const std::uint64_t elapsed=wipe ? (3-wipeSeconds)*673200ULL : 0;
-    const std::uint64_t remaining=wipe ? wipeSeconds*673200ULL : 0;
-    for (const auto value:std::array<std::uint64_t,5>{elapsed,elapsed,elapsed,remaining,0})
-        if (!w.write(value,64)) return false;
-    if (!(w.write(wipe?0x3F800000U:0U,32)&&w.bit_count()==kBits&&w.finish(used)&&used==kBytes)) return false;
-    for (std::size_t i=0;i<kBytes;++i) output[i]=b[i];
-    return true;
-}
-[[nodiscard]] inline bool decode(std::span<const std::byte> body, std::size_t bits, bool& enabled) noexcept {
-    if (bits!=kBits||body.size()!=kBytes) return false;
-    encoding::bits::Reader r(body); std::uint64_t v{};
-    if (!r.read(1,v)) return false;
-    const bool active=v!=0;
-    if (!(r.read(1,v)&&v==0&&r.read(2,v)&&v==1&&r.read(2,v)&&v<=1)) return false;
-    const bool wipe=v==1;
-    if (!r.read(1,v) || v!=wipe || (wipe && !active)) return false;
-    std::array<std::uint64_t,5> timer{};
-    for (auto& word:timer) if (!r.read(64,word)) return false;
-    if (wipe) {
-        if (timer[0]!=timer[1] || timer[1]!=timer[2] || timer[2]>2019600
-            || timer[3]>2019600 || timer[2]+timer[3]!=2019600 || timer[3]%673200 || timer[4]) return false;
-    } else for (auto word:timer) if (word) return false;
-    if (!(r.read(32,v)&&v==(wipe?0x3F800000U:0U)&&r.read(1,v)&&v==0)) return false;
-    enabled=active;return true;
+
+namespace fields = auth_fields;
+
+inline constexpr std::uint8_t kSlotType = 35;
+inline constexpr std::uint32_t kComponentClass = 0x808099BDU;
+inline constexpr std::uint32_t kSchema = 0x808099BFU;
+inline constexpr std::size_t kBits = 359;
+inline constexpr std::size_t kBytes = 45;
+/** Native durations count 673,200 ticks per second. */
+inline constexpr std::uint64_t kTicksPerSecond = 673'200;
+/** The wipe countdown runs three seconds. */
+inline constexpr int kMaximumWipeSeconds = 3;
+/** No countdown requested. */
+inline constexpr int kNoWipe = -1;
+/** Field .1 and .2 are written as 0 and 1. Meaning unverified. */
+inline constexpr std::uint8_t kSecondFieldWidth = 1;
+inline constexpr std::uint8_t kThirdFieldWidth = 2;
+inline constexpr std::uint32_t kThirdFieldValue = 1;
+/** Field .3 selects the countdown: 1 with bias one selects the native wipe timer. */
+inline constexpr std::uint8_t kTimerSelectWidth = 2;
+/** Five 64-bit tick words: elapsed three times, remaining, then zero. */
+inline constexpr std::size_t kTimerWordCount = 5;
+inline constexpr std::uint8_t kTimerWordWidth = 64;
+/** The trailing 32-bit scale is IEEE 1.0 while a countdown runs, else zero. */
+inline constexpr std::uint32_t kOneAsFloatBits = 0x3F800000U;
+
+/**
+ * Encodes the sensor body.
+ * @param enabled Field .0, the darkness restriction.
+ * @param wipeSeconds Remaining countdown seconds, or kNoWipe. A countdown needs enabled.
+ * @param output Exactly kBytes.
+ */
+[[nodiscard]] inline bool
+encode(bool enabled, std::span<std::byte> output, int wipeSeconds = kNoWipe) noexcept {
+    if (output.size() != kBytes || wipeSeconds < kNoWipe || wipeSeconds > kMaximumWipeSeconds
+        || (!enabled && wipeSeconds != kNoWipe)) {
+        return false;
+    }
+    const bool wipe = wipeSeconds != kNoWipe;
+    const std::uint64_t elapsed =
+        wipe ? static_cast<std::uint64_t>(kMaximumWipeSeconds - wipeSeconds) * kTicksPerSecond : 0;
+    const std::uint64_t remaining =
+        wipe ? static_cast<std::uint64_t>(wipeSeconds) * kTicksPerSecond : 0;
+    encoding::bits::Writer writer(output);
+    std::size_t written = 0;
+    const std::array<fields::Field, 5> header{{
+        {enabled ? 1U : 0U, fields::kBoolWidth},
+        {0, kSecondFieldWidth},
+        {kThirdFieldValue, kThirdFieldWidth},
+        {wipe ? 1U : 0U, kTimerSelectWidth},
+        {wipe ? 1U : 0U, fields::kBoolWidth},
+    }};
+    const std::array<fields::Field, kTimerWordCount> timer{{
+        {elapsed, kTimerWordWidth},
+        {elapsed, kTimerWordWidth},
+        {elapsed, kTimerWordWidth},
+        {remaining, kTimerWordWidth},
+        {0, kTimerWordWidth},
+    }};
+    return fields::write_fields(writer, header) && fields::write_fields(writer, timer)
+           && writer.write(wipe ? kOneAsFloatBits : 0U, 32)
+           && fields::finish_exact(writer, kBits, kBytes, written);
 }
+
+/**
+ * Reads field .0 back from a body this tree encoded, for the roster's darkness policy.
+ * @return False when the body is not this sensor's size.
+ */
+[[nodiscard]] inline bool
+read_enabled(std::span<const std::byte> body, std::size_t bits, bool& enabled) noexcept {
+    if (bits != kBits || body.size() != kBytes) {
+        return false;
+    }
+    encoding::bits::Reader reader(body);
+    std::uint64_t value = 0;
+    if (!reader.read(fields::kBoolWidth, value)) {
+        return false;
+    }
+    enabled = value != 0;
+    return true;
 }
+
+} // namespace sunrise::middleware::bap::activity_message::darkness_zone

+ 27 - 20
Sunrise/src/middleware/bap/activity_message/ghost_link_auth.h

@@ -1,39 +1,46 @@
 #pragma once
-#include <array>
+
 #include <cstddef>
 #include <cstdint>
 #include <span>
-#include "../../encoding/bit_reader.h"
+
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+
+// Type-65 Ghost-link sensor. Field .0 is a monotonic reset generation, .1 enables the native
+// interaction, and .2 keeps the authored interaction hash when it carries the no-name hash.
 
 namespace sunrise::middleware::bap::activity_message::ghost_link {
+
+namespace fields = auth_fields;
+
 inline constexpr std::uint8_t kSlotType = 65;
 inline constexpr std::uint32_t kComponentClass = 0x80804D31U;
 inline constexpr std::uint32_t kAuthSchema = 0x80804D3FU;
 inline constexpr std::uint32_t kSenseSchema = 0x80804D3EU;
 inline constexpr std::size_t kBitCount = 65;
 inline constexpr std::size_t kByteCount = 9;
-inline constexpr std::uint32_t kAuthoredInteraction = 0x811C9DC5U;
+/** Field .2 value that preserves the authored interaction. */
+inline constexpr std::uint32_t kAuthoredInteraction = fields::kClientRefAbsentKey;
 
-// E4DFB0: +0 is a monotonic reset generation, +4 enables native interaction,
-// and +8 preserves the authored interaction hash when set to the empty FNV hash.
-[[nodiscard]] inline bool encode(std::int32_t generation, bool enabled,
-                                 std::span<std::byte> output, std::size_t& written) noexcept {
+/**
+ * Encodes the sensor body.
+ * @param generation Positive reset generation.
+ * @param output At least kByteCount. @param written Receives kByteCount.
+ */
+[[nodiscard]] inline bool encode(std::int32_t generation,
+                                 bool enabled,
+                                 std::span<std::byte> output,
+                                 std::size_t& written) noexcept {
     written = 0;
-    if (generation <= 0 || output.size() < kByteCount) return false;
+    if (generation <= 0 || output.size() < kByteCount) {
+        return false;
+    }
     encoding::bits::Writer writer(output.first(kByteCount));
-    return writer.write(static_cast<std::uint32_t>(generation) + 0x80000000U, 32)
-           && writer.write(enabled ? 1U : 0U, 1)
+    return writer.write(static_cast<std::uint32_t>(generation) + fields::kSigned32Bias, 32)
+           && writer.write(enabled ? 1U : 0U, fields::kBoolWidth)
            && writer.write(kAuthoredInteraction, 32)
-           && writer.bit_count() == kBitCount && writer.finish(written) && written == kByteCount;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (input.size() != kByteCount || bits != kBitCount
-        || (std::to_integer<unsigned>(input.back()) & 0x7FU) != 0) return false;
-    encoding::bits::Reader reader(input);
-    std::uint64_t generation = 0, enabled = 0, interaction = 0;
-    return reader.read(32, generation) && generation > 0x80000000ULL
-           && reader.read(1, enabled) && reader.read(32, interaction)
-           && interaction == kAuthoredInteraction;
+           && fields::finish_exact(writer, kBitCount, kByteCount, written);
 }
+
 } // namespace sunrise::middleware::bap::activity_message::ghost_link

+ 77 - 44
Sunrise/src/middleware/bap/activity_message/interactable_object_auth.h

@@ -1,54 +1,87 @@
 #pragma once
-#include <algorithm>
+
 #include <array>
 #include <cstddef>
 #include <cstdint>
 #include <span>
-#include "../../encoding/bit_reader.h"
+
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+
+// Type-4 object Auth that spawns authored entry zero and subscribes to accepted player use.
+// The client answers the subscription on its Sense reply list with the interaction latch.
+
 namespace sunrise::middleware::bap::activity_message::interactable_object {
+
+namespace fields = auth_fields;
+
+/** Object Auth schema; the SDK format table carries the same value as kObjectAuthSchema. */
 inline constexpr std::uint32_t kSchema = 0x8080992FU;
-inline constexpr std::uint32_t kReply = 0x80804FB7U;
-inline constexpr std::size_t kBits = 375, kBytes = 47;
-inline constexpr std::size_t kOwnerBits = kBits + 33, kOwnerBytes = (kOwnerBits + 7) / 8;
-// Authored entry/transform plus one interaction subscription. F32220 emits 80804FB7
-// only when 80804FB8 exists. Revision zero preserves the native one-shot used latch;
-// F36640, reached after accepted player use, is responsible for setting it.
-[[nodiscard]] inline bool encode(std::int32_t generation, std::span<std::byte> output,
-                                 std::size_t& written, bool trackOwner = false, bool active = true) noexcept {
+/** Registered reply the client emits for the interaction subscription below. */
+inline constexpr std::uint32_t kInteractionReply = 0x80804FB7U;
+/** Subscription schema that makes the client emit kInteractionReply. */
+inline constexpr std::uint32_t kInteractionSubscription = 0x80804FB8U;
+/** Optional ownership subscription; the client answers it with the owner key reply. */
+inline constexpr std::uint32_t kOwnershipSubscription = 0x80809ACDU;
+inline constexpr std::uint32_t kOwnershipReply = 0x80809ACCU;
+inline constexpr std::size_t kBits = 375;
+inline constexpr std::size_t kBytes = 47;
+/** The ownership subscription adds its presence bit and schema. */
+inline constexpr std::size_t kOwnerBits = kBits + fields::kPresenceWidth + 32;
+inline constexpr std::size_t kOwnerBytes = (kOwnerBits + 7) / 8;
+/** Reply subscription count: one, or two with ownership. */
+inline constexpr std::uint8_t kSubscriptionCountWidth = 2;
+/** Use-visibility mode. Wire zero is native -1, which lets use through with no player filter. */
+inline constexpr std::uint8_t kUseVisibilityWidth = 2;
+inline constexpr std::uint32_t kUseVisibilityDefault = 0;
+
+/**
+ * Encodes the object body.
+ * @param generation Positive object generation; a new one respawns the entry.
+ * @param trackOwner Adds the ownership subscription.
+ * @param active Field .2, whether the entry is active.
+ * @param written Receives kBytes or kOwnerBytes.
+ */
+[[nodiscard]] inline bool encode(std::int32_t generation,
+                                 std::span<std::byte> output,
+                                 std::size_t& written,
+                                 bool trackOwner = false,
+                                 bool active = true) noexcept {
     written = 0;
-    const auto bytes = trackOwner ? kOwnerBytes : kBytes;
-    const auto bits = trackOwner ? kOwnerBits : kBits;
-    if (generation <= 0 || output.size() < bytes) return false;
-    encoding::bits::Writer w(output.first(bytes));
-    const auto absent = [&]() { return w.write(0x811C9DC5U,32) && w.write(0,7) && w.write(0x7FFF,16); };
-    return w.write(static_cast<std::uint32_t>(generation)+0x80000000U,32)
-        && w.write(0x80000000U,32) && w.write(active ? 1 : 0,1) && w.write(0,1)
-        && w.write(0x80000000U,32) && absent()
-        && w.write(0,32) && w.write(0,32) && w.write(0,32) && w.write(0,1)
-        && w.write(trackOwner ? 2 : 1,2) && w.write(1,1) && w.write(0x80804FB8U,32)
-        // F33930 maps native -1 (wire 0) to +704=false. F32CD0 permits use
-        // when that flag is false and no player filter is attached. Native +1
-        // (wire 2) inverts that default and HIDES use without a matching filter.
-        // Authored item/geometry criteria are still evaluated by F32CD0.
-        && w.write(0,2) && absent() && w.write(0x80000000U,32) && w.write(0,1)
-        // Native 9F0750 accepts empty 80809ACD and returns ownership in 80809ACC.
-        && (!trackOwner || (w.write(1,1) && w.write(0x80809ACDU,32)))
-        && w.bit_count()==bits && w.finish(written) && written==bytes;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    const bool owner = bits == kOwnerBits;
-    if ((!owner && bits != kBits) || input.size() != (owner ? kOwnerBytes : kBytes)) return false;
-    encoding::bits::Reader reader(input);
-    std::uint64_t generation{};
-    if (!reader.read(32,generation) || generation <= 0x80000000ULL) return false;
-    std::uint64_t active{};
-    if (!reader.skip(32) || !reader.read(1, active)) return false;
-    std::uint64_t mode{};
-    if (!reader.skip(220) || !reader.read(2, mode) || mode != 0) return false;
-    std::array<std::byte,kOwnerBytes> canonical{};
-    std::size_t written{};
-    return encode(static_cast<std::int32_t>(generation-0x80000000ULL),canonical,written,owner,active != 0)
-        && std::equal(input.begin(),input.end(),canonical.begin());
-}
+    const std::size_t bytes = trackOwner ? kOwnerBytes : kBytes;
+    const std::size_t bits = trackOwner ? kOwnerBits : kBits;
+    if (generation <= 0 || output.size() < bytes) {
+        return false;
+    }
+    encoding::bits::Writer writer(output.first(bytes));
+    const std::array<fields::Field, 5> head{{
+        {static_cast<std::uint32_t>(generation) + fields::kSigned32Bias, 32},
+        {fields::kSigned32Bias, 32}, // signed zero
+        {active ? 1U : 0U, fields::kBoolWidth},
+        {0, fields::kBoolWidth},
+        {fields::kSigned32Bias, 32}, // signed zero
+    }};
+    const std::array<fields::Field, 7> transform{{
+        {0, 32},
+        {0, 32},
+        {0, 32},
+        {0, fields::kBoolWidth},
+        {trackOwner ? 2U : 1U, kSubscriptionCountWidth},
+        {1, fields::kPresenceWidth},
+        {kInteractionSubscription, 32},
+    }};
+    const std::array<fields::Field, 1> visibility{{{kUseVisibilityDefault, kUseVisibilityWidth}}};
+    const std::array<fields::Field, 2> tail{{
+        {fields::kSigned32Bias, 32}, // signed zero
+        {0, fields::kBoolWidth},
+    }};
+    return fields::write_fields(writer, head) && fields::write_absent_client_ref(writer)
+           && fields::write_fields(writer, transform) && fields::write_fields(writer, visibility)
+           && fields::write_absent_client_ref(writer) && fields::write_fields(writer, tail)
+           && (!trackOwner
+               || (writer.write(1, fields::kPresenceWidth)
+                   && writer.write(kOwnershipSubscription, 32)))
+           && fields::finish_exact(writer, bits, bytes, written);
 }
+
+} // namespace sunrise::middleware::bap::activity_message::interactable_object

+ 251 - 80
Sunrise/src/middleware/bap/activity_message/mission_auth_patch.h

@@ -1,100 +1,271 @@
 #pragma once
+
 #include <array>
 #include <cstddef>
 #include <cstdint>
 #include <span>
+
 #include "../../encoding/bit_reader.h"
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+#include "scriptable_auth_body.h"
+#include "squad_auth_body.h"
+
+// A native sensor override replaces the whole Auth object. A mission API body carries only the
+// root fields it sets, so it is composed over the last transported body before it is staged.
+// TODO: the field tables below repeat the squad and combatant layouts their codecs own. Read the
+// layout from the codecs once they expose it.
 
 namespace sunrise::middleware::bap::activity_message::mission_auth_patch {
-// Native sensor overrides replace the full Auth object. An absent root field in a
-// mission API call means preserve it, so materialize against the last staged body.
-// This deliberately supports only the squad and named-transport layouts these APIs
-// emit. Unsupported nested programs are refused rather than losing retained state.
+
+namespace fields = auth_fields;
+
+/** Largest body either layout produces. */
 inline constexpr std::size_t kCapacity = 256;
-struct Field { std::size_t offset{}, bits{}; bool present{}; };
-struct Layout { std::array<Field,21> fields{}; std::size_t count{}; };
-[[nodiscard]] inline bool parse(std::uint32_t schema, std::span<const std::byte> body,
-                                std::size_t bits, Layout& out) noexcept {
-    if (body.empty() || body.size()>kCapacity || body.size()!=(bits+7)/8) return false;
-    const bool squad = schema==0x80807EC9U;
-    if (!squad && schema!=0x80807DA1U) return false;
-    encoding::bits::Reader r(body); out={}; out.count=squad?21:8;
-    auto position = [&] { return body.size()*8-r.remaining_bits(); };
-    for (std::size_t i=0;i<out.count;++i) {
-        auto& f=out.fields[i]; f.offset=position();
-        const bool optional=squad?(i!=18 && i!=19):(i==0 || i>=4);
-        std::uint64_t v=1;
-        if (optional && !r.read(1,v)) return false;
-        f.present=v!=0;
-        if (f.present) {
-            std::size_t width=0;
-            if (squad) {
-                if (i==0 || i==1 || (i>=9 && i<=12)) width=55;
-                else if (i==2) return false;
-                else if (i==3 || i==4) {
-                    if (!r.read(4,v) || v>8) return false;
-                    width=32*v;
-                } else if (i==5) width=13;
-                else if (i==6 || i==13 || i==14 || i==17) width=31;
-                else if (i==7 || i==8 || i==20) width=32;
-                else if (i==15) width=6;
-                else if (i==16) width=5;
-                else if (i==18) width=2;
-                else if (i==19) width=3;
-            } else {
-                if (i==0) width=31;
-                else if (i==1) width=2;
-                else if (i==2) width=3;
-                else if (i==3) width=1;
-                else if (i==4 || i==5) return false;
-                else if (i==6) {
-                    if (!r.skip(31+6) || !r.read(6,v) || v!=1
-                        || !r.read(1,v) || v!=1 || !r.read(4,v) || (v!=4 && v!=10)) return false;
-                    const auto kind = v;
-                    if (!r.read(2,v) || v!=1) return false;
-                    width=kind==4 ? 64 : 162; // kind-3 ClientRef, marker byte, enabled bit
-                } else if (i==7) {
-                    if (!r.read(4,v) || v>8) return false;
-                    width=55*v+31;
-                }
+/** Root field counts of the two supported layouts. */
+inline constexpr std::size_t kSquadFieldCount = 21;
+inline constexpr std::size_t kCombatantFieldCount = 8;
+
+/** How one root field's width is found. */
+enum class Kind : std::uint8_t {
+    /** Fixed width. */
+    fixed,
+    /** A 4-bit count of 32-bit values. */
+    counted32,
+    /** A field this composer does not carry; a present one refuses the body. */
+    refused,
+    /** Combatant field .6: the program header, then a kind-specific body. */
+    program,
+    /** Combatant field .7: a 4-bit squad count, one ClientRef each, then a 31-bit revision. */
+    manifest,
+};
+
+/** One root field: whether a presence bit precedes it, and how wide it is. */
+struct FieldRule final {
+    Kind kind{Kind::fixed};
+    bool optional{true};
+    std::uint16_t width{};
+};
+
+/** Squad Auth root fields .0 to .20. Widths are bits after the presence bit. */
+inline constexpr std::array<FieldRule, kSquadFieldCount> kSquadRules{{
+    {Kind::fixed, true, fields::kClientRefBits}, // .0 objective ClientRef
+    {Kind::fixed, true, fields::kClientRefBits}, // .1
+    {Kind::refused, true, 0},                    // .2
+    {Kind::counted32, true, 0},                  // .3 requested counts
+    {Kind::counted32, true, 0},                  // .4
+    {Kind::fixed, true, 13},                     // .5 authored profile
+    {Kind::fixed, true, 31},                     // .6 spawn generation
+    {Kind::fixed, true, 32},                     // .7
+    {Kind::fixed, true, 32},                     // .8
+    {Kind::fixed, true, fields::kClientRefBits}, // .9
+    {Kind::fixed, true, fields::kClientRefBits}, // .10
+    {Kind::fixed, true, fields::kClientRefBits}, // .11 spawn reference
+    {Kind::fixed, true, fields::kClientRefBits}, // .12 spawn reference
+    {Kind::fixed, true, 31},                     // .13 objective revision
+    {Kind::fixed, true, 31},                     // .14
+    {Kind::fixed, true, 6},                      // .15
+    {Kind::fixed, true, 5},                      // .16 task group
+    {Kind::fixed, true, 31},                     // .17
+    {Kind::fixed, false, 2},                     // .18 active
+    {Kind::fixed, false, 3},                     // .19 mode
+    {Kind::fixed, true, 32},                     // .20 name hash
+}};
+
+/** Combatant Auth root fields .0 to .7. */
+inline constexpr std::array<FieldRule, kCombatantFieldCount> kCombatantRules{{
+    {Kind::fixed, true, 31},   // .0 spawn generation
+    {Kind::fixed, false, 2},   // .1 mode
+    {Kind::fixed, false, 3},   // .2 marker
+    {Kind::fixed, false, 1},   // .3 enabled
+    {Kind::refused, true, 0},  // .4
+    {Kind::refused, true, 0},  // .5
+    {Kind::program, true, 0},  // .6
+    {Kind::manifest, true, 0}, // .7
+}};
+
+/** Combatant .6 program layout: revision, two 6-bit header words, a presence bit, kind, completion.
+ */
+inline constexpr std::size_t kProgramLeadBits = 31 + 6;
+inline constexpr std::uint8_t kProgramHeaderWidth = 6;
+inline constexpr std::uint32_t kProgramHeaderSecond = 1;
+inline constexpr std::uint8_t kProgramKindWidth = 4;
+inline constexpr std::uint8_t kCompletionWidth = 2;
+inline constexpr std::uint32_t kCompletionValue = 1;
+/** Biased program kinds this composer carries: a path (3) and an action (9). */
+inline constexpr std::uint32_t kPathKindWire = 4;
+inline constexpr std::uint32_t kActionKindWire = 10;
+/** Bits after the completion selector: a path ClientRef, marker and flag; or the action body. */
+inline constexpr std::size_t kPathBodyBits = fields::kClientRefBits + 8 + 1;
+inline constexpr std::size_t kActionBodyBits = 32 + 32 + 32 + fields::kClientRefBits + 3 + 8;
+/** Combatant .7 manifest: a 4-bit count of ClientRefs, then a 31-bit delivery revision. */
+inline constexpr std::uint8_t kCountWidth = 4;
+inline constexpr std::size_t kMaximumCount = 8;
+
+/** Where one root field sits in a parsed body. */
+struct Field final {
+    std::size_t offset{};
+    std::size_t bits{};
+    bool present{};
+};
+
+struct Layout final {
+    std::array<Field, kSquadFieldCount> fields{};
+    std::size_t count{};
+};
+
+/** @return The rules for one supported schema, or an empty span. */
+[[nodiscard]] inline std::span<const FieldRule> rules_for(std::uint32_t schema) noexcept {
+    if (schema == squad_auth::kSchema) {
+        return kSquadRules;
+    }
+    if (schema == scriptable_auth::kType2Schema) {
+        return kCombatantRules;
+    }
+    return {};
+}
+
+/** Skips a combatant .6 program body. @return False on a kind this composer does not carry. */
+[[nodiscard]] inline bool skip_program(encoding::bits::Reader& reader) noexcept {
+    std::uint64_t value = 0;
+    if (!reader.skip(kProgramLeadBits) || !reader.read(kProgramHeaderWidth, value)
+        || value != kProgramHeaderSecond || !reader.read(fields::kPresenceWidth, value)
+        || value != 1 || !reader.read(kProgramKindWidth, value)
+        || (value != kPathKindWire && value != kActionKindWire)) {
+        return false;
+    }
+    const std::uint64_t kind = value;
+    return reader.read(kCompletionWidth, value) && value == kCompletionValue
+           && reader.skip(kind == kPathKindWire ? kPathBodyBits : kActionBodyBits);
+}
+
+/** Skips a combatant .7 manifest. */
+[[nodiscard]] inline bool skip_manifest(encoding::bits::Reader& reader) noexcept {
+    std::uint64_t count = 0;
+    return reader.read(kCountWidth, count) && count <= kMaximumCount
+           && reader.skip(fields::kClientRefBits * count + fields::kCounterWidth);
+}
+
+/**
+ * Locates every root field of one body.
+ * @param bits Meaningful bit count of the body.
+ * @return False on an unsupported schema, a refused field, a size mismatch or nonzero padding.
+ */
+[[nodiscard]] inline bool parse(std::uint32_t schema,
+                                std::span<const std::byte> body,
+                                std::size_t bits,
+                                Layout& out) noexcept {
+    const std::span<const FieldRule> rules = rules_for(schema);
+    if (rules.empty() || body.empty() || body.size() > kCapacity
+        || !fields::bytes_match_bits(body.size(), bits)) {
+        return false;
+    }
+    encoding::bits::Reader reader(body);
+    const auto position = [&] { return body.size() * 8 - reader.remaining_bits(); };
+    out = {};
+    out.count = rules.size();
+    for (std::size_t index = 0; index < rules.size(); ++index) {
+        const FieldRule& rule = rules[index];
+        Field& field = out.fields[index];
+        field.offset = position();
+        std::uint64_t present = 1;
+        if (rule.optional && !reader.read(fields::kPresenceWidth, present)) {
+            return false;
+        }
+        field.present = present != 0;
+        if (field.present) {
+            std::uint64_t count = 0;
+            bool skipped = false;
+            switch (rule.kind) {
+            case Kind::fixed:
+                skipped = reader.skip(rule.width);
+                break;
+            case Kind::counted32:
+                skipped = reader.read(kCountWidth, count) && count <= kMaximumCount
+                          && reader.skip(32 * count);
+                break;
+            case Kind::program:
+                skipped = skip_program(reader);
+                break;
+            case Kind::manifest:
+                skipped = skip_manifest(reader);
+                break;
+            case Kind::refused:
+                break;
+            }
+            if (!skipped) {
+                return false;
             }
-            if (!r.skip(width)) return false;
         }
-        f.bits=position()-f.offset;
+        field.bits = position() - field.offset;
     }
-    std::uint64_t padding{};
-    return position()==bits && r.read(static_cast<std::uint8_t>(body.size()*8-bits),padding) && padding==0;
+    std::uint64_t padding = 0;
+    return position() == bits
+           && reader.read(static_cast<std::uint8_t>(body.size() * 8 - bits), padding)
+           && padding == 0;
 }
-[[nodiscard]] inline bool copy_field(encoding::bits::Writer& w, std::span<const std::byte> body,
-                                     Field f) noexcept {
-    encoding::bits::Reader r(body);
-    if (!r.skip(f.offset)) return false;
-    while (f.bits) {
-        const auto n=static_cast<std::uint8_t>(f.bits>64?64:f.bits);
-        std::uint64_t v{};
-        if (!r.read(n,v) || !w.write(v,n)) return false;
-        f.bits-=n;
+
+/** Copies one located field, in 64-bit pieces, from a body into the writer. */
+[[nodiscard]] inline bool
+copy_field(encoding::bits::Writer& writer, std::span<const std::byte> body, Field field) noexcept {
+    encoding::bits::Reader reader(body);
+    if (!reader.skip(field.offset)) {
+        return false;
+    }
+    while (field.bits != 0) {
+        const auto piece = static_cast<std::uint8_t>(field.bits > 64 ? 64 : field.bits);
+        std::uint64_t value = 0;
+        if (!reader.read(piece, value) || !writer.write(value, piece)) {
+            return false;
+        }
+        field.bits -= piece;
     }
     return true;
 }
+
+/**
+ * Composes a patch over the previous body: a field the patch leaves absent keeps the previous
+ * value. With no previous body the patch is written as it is.
+ * @param written Receives the byte count. @param bits Receives the meaningful bit count.
+ * @return False when either body fails parse() or the result does not fit output.
+ */
 [[nodiscard]] inline bool compose(std::uint32_t schema,
-    std::span<const std::byte> previous, std::size_t previousBits,
-    std::span<const std::byte> patch, std::size_t patchBits,
-    std::span<std::byte> output, std::size_t& written, std::size_t& bits) noexcept {
-    written=bits=0;
-    Layout old{}, next{};
-    if (!parse(schema,patch,patchBits,next)
-        || (!previous.empty() && !parse(schema,previous,previousBits,old))) return false;
-    std::array<std::byte,kCapacity> staged{};
-    encoding::bits::Writer w(staged);
-    for (std::size_t i=0;i<next.count;++i) {
-        const bool keep=!previous.empty() && !next.fields[i].present && old.fields[i].present;
-        if (!copy_field(w,keep?previous:patch,keep?old.fields[i]:next.fields[i])) return false;
+                                  std::span<const std::byte> previous,
+                                  std::size_t previousBits,
+                                  std::span<const std::byte> patch,
+                                  std::size_t patchBits,
+                                  std::span<std::byte> output,
+                                  std::size_t& written,
+                                  std::size_t& bits) noexcept {
+    written = 0;
+    bits = 0;
+    Layout old{};
+    Layout next{};
+    if (!parse(schema, patch, patchBits, next)
+        || (!previous.empty() && !parse(schema, previous, previousBits, old))) {
+        return false;
     }
-    const auto usedBits=w.bit_count(); std::size_t used{};
-    if (!w.finish(used) || output.size()<used) return false;
-    for (std::size_t i=0;i<used;++i) output[i]=staged[i];
-    written=used; bits=usedBits; return true;
-}
+    std::array<std::byte, kCapacity> staged{};
+    encoding::bits::Writer writer(staged);
+    for (std::size_t index = 0; index < next.count; ++index) {
+        const bool keep =
+            !previous.empty() && !next.fields[index].present && old.fields[index].present;
+        if (!copy_field(
+                writer, keep ? previous : patch, keep ? old.fields[index] : next.fields[index])) {
+            return false;
+        }
+    }
+    const std::size_t usedBits = writer.bit_count();
+    std::size_t used = 0;
+    if (!writer.finish(used) || output.size() < used) {
+        return false;
+    }
+    for (std::size_t index = 0; index < used; ++index) {
+        output[index] = staged[index];
+    }
+    written = used;
+    bits = usedBits;
+    return true;
 }
+
+} // namespace sunrise::middleware::bap::activity_message::mission_auth_patch

+ 58 - 48
Sunrise/src/middleware/bap/activity_message/mission_effect_auth.h

@@ -1,56 +1,66 @@
 #pragma once
+
 #include <array>
+#include <cstddef>
+#include <cstdint>
 #include <span>
-#include "scriptable_auth_body.h"
+
 #include "../../encoding/bit_writer.h"
-#include "../../encoding/bit_reader.h"
+#include "auth_fields.h"
+#include "scriptable_auth_body.h"
+
+// Type-26 mission effect (hop-on). A new revision detaches the old effect, then the client
+// attaches the authored effect to the entities its type-34 filter selects. No filter removes all.
+
 namespace sunrise::middleware::bap::activity_message::mission_effect {
-inline constexpr std::size_t kBits=186, kBytes=24;
-// Native 9EF8A0/9F1F10 remove old attachments on a new revision, then 9EF940
-// attaches the authored hop-on to the filter's entities. An absent filter removes all.
-inline bool encode(scriptable_auth::Type2LaneClientRef filter, bool enabled,
-                   std::int32_t revision, std::span<std::byte> out, std::size_t& written) noexcept {
-    if (revision<=0 || out.size()<kBytes || (enabled && (filter.slotType!=34 || filter.slotIndex<0))) return false;
-    if (!enabled) filter={};
-    encoding::bits::Writer w(out.first(kBytes));
-    return w.write(0,1) && w.write(!enabled,1)
-        && w.write(0x80000000U,32) && w.write(0x80000000U,32) && w.write(0x80000000U,32)
-        && w.write(static_cast<std::uint32_t>(revision)+0x80000000U,32)
-        && w.write(filter.registryKey,32) && w.write(static_cast<unsigned>(filter.slotType)+1U,7)
-        && w.write(static_cast<std::int32_t>(filter.slotIndex)+32768,16)
-        && w.write(0,1) && w.bit_count()==kBits && w.finish(written) && written==kBytes;
-}
 
-inline bool validate_filter(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (bits<4 || bits>732 || input.size()!=(bits+7)/8) return false;
-    encoding::bits::Reader r(input); std::uint64_t count{},v{},schema{},mode{};
-    if (!r.read(4,count) || count>8) return false;
-    std::size_t consumed=4;
-    for (std::uint64_t i=0;i<count;++i) {
-        if (!r.read(1,v) || v!=1 || !r.read(32,schema) || !r.read(2,mode)) return false;
-        consumed+=35;
-        if (schema==0x8080957DU) { if (mode!=1 && mode!=2) return false; continue; }
-        if (schema==0x80809576U) {
-            if (!r.read(1,v) || !((mode==2 && v==0) || (mode==1 && v==1))) return false;
-            ++consumed;
-        } else if (schema!=0x80809579U || mode!=1) return false;
-        if (!r.read(32,v) || !r.read(7,v) || v!=(schema==0x80809576U ? 61U : 5U)
-            || !r.read(16,v) || v<32768U) return false;
-        consumed+=55;
+namespace fields = auth_fields;
+
+inline constexpr std::uint8_t kSlotType = 26;
+inline constexpr std::uint32_t kComponentClass = 0x8080953FU;
+inline constexpr std::uint32_t kSchema = 0x8080954BU;
+inline constexpr std::size_t kBits = 186;
+inline constexpr std::size_t kBytes = 24;
+
+/**
+ * Encodes the effect body.
+ * @param filter Type-34 filter slot; ignored when disabled.
+ * @param revision Positive revision; a change re-attaches.
+ * @param written Receives kBytes.
+ */
+[[nodiscard]] inline bool encode(scriptable_auth::Type2LaneClientRef filter,
+                                 bool enabled,
+                                 std::int32_t revision,
+                                 std::span<std::byte> output,
+                                 std::size_t& written) noexcept {
+    written = 0;
+    if (revision <= 0 || output.size() < kBytes
+        || (enabled
+            && (filter.slotType != scriptable_auth::kType34SlotType || filter.slotIndex < 0))) {
+        return false;
     }
-    return consumed==bits && (bits%8==0 || (std::to_integer<unsigned>(input.back()) & ((1U<<(8-bits%8))-1))==0);
-}
-inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (bits!=kBits || input.size()!=kBytes) return false;
-    encoding::bits::Reader r(input); std::uint64_t v{},disabled{},revision{},registry{},type{},index{};
-    if (!r.read(1,v)||v!=0||!r.read(1,disabled)) return false;
-    for (int i=0;i<3;++i) if (!r.read(32,v)||v!=0x80000000U) return false;
-    if (!r.read(32,revision)||revision<=0x80000000U||!r.read(32,registry)||!r.read(7,type)
-        ||!r.read(16,index)||!r.read(1,v)||v!=0) return false;
-    std::array<std::byte,kBytes> expected{};std::size_t written{};
-    if (!encode({static_cast<std::uint32_t>(registry),static_cast<std::int8_t>(type-1),static_cast<std::int16_t>(index-32768)},
-                !disabled,static_cast<std::int32_t>(revision-0x80000000U),expected,written)) return false;
-    for(std::size_t i=0;i<kBytes;++i)if(expected[i]!=input[i])return false;
-    return true;
-}
+    if (!enabled) {
+        filter = {};
+    }
+    encoding::bits::Writer writer(output.first(kBytes));
+    const std::array<fields::Field, 6> head{{
+        {0, fields::kBoolWidth},
+        {enabled ? 0U : 1U, fields::kBoolWidth}, // disabled
+        {fields::kSigned32Bias, 32},             // signed zero
+        {fields::kSigned32Bias, 32},             // signed zero
+        {fields::kSigned32Bias, 32},             // signed zero
+        {static_cast<std::uint32_t>(revision) + fields::kSigned32Bias, 32},
+    }};
+    return fields::write_fields(writer, head)
+           && writer.write(filter.registryKey, fields::kClientRefKeyWidth)
+           && writer.write(static_cast<std::uint32_t>(filter.slotType) + fields::kClientRefTypeBias,
+                           fields::kClientRefTypeWidth)
+           && writer.write(
+               static_cast<std::uint32_t>(static_cast<std::int32_t>(filter.slotIndex)
+                                          + static_cast<std::int32_t>(fields::kClientRefIndexBias)),
+               fields::kClientRefIndexWidth)
+           && writer.write(0, fields::kBoolWidth)
+           && fields::finish_exact(writer, kBits, kBytes, written);
 }
+
+} // namespace sunrise::middleware::bap::activity_message::mission_effect

+ 51 - 34
Sunrise/src/middleware/bap/activity_message/music_section_auth.h

@@ -1,44 +1,61 @@
 #pragma once
-#include <algorithm>
-#include <array>
+
+#include <cstddef>
 #include <cstdint>
 #include <span>
-#include "../../encoding/bit_reader.h"
+
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+
+// Type-11 music sensor. The body opens with a 128-bit section selection mask, then one optional
+// condition ClientRef per section plus a final one that gates the whole sensor. One set bit lets
+// the native bank play that section's transition.
 
 namespace sunrise::middleware::bap::activity_message::music_section {
-inline constexpr std::uint32_t kSchema = 0x80804F58U, kClass = 0x80804E8EU;
-inline constexpr std::size_t kBits = 7223, kBytes = (kBits + 7) / 8;
-// 1069A10 applies this Auth to the native music sensor; 106AEA0 evaluates it.
-// DEF9F0 treats 80809291 as a 128-bit selection mask (NOT four cue hashes),
-// followed by 128 optional condition refs. The final ref gates the whole sensor.
-// A single enabled authored section lets the native bank perform its transition.
-[[nodiscard]] inline bool encode(std::uint8_t section, bool enabled,
-    std::span<std::byte> output, std::size_t& written) noexcept {
+
+namespace fields = auth_fields;
+
+inline constexpr std::uint8_t kSlotType = 11;
+inline constexpr std::uint32_t kComponentClass = 0x80804E8EU;
+inline constexpr std::uint32_t kSchema = 0x80804F58U;
+/** The mask spans four 32-bit lanes, one bit per section. */
+inline constexpr std::size_t kSectionCount = 128;
+inline constexpr std::size_t kMaskLaneCount = 4;
+inline constexpr std::uint8_t kMaskLaneWidth = 32;
+/** One condition ClientRef per section, then the sensor gate. All are left unset. */
+inline constexpr std::size_t kConditionRefCount = kSectionCount + 1;
+inline constexpr std::size_t kBits =
+    kMaskLaneCount * kMaskLaneWidth + kConditionRefCount * fields::kClientRefBits;
+inline constexpr std::size_t kBytes = (kBits + 7) / 8;
+
+/**
+ * Encodes the sensor body with at most one section selected.
+ * @param section Section index below kSectionCount.
+ * @param enabled False clears the mask.
+ * @param written Receives kBytes.
+ */
+[[nodiscard]] inline bool encode(std::uint8_t section,
+                                 bool enabled,
+                                 std::span<std::byte> output,
+                                 std::size_t& written) noexcept {
     written = 0;
-    if (section >= 128 || output.size() < kBytes) return false;
-    encoding::bits::Writer w(output.first(kBytes));
-    for (std::size_t lane = 0; lane < 4; ++lane)
-        if (!w.write(enabled && section / 32 == lane ? std::uint32_t{1} << (section % 32) : 0, 32)) return false;
-    for (std::size_t i = 0; i < 129; ++i)
-        if (!w.write(0x811C9DC5U, 32) || !w.write(0, 7) || !w.write(0x7FFF, 16)) return false;
-    return w.bit_count() == kBits && w.finish(written) && written == kBytes;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (bits != kBits || input.size() != kBytes) return false;
-    encoding::bits::Reader r(input);
-    bool enabled{}; std::uint8_t section{};
-    for (std::size_t lane = 0; lane < 4; ++lane) {
-        std::uint64_t mask{};
-        if (!r.read(32, mask)) return false;
-        if (mask != 0) {
-            if (enabled || (mask & (mask - 1)) != 0) return false;
-            enabled = true;
-            for (std::size_t bit = 0; bit < 32; ++bit)
-                if (mask & (std::uint64_t{1} << bit)) section = static_cast<std::uint8_t>(lane * 32 + bit);
+    if (section >= kSectionCount || output.size() < kBytes) {
+        return false;
+    }
+    encoding::bits::Writer writer(output.first(kBytes));
+    for (std::size_t lane = 0; lane < kMaskLaneCount; ++lane) {
+        const bool selected = enabled && section / kMaskLaneWidth == lane;
+        const std::uint32_t mask = selected ? std::uint32_t{1} << (section % kMaskLaneWidth) : 0U;
+        if (!writer.write(mask, kMaskLaneWidth)) {
+            return false;
         }
     }
-    std::array<std::byte, kBytes> canonical{}; std::size_t written{};
-    return encode(section, enabled, canonical, written) && std::equal(input.begin(), input.end(), canonical.begin());
-}
+    for (std::size_t index = 0; index < kConditionRefCount; ++index) {
+        if (!fields::write_absent_client_ref(writer)) {
+            return false;
+        }
+    }
+    return fields::finish_exact(writer, kBits, kBytes, written);
 }
+
+} // namespace sunrise::middleware::bap::activity_message::music_section

+ 4 - 4
Sunrise/src/middleware/bap/activity_message/replicate_membership.h

@@ -13,9 +13,9 @@ namespace sunrise::middleware::bap::activity_message::replicate_membership {
 
 /** Membership snapshots use activity message type 12. */
 inline constexpr std::uint32_t kMessageType = 12;
-/** One local player and no remote replication member is 30,077 meaningful bits. */
+/** One local player and an explicit empty view mask use 30,077 meaningful bits. */
 inline constexpr std::size_t kMeaningfulBitCount = 30'077;
-/** The one-member snapshot is 3,760 bytes, the last carrying three zero padding bits. */
+/** The one-member snapshot has three zero padding bits. */
 inline constexpr std::size_t kEncodedSize = 3'760;
 /** A remote row adds its channel, process identity, view identity, and player snapshot. */
 inline constexpr std::size_t kRemoteMemberBitDelta = 3'044;
@@ -143,9 +143,9 @@ occupied_member_mask(const MembershipSnapshot& snapshot) noexcept {
     return kLocalMemberMask | (snapshot.remoteViewMember.present ? kRemoteMemberMask : 0U);
 }
 
-/** Native membership mask_6 enables entity transmission to the advertised host. */
+/** The native view updater skips the own member and activates the advertised remote member. */
 [[nodiscard]] constexpr std::uint32_t
-entity_replication_member_mask(const MembershipSnapshot& snapshot) noexcept {
+active_view_mask(const MembershipSnapshot& snapshot) noexcept {
     return snapshot.remoteViewMember.present ? kRemoteMemberMask : 0U;
 }
 

+ 31 - 20
Sunrise/src/middleware/bap/activity_message/roster_presence.h

@@ -1,31 +1,42 @@
 #pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
 #include "sensor_auth_update.h"
 
 namespace sunrise::middleware::bap::activity_message::sensor_auth_update {
-inline bool extend_key_order(std::span<std::uint32_t> order, std::size_t& count,
-    std::span<const std::uint32_t> incoming) noexcept {
-    if (count > order.size()) return false;
-    for (auto key : incoming) {
-        bool known = false;
-        for (std::size_t i = 0; i < count; ++i) known = known || order[i] == key;
-        if (!known) {
-            if (count == order.size()) return false;
-            order[count++] = key;
+
+/** One presence word covers 32 retained keys. */
+inline constexpr std::size_t kPresenceWordBits = 32;
+
+/** @return True when the roster holds the key and has not retired it. */
+[[nodiscard]] inline bool key_present(const Roster& roster, std::uint32_t key) noexcept {
+    for (std::size_t index = 0; index < roster.groupCount; ++index) {
+        if (roster.groups[index].key == key) {
+            return !roster.groups[index].retired;
         }
     }
-    return true;
-}
-inline bool key_present(const Roster& roster, std::uint32_t key) noexcept {
-    for (std::size_t i = 0; i < roster.groupCount; ++i)
-        if (roster.groups[i].key == key) return !roster.groups[i].retired;
     return false;
 }
-// The mask indexes the retained key array. A removed key must not shift its neighbours.
-inline std::uint32_t presence_word(const Roster& roster,
-    std::span<const std::uint32_t> keys, std::size_t word) noexcept {
+
+/**
+ * Builds one presence word over the retained key array. The mask indexes that array, so a
+ * retired key clears its bit and never shifts its neighbours.
+ */
+[[nodiscard]] inline std::uint32_t presence_word(const Roster& roster,
+                                                 std::span<const std::uint32_t> keys,
+                                                 std::size_t word) noexcept {
     std::uint32_t mask = 0;
-    for (std::size_t bit = 0; bit < 32 && word * 32 + bit < keys.size(); ++bit)
-        if (key_present(roster, keys[word * 32 + bit])) mask |= std::uint32_t{1} << bit;
+    for (std::size_t bit = 0;
+         bit < kPresenceWordBits && word * kPresenceWordBits + bit < keys.size();
+         ++bit) {
+        if (key_present(roster, keys[word * kPresenceWordBits + bit])) {
+            mask |= std::uint32_t{1} << bit;
+        }
+    }
     return mask;
 }
-}
+
+} // namespace sunrise::middleware::bap::activity_message::sensor_auth_update

+ 70 - 42
Sunrise/src/middleware/bap/activity_message/scene_events_auth.h

@@ -1,51 +1,79 @@
 #pragma once
-#include <array>
+
+#include <cstddef>
 #include <cstdint>
 #include <span>
-#include "../../encoding/bit_reader.h"
+
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+
+// Type-43 authored scene Auth in its event-only form: generation, no clear, no dependencies,
+// a zero scalar, then the cumulative event keys. The generation stays while events are added,
+// because a new generation restarts the scene.
+
 namespace sunrise::middleware::bap::activity_message::scene_events {
-inline constexpr std::uint32_t kSchema=0x8080626B;
-inline constexpr std::size_t kMaximumEvents=32, kMaximumBytes=138;
-// Full type-43 Auth: generation, clear, dependency count, scalar, event count, keys.
-// Keep generation unchanged when appending events: changing it restarts the scene.
-inline bool encode(std::int32_t generation, std::span<const std::uint32_t> events,
-                   std::span<std::byte> output, std::size_t& bytes, std::size_t& bits) noexcept {
-    bytes=bits=0;
-    if (generation<=0 || events.size()>kMaximumEvents || output.size()<(74+32*events.size()+7)/8)
+
+namespace fields = auth_fields;
+
+inline constexpr std::uint8_t kSlotType = 43;
+/** The SDK format table carries the same class and schema for authored scenes. */
+inline constexpr std::uint32_t kComponentClass = 0x80806382U;
+inline constexpr std::uint32_t kSchema = 0x8080626BU;
+/** Header: 32-bit generation, clear bit, 4-bit dependency count, 31-bit scalar, 6-bit count. */
+inline constexpr std::uint8_t kDependencyCountWidth = 4;
+inline constexpr std::uint8_t kScalarWidth = 31;
+inline constexpr std::uint8_t kEventCountWidth = 6;
+inline constexpr std::size_t kHeaderBits =
+    32 + fields::kBoolWidth + kDependencyCountWidth + kScalarWidth + kEventCountWidth;
+inline constexpr std::uint8_t kEventKeyWidth = 32;
+/** Events this API carries per body; the 6-bit count could name more. */
+inline constexpr std::size_t kMaximumEvents = 32;
+inline constexpr std::size_t kMaximumBytes =
+    (kHeaderBits + kEventKeyWidth * kMaximumEvents + 7) / 8;
+/** All-one bits is not an event key. */
+inline constexpr std::uint32_t kInvalidEventKey = 0xFFFFFFFFU;
+
+/**
+ * Encodes the scene body.
+ * @param generation Positive scene generation.
+ * @param events Distinct nonzero event keys.
+ * @param bytes Receives the byte count. @param bits Receives the meaningful bit count.
+ */
+[[nodiscard]] inline bool encode(std::int32_t generation,
+                                 std::span<const std::uint32_t> events,
+                                 std::span<std::byte> output,
+                                 std::size_t& bytes,
+                                 std::size_t& bits) noexcept {
+    bytes = 0;
+    bits = 0;
+    const std::size_t expectedBits = kHeaderBits + kEventKeyWidth * events.size();
+    if (generation <= 0 || events.size() > kMaximumEvents
+        || output.size() < (expectedBits + 7) / 8) {
         return false;
-    for (std::size_t i=0;i<events.size();++i) {
-        if (!events[i] || events[i]==0xFFFFFFFFU) return false;
-        for (std::size_t j=0;j<i;++j) if (events[i]==events[j]) return false;
     }
-    encoding::bits::Writer w(output);
-    if (!w.write(static_cast<std::uint32_t>(generation)+0x80000000U,32)
-        || !w.write(0,1) || !w.write(0,4) || !w.write(0,31) || !w.write(events.size(),6)) return false;
-    for (const auto event : events) if (!w.write(event,32)) return false;
-    bits=w.bit_count(); return w.finish(bytes);
-}
-
-// Admit only the event-only subset above; dependencies, clear commands and scalar
-// inputs require their own checked API. Reject malformed lengths and nonzero padding.
-inline bool validate(std::span<const std::byte> input, std::size_t bits) noexcept {
-    if (bits<74 || bits>74+32*kMaximumEvents || input.size()!=(bits+7)/8) return false;
-    encoding::bits::Reader r(input);
-    std::uint64_t generation{}, value{}, count{};
-    if (!r.read(32,generation) || generation<=0x80000000U
-        || !r.read(1,value) || value!=0 || !r.read(4,value) || value!=0
-        || !r.read(31,value) || value!=0 || !r.read(6,count)
-        || count>kMaximumEvents || bits!=74+32*count) return false;
-    std::array<std::uint32_t,kMaximumEvents> events{};
-    for (std::size_t i=0;i<count;++i) {
-        if (!r.read(32,value)) return false;
-        events[i]=static_cast<std::uint32_t>(value);
+    for (std::size_t index = 0; index < events.size(); ++index) {
+        if (events[index] == 0 || events[index] == kInvalidEventKey) {
+            return false;
+        }
+        for (std::size_t prior = 0; prior < index; ++prior) {
+            if (events[index] == events[prior]) {
+                return false;
+            }
+        }
     }
-    std::array<std::byte,kMaximumBytes> canonical{};
-    std::size_t written{}, expectedBits{};
-    if (!encode(static_cast<std::int32_t>(generation-0x80000000U),
-                std::span(events).first(count),canonical,written,expectedBits)
-        || written!=input.size() || expectedBits!=bits) return false;
-    for (std::size_t i=0;i<written;++i) if (canonical[i]!=input[i]) return false;
-    return true;
-}
+    encoding::bits::Writer writer(output);
+    if (!writer.write(static_cast<std::uint32_t>(generation) + fields::kSigned32Bias, 32)
+        || !writer.write(0, fields::kBoolWidth) || !writer.write(0, kDependencyCountWidth)
+        || !writer.write(0, kScalarWidth) || !writer.write(events.size(), kEventCountWidth)) {
+        return false;
+    }
+    for (const std::uint32_t event : events) {
+        if (!writer.write(event, kEventKeyWidth)) {
+            return false;
+        }
+    }
+    bits = writer.bit_count();
+    return bits == expectedBits && writer.finish(bytes);
 }
+
+} // namespace sunrise::middleware::bap::activity_message::scene_events

+ 6 - 1
Sunrise/src/middleware/bap/activity_message/scriptable_auth_body.h

@@ -175,6 +175,7 @@ inline constexpr std::size_t kType2MaximumBodyByteCount = (kType2MaximumBodyBitC
 
 /** ClientRef slot type and Auth schema for the object-filter sensor. */
 inline constexpr std::uint8_t kType34SlotType = 34;
+inline constexpr std::uint32_t kType34ComponentClass = 0x80809568U;
 inline constexpr std::uint32_t kType34Schema = 0x8080956AU;
 inline constexpr std::size_t kType34PredicateCapacity = 8;
 
@@ -418,6 +419,10 @@ inline constexpr std::uint32_t kType68Schema = 0x80804F67;
 inline constexpr std::size_t kType68EntryCount = 3;
 inline constexpr std::size_t kType68BitCount = 4'802;
 inline constexpr std::size_t kType68ByteCount = (kType68BitCount + 7) / 8;
+/** ClientRef slot type of an authored navigation marker a directive may point at. */
+inline constexpr std::uint8_t kType47SlotType = 47;
+/** ClientRef slot type of an authored volume an object filter may test against. */
+inline constexpr std::uint8_t kType60SlotType = 60;
 /** ClientRef slot type for the encounter/player engagement observer. */
 inline constexpr std::uint8_t kType70SlotType = 70;
 inline constexpr std::uint32_t kType70Schema = 0x808094F1;
@@ -466,7 +471,7 @@ struct Type68Preset final {
     bool visible{true};
     /** Authored type-47 destination; absent removes the explicit guidance marker. */
     Type2LaneClientRef navpoint{};
-    /** Player audience checked by native 1009740 before showing the mission banner. */
+    /** Type-70 engagement sensor the client tests before it shows the mission banner. */
     Type2LaneClientRef audience{};
 };
 

+ 29 - 11
Sunrise/src/middleware/bap/activity_message/sense_observation_packet.h

@@ -1,26 +1,44 @@
 #pragma once
+
+#include <cstddef>
+
 #include "sense_update.h"
 
 namespace sunrise::middleware::bap::activity_message::sense_update {
-// Partial means the complete envelope closed, but a length-delimited group had
-// an unsupported member. Other fully decoded objects still carry valid receipts.
+
+/** Complete, or partial: the envelope closed but one delimited group had an unsupported member. */
 [[nodiscard]] inline bool observation_status(DecodeStatus status) noexcept {
     return status == DecodeStatus::complete || status == DecodeStatus::partial;
 }
+
+/**
+ * Tests whether every decoded object of a packet owns a bounded value range and a generation.
+ * A complete packet may hold no undecoded object; a partial one may, and those are skipped.
+ */
 [[nodiscard]] inline bool observation_packet(const DecodedPacket& packet) noexcept {
     if (!observation_status(packet.status) || packet.objectsTruncated || packet.valuesTruncated
-        || packet.objectCount > packet.objects.size() || packet.valueCount > packet.values.size()) return false;
-    std::size_t end{}, decoded{};
-    for (std::size_t i=0; i<packet.objectCount; ++i) {
-        const auto& object=packet.objects[i];
-        if (object.firstValue != end || object.valueCount > packet.valueCount-end
-            || object.status == ObjectStatus::malformed) return false;
+        || packet.objectCount > packet.objects.size() || packet.valueCount > packet.values.size()) {
+        return false;
+    }
+    std::size_t end = 0;
+    std::size_t decoded = 0;
+    for (std::size_t index = 0; index < packet.objectCount; ++index) {
+        const DecodedObject& object = packet.objects[index];
+        if (object.firstValue != end || object.valueCount > packet.valueCount - end
+            || object.status == ObjectStatus::malformed) {
+            return false;
+        }
         end += object.valueCount;
         if (object.status == ObjectStatus::decoded) {
-            if (!object.hasGeneration) return false;
+            if (!object.hasGeneration) {
+                return false;
+            }
             ++decoded;
-        } else if (packet.status == DecodeStatus::complete) return false;
+        } else if (packet.status == DecodeStatus::complete) {
+            return false;
+        }
     }
     return end == packet.valueCount && decoded == packet.objectsDecoded;
 }
-}
+
+} // namespace sunrise::middleware::bap::activity_message::sense_update

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

@@ -184,7 +184,7 @@ struct Snapshot final {
     bool hasGrant{};
     bool hasRegion{};
     bool hasSpawnOverride{};
-    /** SDK darkness policy; lifetime +12 stores a bubble, not the slice-set index. */
+    /** Set when the estate carries a darkness sensor body; the lifetime then names the bubble. */
     bool hasDarknessPolicy{};
     bool darknessEnabled{};
     /** Hold the client's spawn while it loads by emitting `awaiting_client_sync`. */

+ 4 - 3
Sunrise/src/middleware/bap/activity_message/squad_auth_body.h

@@ -47,12 +47,13 @@ inline constexpr std::size_t kAuthoredProfileBitCount = 13;
 /** Buffer one squad body needs at the full requested-count length. */
 inline constexpr std::size_t kMaximumBitCount = exact_body_bit_count(kMaximumRequestedCountLength);
 inline constexpr std::size_t kMaximumByteCount = (kMaximumBitCount + 7) / 8;
-/** Full native Auth may also retain objective fields alongside the spawn preset. */
-inline constexpr std::size_t kMaximumRetainedByteCount = (1313U + 7U) / 8U;
+/** A retained body may carry the objective fields too, up to 1,313 bits. */
+inline constexpr std::size_t kMaximumRetainedBitCount = 1'313;
+inline constexpr std::size_t kMaximumRetainedByteCount = (kMaximumRetainedBitCount + 7U) / 8U;
 /** Spawn generation is an unsigned logical value stored in a 31-bit field. */
 inline constexpr std::uint32_t kMaximumGeneration = 0x7FFFFFFF;
 
-/** Native 4E4580 skips ordinary placement for mode 3, retaining requests for delivery. */
+/** Mode 3 skips ordinary placement and keeps the requests for a passenger delivery. */
 enum class Mode : std::uint8_t {
     mode0 = 0,
     mode2 = 2,

+ 78 - 37
Sunrise/src/middleware/bap/activity_message/squad_objective_auth.h

@@ -1,46 +1,87 @@
 #pragma once
+
 #include <array>
+#include <cstddef>
 #include <cstdint>
 #include <span>
-#include "../../encoding/bit_reader.h"
+
 #include "../../encoding/bit_writer.h"
+#include "auth_fields.h"
+#include "scriptable_auth_body.h"
+#include "squad_auth_body.h"
+
+// Type-1 squad Auth that assigns the squad to a native combat objective. Root .0 names the
+// objective, .13 carries the revision that invalidates its cost pass, .16 links a task group.
+// Counts, profile and spawn generation stay absent so the placement is untouched.
+
 namespace sunrise::middleware::bap::activity_message::squad_objective {
-inline constexpr std::uint32_t kSchema = 0x80807EC9;
-inline constexpr std::size_t kBits = 153, kBytes = 20;
-struct Request {
-    std::uint32_t registry{}, revision{};
-    std::uint16_t index{};
-    std::int32_t group{-1};
+
+namespace fields = auth_fields;
+
+inline constexpr std::uint32_t kSchema = squad_auth::kSchema;
+inline constexpr std::size_t kBits = 153;
+inline constexpr std::size_t kBytes = 20;
+/** Absent presence bits for fields .1 to .12. */
+inline constexpr std::uint8_t kAbsentMiddleFieldCount = 12;
+/** Root .15 is present and zero. Meaning unverified. */
+inline constexpr std::uint8_t kField15Width = 6;
+/** Root .16 task group: 5 bits with bias one, so -1 requests costs without a link. */
+inline constexpr std::uint8_t kTaskGroupWidth = 5;
+inline constexpr std::int32_t kTaskGroupBias = 1;
+inline constexpr std::int32_t kNoTaskGroup = -1;
+/** One objective sensor carries this many task groups. */
+inline constexpr std::int32_t kTaskGroupCount = 24;
+/** Root .18 active, 2 bits, written as the active wire value. */
+inline constexpr std::uint8_t kActiveWidth = 2;
+inline constexpr std::uint32_t kActiveValue = 2;
+/** Root .19 mode, 3 bits with bias one. Reserve keeps the counts for a delivery. */
+inline constexpr std::uint8_t kModeWidth = 3;
+inline constexpr std::uint32_t kModeBias = 1;
+
+struct Request final {
+    std::uint32_t registryKey{};
+    std::uint32_t revision{};
+    std::uint16_t objectiveIndex{};
+    std::int32_t taskGroup{kNoTaskGroup};
     bool reserved{};
 };
-// 4E2A90: root 0 selects the objective, root 13 invalidates its cost calculation,
-// root 16 links into an authored task group. -1 requests costs without linking a group.
-// Root 15 is explicitly unset; counts/profile/spawn generation remain absent.
-[[nodiscard]] inline bool encode(Request q, std::span<std::byte> output) noexcept {
-    if (output.size() != kBytes || !q.registry || !q.revision || q.revision > 0x7fffffff
-        || q.index > 32767 || q.group < -1 || q.group >= 24) return false;
-    std::array<std::byte, kBytes> b{};
-    encoding::bits::Writer w(b); std::size_t written{};
-    bool ok = w.write(1,1) && w.write(q.registry,32) && w.write(4,7)
-        && w.write(q.index + 32768U,16) && w.write(0,12)
-        && w.write(1,1) && w.write(q.revision,31) && w.write(0,1)
-        && w.write(1,1) && w.write(0,6) && w.write(1,1) && w.write(q.group + 1,5)
-        && w.write(0,1) && w.write(2,2) && w.write(q.reserved ? 4 : 3,3)
-        && w.write(1,1) && w.write(0x811c9dc5,32)
-        && w.bit_count() == kBits && w.finish(written) && written == kBytes;
-    if (ok) for (std::size_t i=0;i<kBytes;++i) output[i]=b[i];
-    return ok;
-}
-[[nodiscard]] inline bool validate(std::span<const std::byte> b, std::size_t bits) noexcept {
-    if (bits != kBits || b.size()!=kBytes) return false;
-    encoding::bits::Reader r(b); std::uint64_t v{}, registry{}, revision{};
-    return r.read(1,v)&&v==1 && r.read(32,registry)&&registry
-        && r.read(7,v)&&v==4 && r.read(16,v)&&v>=32768
-        && r.read(12,v)&&v==0 && r.read(1,v)&&v==1 && r.read(31,revision)&&revision
-        && r.read(1,v)&&v==0 && r.read(1,v)&&v==1 && r.read(6,v)&&v==0
-        && r.read(1,v)&&v==1 && r.read(5,v)&&v<=24
-        && r.read(1,v)&&v==0 && r.read(2,v)&&v==2 && r.read(3,v)&&(v==3 || v==4)
-        && r.read(1,v)&&v==1 && r.read(32,v)&&v==0x811c9dc5
-        && r.read(7,v)&&v==0;
-}
+
+/**
+ * Encodes the objective assignment.
+ * @param output Exactly kBytes.
+ * @return False on an out-of-range revision, index or task group.
+ */
+[[nodiscard]] inline bool encode(const Request& request, std::span<std::byte> output) noexcept {
+    if (output.size() != kBytes || request.registryKey == 0 || request.revision == 0
+        || request.revision > fields::kMaximumCounter
+        || request.objectiveIndex > fields::kMaximumClientRefIndex
+        || request.taskGroup < kNoTaskGroup || request.taskGroup >= kTaskGroupCount) {
+        return false;
+    }
+    const auto mode = static_cast<std::uint32_t>(request.reserved ? squad_auth::Mode::reserve
+                                                                  : squad_auth::Mode::mode2);
+    encoding::bits::Writer writer(output);
+    std::size_t written = 0;
+    const std::array<fields::Field, 13> tail{{
+        {0, kAbsentMiddleFieldCount},
+        {1, fields::kPresenceWidth}, // .13 present
+        {request.revision, fields::kCounterWidth},
+        {0, fields::kPresenceWidth}, // .14 absent
+        {1, fields::kPresenceWidth}, // .15 present
+        {0, kField15Width},
+        {1, fields::kPresenceWidth}, // .16 present
+        {static_cast<std::uint32_t>(request.taskGroup + kTaskGroupBias), kTaskGroupWidth},
+        {0, fields::kPresenceWidth}, // .17 absent
+        {kActiveValue, kActiveWidth},
+        {mode + kModeBias, kModeWidth},
+        {1, fields::kPresenceWidth},       // .20 present
+        {fields::kClientRefAbsentKey, 32}, // no name
+    }};
+    return writer.write(1, fields::kPresenceWidth)
+           && fields::write_client_ref(
+               writer, request.registryKey, scriptable_auth::kType3SlotType, request.objectiveIndex)
+           && fields::write_fields(writer, tail)
+           && fields::finish_exact(writer, kBits, kBytes, written);
 }
+
+} // namespace sunrise::middleware::bap::activity_message::squad_objective

+ 73 - 29
Sunrise/src/server/activity/activity_sdk_behavior_scope.h

@@ -1,40 +1,84 @@
 #pragma once
+
+#include <cstdint>
 #include <span>
+
+#include "../../middleware/content/packages/tables/scenario_reader.h"
 #include "../../state/activity_sdk/format.h"
 
+// A mission seed is published at the opening area and later bubbles keep that lease. Their local
+// sequences and scenes belong to the state the live region selects, not the seed state.
+
 namespace sunrise::server::activity::behavior_scope {
-namespace f = state::activity_sdk::format;
-
-// A mission seed is published at the opening area. Later bubbles keep that lease,
-// but their local sequences/scenes belong to the state selected by the live region.
-[[nodiscard]] inline bool live_state(std::span<const f::State> states,
-    std::span<const f::Bubble> bubbles, std::uint32_t scenario, std::uint32_t row,
-    std::int32_t region) noexcept {
-    if (region < 0 || row >= states.size()) return false;
-    const auto& s = states[row];
-    if (s.scenarioIndex != scenario || s.bubbleIndex >= bubbles.size()
-        || bubbles[s.bubbleIndex].scenarioIndex != scenario || s.stateOrdinal >= 8) return false;
-    return std::uint64_t(s.sliceSetIndex) + s.stateOrdinal == std::uint32_t(region);
+
+namespace format = state::activity_sdk::format;
+
+/** Occurrence ranks: the live region's state wins over the seed state. */
+inline constexpr unsigned kRankNone = 0;
+inline constexpr unsigned kRankSeedState = 1;
+inline constexpr unsigned kRankLiveState = 2;
+
+/** @return True when the state row is the one the client's region packs. */
+[[nodiscard]] inline bool live_state(std::span<const format::State> states,
+                                     std::span<const format::Bubble> bubbles,
+                                     std::uint32_t scenario,
+                                     std::uint32_t row,
+                                     std::int32_t region) noexcept {
+    if (region < 0 || row >= states.size()) {
+        return false;
+    }
+    const format::State& state = states[row];
+    if (state.scenarioIndex != scenario || state.bubbleIndex >= bubbles.size()
+        || bubbles[state.bubbleIndex].scenarioIndex != scenario
+        || state.stateOrdinal >= middleware::content::packages::tables::kSliceSetIndexFactor) {
+        return false;
+    }
+    return static_cast<std::uint64_t>(state.sliceSetIndex) + state.stateOrdinal
+           == static_cast<std::uint32_t>(region);
 }
 
-struct Selection { std::uint32_t row{f::kAbsentIndex}; bool ambiguous{}; };
-[[nodiscard]] inline Selection select(std::span<const f::Occurrence> occurrences,
-    std::span<const f::State> states, std::span<const f::Bubble> bubbles,
-    std::uint32_t scenario, std::uint32_t object, std::uint32_t seedState,
-    std::int32_t region) noexcept {
+struct Selection final {
+    std::uint32_t row{format::kAbsentIndex};
+    /** Two occurrences of equal rank name different states. */
+    bool ambiguous{};
+};
+
+/**
+ * Selects the occurrence of one object for the live region, or for the seed state when the
+ * live region has none.
+ */
+[[nodiscard]] inline Selection select(std::span<const format::Occurrence> occurrences,
+                                      std::span<const format::State> states,
+                                      std::span<const format::Bubble> bubbles,
+                                      std::uint32_t scenario,
+                                      std::uint32_t object,
+                                      std::uint32_t seedState,
+                                      std::int32_t region) noexcept {
     Selection result{};
-    unsigned best{};
-    for (std::uint32_t i = 0; i < occurrences.size(); ++i) {
-        const auto& o = occurrences[i];
-        if (o.scenarioIndex != scenario || o.objectIndex != object
-            || o.stateIndex >= states.size() || states[o.stateIndex].scenarioIndex != scenario
-            || o.bubbleIndex != states[o.stateIndex].bubbleIndex) continue;
-        const unsigned rank = live_state(states, bubbles, scenario, o.stateIndex, region) ? 2
-            : o.stateIndex == seedState ? 1 : 0;
-        if (!rank || rank < best) continue;
-        if (rank > best) { result = {i, false}; best = rank; }
-        else if (occurrences[result.row].stateIndex != o.stateIndex) result.ambiguous = true;
+    unsigned best = kRankNone;
+    for (std::uint32_t index = 0; index < occurrences.size(); ++index) {
+        const format::Occurrence& occurrence = occurrences[index];
+        if (occurrence.scenarioIndex != scenario || occurrence.objectIndex != object
+            || occurrence.stateIndex >= states.size()
+            || states[occurrence.stateIndex].scenarioIndex != scenario
+            || occurrence.bubbleIndex != states[occurrence.stateIndex].bubbleIndex) {
+            continue;
+        }
+        const unsigned rank = live_state(states, bubbles, scenario, occurrence.stateIndex, region)
+                                  ? kRankLiveState
+                              : occurrence.stateIndex == seedState ? kRankSeedState
+                                                                   : kRankNone;
+        if (rank == kRankNone || rank < best) {
+            continue;
+        }
+        if (rank > best) {
+            result = {index, false};
+            best = rank;
+        } else if (occurrences[result.row].stateIndex != occurrence.stateIndex) {
+            result.ambiguous = true;
+        }
     }
     return result;
 }
-}
+
+} // namespace sunrise::server::activity::behavior_scope

+ 42 - 1
Sunrise/src/server/activity/activity_sdk_device_prepare.cpp

@@ -1,7 +1,15 @@
 #include <algorithm>
 #include <limits>
 
+#include "../../middleware/bap/activity_message/damage_monitor_auth.h"
+#include "../../middleware/bap/activity_message/darkness_zone_auth.h"
+#include "../../middleware/bap/activity_message/ghost_link_auth.h"
+#include "../../middleware/bap/activity_message/interactable_object_auth.h"
+#include "../../middleware/bap/activity_message/mission_effect_auth.h"
+#include "../../middleware/bap/activity_message/music_section_auth.h"
+#include "../../middleware/bap/activity_message/scene_events_auth.h"
 #include "../../middleware/bap/activity_message/sensor_auth_update.h"
+#include "../../middleware/bap/activity_message/squad_objective_auth.h"
 #include "../../middleware/content/packages/tables/region_reader.h"
 #include "../../state/activity/runtime.h"
 #include "activity_sdk_device_internal.h"
@@ -437,6 +445,38 @@ prepare_slot(const sdk::BoundView& view, std::uint32_t slotRow, PreparedDevice&
     // An occupancy Auth body is exactly 87 bits, which the wire pads to 11 bytes.
     constexpr std::size_t kOccupancyAuthBits = 87;
     constexpr std::size_t kOccupancyAuthBytes = 11;
+    // Bodies this tree encodes are admitted by slot identity. Their shape belongs to the encoder.
+    namespace message = middleware::bap::activity_message;
+    const auto typed = [&](std::uint32_t type, std::uint32_t componentClass, std::uint32_t schema) {
+        return slotType == type && slot.componentClass == componentClass && authSchema == schema;
+    };
+    const bool typedSdkBody =
+        typed(auth::kType34SlotType, auth::kType34ComponentClass, auth::kType34Schema)
+        || typed(message::mission_effect::kSlotType,
+                 message::mission_effect::kComponentClass,
+                 message::mission_effect::kSchema)
+        || typed(message::music_section::kSlotType,
+                 message::music_section::kComponentClass,
+                 message::music_section::kSchema)
+        || typed(message::scene_events::kSlotType,
+                 message::scene_events::kComponentClass,
+                 message::scene_events::kSchema)
+        || typed(message::damage_monitor::kSlotType,
+                 message::damage_monitor::kComponentClass,
+                 message::damage_monitor::kAuthSchema)
+        || typed(message::darkness_zone::kSlotType,
+                 message::darkness_zone::kComponentClass,
+                 message::darkness_zone::kSchema)
+        || typed(format::kObjectSlotType,
+                 format::kObjectComponentClass,
+                 message::interactable_object::kSchema)
+        || typed(message::ghost_link::kSlotType,
+                 message::ghost_link::kComponentClass,
+                 message::ghost_link::kAuthSchema)
+        || typed(format::kSquadSlotType,
+                 format::kSquadComponentClass,
+                 message::squad_objective::kSchema)
+        || typed(auth::kType2SlotType, auth::kType2ComponentClass, auth::kType2Schema);
     const bool occupancy = slotType == format::kOccupancySlotType
                            && authSchema == format::kOccupancyAuthSchema
                            && bitCount == kOccupancyAuthBits && body.size() == kOccupancyAuthBytes;
@@ -460,7 +500,8 @@ prepare_slot(const sdk::BoundView& view, std::uint32_t slotRow, PreparedDevice&
         slotType == middleware::bap::activity_message::scriptable_auth::kType2SlotType
         && authSchema == middleware::bap::activity_message::scriptable_auth::kType2Schema
         && middleware::bap::activity_message::scriptable_auth::validate_type2_body(body, bitCount);
-    if (!occupancy && !directive && !engagement && !publicEvent && !performance && !combatant) {
+    if (!typedSdkBody && !occupancy && !directive && !engagement && !publicEvent && !performance
+        && !combatant) {
         return Status::invalidBody;
     }
     return Status::ready;

+ 16 - 498
Sunrise/src/server/activity/activity_sdk_device_runtime.cpp

@@ -1,16 +1,3 @@
-#include "../../middleware/bap/activity_message/music_section_auth.h"
-#include "../../middleware/bap/activity_message/mission_effect_auth.h"
-#include "../../middleware/bap/activity_message/darkness_zone_auth.h"
-#include "../../middleware/bap/activity_message/squad_objective_auth.h"
-#include "../../middleware/bap/activity_message/combatant_path_auth.h"
-#include "../../middleware/bap/activity_message/combatant_action_auth.h"
-#include "../../middleware/bap/activity_message/combatant_delivery_auth.h"
-#include "../../middleware/bap/activity_message/combatant_retire_auth.h"
-#include "../../middleware/bap/activity_message/ghost_link_auth.h"
-#include "../../middleware/bap/activity_message/scene_events_auth.h"
-#include "activity_sdk_device_runtime.h"
-#include "../../middleware/bap/activity_message/interactable_object_auth.h"
-
 #include <algorithm>
 #include <array>
 #include <cstddef>
@@ -18,7 +5,15 @@
 #include <span>
 #include <string_view>
 
+#include "../../middleware/bap/activity_message/damage_monitor_auth.h"
+#include "../../middleware/bap/activity_message/darkness_zone_auth.h"
+#include "../../middleware/bap/activity_message/ghost_link_auth.h"
+#include "../../middleware/bap/activity_message/interactable_object_auth.h"
+#include "../../middleware/bap/activity_message/mission_effect_auth.h"
+#include "../../middleware/bap/activity_message/music_section_auth.h"
+#include "../../middleware/bap/activity_message/scene_events_auth.h"
 #include "../../middleware/bap/activity_message/sensor_auth_update.h"
+#include "../../middleware/bap/activity_message/squad_objective_auth.h"
 #include "../../middleware/content/packages/tables/region_reader.h"
 #include "../../state/activity/runtime.h"
 #include "../../state/build_data/runtime.h"
@@ -40,491 +35,14 @@ static_assert(format::kDeviceAuthSchema == auth::kType23Schema);
 static_assert(format::kObjectSlotType == auth::kType4SlotType);
 static_assert(format::kObjectAuthSchema == auth::kType4Schema);
 
-/** Exact private route retained between preflight and queueing. */
-struct PreparedDevice final {
-    host::ScriptableTarget target{};
-    layouts::RosterGroup generatedRosterGroup{};
-    std::uint64_t activityClientGeneration{};
-    std::uint32_t scenarioRow{format::kAbsentIndex};
-    std::uint32_t stateRow{format::kAbsentIndex};
-    std::int32_t effectiveRegion{-1};
-};
-
-/** Maps the SDK binding validator to this API's stable refusal surface. */
-[[nodiscard]] Status binding_status(const sdk::BoundView& view,
-                                    server::bap::ActivityLinkView& link) noexcept {
-    if (view.catalog == nullptr || sdk::bound_activity(view) == nullptr
-        || sdk::bound_scenario(view) == nullptr) {
-        return Status::invalidView;
-    }
-    if (!state::activity::binding_matches(view.binding)) {
-        return Status::staleBinding;
-    }
-    (void)server::bap::activity_link_view(view.binding, link);
-    switch (
-        sdk::revalidate(view, view.binding, link.matchingLinks, link.activityClientGeneration)) {
-    case sdk::Status::ready:
-        return link.effectiveRegion >= 0 ? Status::ready : Status::noActivityLink;
-    case sdk::Status::missingClient:
-    case sdk::Status::ambiguousClient:
-        return Status::noActivityLink;
-    case sdk::Status::staleSession:
-        return Status::staleBinding;
-    case sdk::Status::staleActivityClient:
-        return Status::staleActivityClient;
-    case sdk::Status::notReady:
-    case sdk::Status::missing:
-    case sdk::Status::wrongSdkBuild:
-    case sdk::Status::catalogInvalid:
-    case sdk::Status::wrongActivity:
-    case sdk::Status::activityJoinNotExact:
-    case sdk::Status::missingScenarioLink:
-        return Status::invalidView;
-    }
-    return Status::invalidView;
-}
-
-/** @return True when both complete groups have the same key and compressed wire layout. */
-[[nodiscard]] bool same_roster_layout(const layouts::RosterGroup& left,
-                                      const layouts::RosterGroup& right) noexcept {
-    if (!layouts::valid_roster_group(left) || !layouts::valid_roster_group(right)
-        || left.registryKey != right.registryKey || left.slotCount != right.slotCount) {
-        return false;
-    }
-    for (std::size_t index = 0; index < left.slotCount; ++index) {
-        if (left.slotTypes[index] != right.slotTypes[index]
-            || left.slotFlags[index] != right.slotFlags[index]
-            || left.slotIndices[index] != right.slotIndices[index]) {
-            return false;
-        }
-    }
-    return true;
-}
-
-/** Finds the selected Auth slot in one materialized group's compressed order. */
-[[nodiscard]] bool selected_slot_offset(const layouts::RosterGroup& group,
-                                        const format::Slot& slot,
-                                        std::uint16_t& output) noexcept {
-    output = 0;
-    if (!layouts::valid_roster_group(group)
-        || slot.slotIndex > (std::numeric_limits<std::uint16_t>::max)()) {
-        return false;
-    }
-    std::size_t matches = 0;
-    for (std::size_t index = 0; index < group.slotCount; ++index) {
-        if (group.slotIndices[index] != slot.slotIndex || group.slotTypes[index] != slot.slotType
-            || (group.slotFlags[index] & layouts::kSlotAuthFlag) == 0) {
-            continue;
-        }
-        output = static_cast<std::uint16_t>(index);
-        ++matches;
-    }
-    return matches == 1;
-}
-
-/** Resolves a materialized occurrence group to at most one canonical roster row. */
-[[nodiscard]] Status canonical_target(const sdk::BoundView& view,
-                                      const format::Scenario& scenario,
-                                      const format::Slot& slot,
-                                      const layouts::RosterGroup& generatedGroup,
-                                      std::int32_t effectiveRegion,
-                                      host::ScriptableTarget& output,
-                                      bool& found) noexcept {
-    output = {};
-    found = false;
-    if (view.binding.destination.packageNameLength == 0
-        || view.binding.destination.packageNameLength
-               > view.binding.destination.packageName.size()) {
-        return Status::invalidView;
-    }
-    const std::string_view destination(
-        reinterpret_cast<const char*>(view.binding.destination.packageName.data()),
-        view.binding.destination.packageNameLength);
-    layouts::Definition layout{};
-    if (!state::build_data::find_scenario_layout(destination, layout) || layout.tag != scenario.tag
-        || layout.rosterGroupCount > layout.rosterGroups.size()
-        || layout.bubbleGroupCount > layout.bubbleGroups.size()) {
-        return Status::targetUnavailable;
-    }
-    const std::uint32_t activeBubble =
-        static_cast<std::uint32_t>(effectiveRegion) / tables::kSliceSetIndexFactor;
-    std::size_t activeMatches = 0;
-    std::uint16_t selectedTable = 0;
-    layouts::RosterGroup selectedGroup{};
-    const auto consider = [&](std::uint16_t tableIndex, bool active) noexcept {
-        layouts::RosterGroup candidate{};
-        if (!state::build_data::find_roster_group(tableIndex, candidate)) {
-            return false;
-        }
-        if (!same_roster_layout(generatedGroup, candidate)) {
-            return true;
-        }
-        if (active) {
-            if (activeMatches == 0) {
-                activeMatches = 1;
-                selectedTable = tableIndex;
-                selectedGroup = candidate;
-            } else if (tableIndex != selectedTable) {
-                // The same canonical table may be published in both the always-active and bubble
-                // lanes. That is one target; only a distinct active table is ambiguous.
-                activeMatches = 2;
-            }
-        }
-        return true;
-    };
-    for (std::size_t index = 0; index < layout.rosterGroupCount; ++index) {
-        if (!consider(layout.rosterGroups[index], true)) {
-            return Status::targetUnavailable;
-        }
-    }
-    for (std::size_t index = 0; index < layout.bubbleGroupCount; ++index) {
-        const bool active =
-            activeBubble < layouts::kBubbleCapacity
-            && (layout.bubbleGroupMasks[index] & (std::uint64_t{1} << activeBubble)) != 0;
-        if (!consider(layout.bubbleGroups[index], active)) {
-            return Status::targetUnavailable;
-        }
-    }
-    if (activeMatches > 1) {
-        return Status::ambiguousTarget;
-    }
-    if (activeMatches == 0) {
-        return Status::ready;
-    }
-    std::uint16_t slotOffset = 0;
-    if (!selected_slot_offset(selectedGroup, slot, slotOffset)) {
-        return Status::invalidSlot;
-    }
-    output.objectTag = selectedGroup.objectTag;
-    output.registryKey = selectedGroup.registryKey;
-    output.authSchema = slot.authSchema;
-    output.rosterGroupIndex = selectedTable;
-    output.rosterSlotOffset = slotOffset;
-    output.slotIndex = static_cast<std::uint16_t>(slot.slotIndex);
-    output.slotType = static_cast<std::uint8_t>(slot.slotType);
-    found = true;
-    return Status::ready;
-}
-
-/** Finishes the exact canonical or state-local route resolved from the bound SDK. */
-[[nodiscard]] Status finish_prepared(const sdk::BoundView& view,
-                                     const server::bap::ActivityLinkView& link,
-                                     PreparedDevice& output) noexcept {
-    output.activityClientGeneration = link.activityClientGeneration;
-    output.scenarioRow = view.scenarioRow;
-    output.effectiveRegion = link.effectiveRegion;
-    return Status::ready;
-}
-
-/** Resolves one SDK slot through a published canonical group or its exact live occurrence. */
-[[nodiscard]] Status
-prepare_slot(const sdk::BoundView& view, std::uint32_t slotRow, PreparedDevice& output) noexcept {
-    output = {};
-    server::bap::ActivityLinkView link{};
-    const Status live = binding_status(view, link);
-    if (live != Status::ready) {
-        return live;
-    }
-    const sdk::Catalog& catalog = *view.catalog;
-    const format::Scenario* const scenario = sdk::bound_scenario(view);
-    const auto slots = catalog.slots();
-    const auto objects = catalog.objects();
-    const auto occurrences = catalog.occurrences();
-    const auto states = catalog.states();
-    const auto bubbles = catalog.bubbles();
-    if (scenario == nullptr || slotRow >= slots.size()) {
-        return Status::invalidSlot;
-    }
-    const format::Slot& slot = slots[slotRow];
-    if (slot.objectIndex >= objects.size()
-        || slot.slotIndex > middleware::bap::activity_message::sensor_auth_update::kMaximumSlotIndex
-        || slot.slotType > middleware::bap::activity_message::sensor_auth_update::kMaximumSlotType
-        || slot.authSchema == 0 || slot.authSchema == format::kAbsentIndex) {
-        return Status::invalidSlot;
-    }
-    if ((slot.flags & format::kSlotSchemaJoinExact) == 0) {
-        return Status::schemaJoinNotExact;
-    }
-
-    const format::Object& object = objects[slot.objectIndex];
-    if (object.objectTag == 0 || object.objectKey == 0
-        || !sdk::materialize_roster_group(catalog, object, output.generatedRosterGroup)) {
-        return Status::invalidSlot;
-    }
-    std::uint16_t slotOffset = 0;
-    if (!selected_slot_offset(output.generatedRosterGroup, slot, slotOffset)) {
-        return Status::invalidSlot;
-    }
-
-    bool canonical = false;
-    const Status canonicalStatus = canonical_target(view,
-                                                    *scenario,
-                                                    slot,
-                                                    output.generatedRosterGroup,
-                                                    link.effectiveRegion,
-                                                    output.target,
-                                                    canonical);
-    if (canonicalStatus != Status::ready) {
-        output = {};
-        return canonicalStatus;
-    }
-    if (canonical) {
-        return finish_prepared(view, link, output);
-    }
-
-    const format::Occurrence* selected = nullptr;
-    std::uint32_t selectedState = format::kAbsentIndex;
-    for (const format::Occurrence& occurrence : sdk::scenario_occurrences(catalog, *scenario)) {
-        if (occurrence.scenarioIndex != view.scenarioRow || &occurrence < occurrences.data()
-            || &occurrence >= occurrences.data() + occurrences.size()) {
-            return Status::invalidView;
-        }
-        if (occurrence.objectIndex != slot.objectIndex) {
-            continue;
-        }
-        if (occurrence.stateIndex >= states.size() || occurrence.bubbleIndex >= bubbles.size()) {
-            return Status::invalidView;
-        }
-        const format::State& candidateState = states[occurrence.stateIndex];
-        const format::Bubble& candidateBubble = bubbles[occurrence.bubbleIndex];
-        if (candidateState.scenarioIndex != view.scenarioRow
-            || candidateBubble.scenarioIndex != view.scenarioRow
-            || candidateState.bubbleIndex != occurrence.bubbleIndex
-            || candidateState.stateOrdinal >= tables::kSliceSetIndexFactor
-            || candidateBubble.bubbleOrdinal >= layouts::kBubbleCapacity) {
-            return Status::invalidView;
-        }
-        const std::uint64_t candidateRegion =
-            static_cast<std::uint64_t>(candidateState.sliceSetIndex) + candidateState.stateOrdinal;
-        if (candidateRegion != static_cast<std::uint64_t>(link.effectiveRegion)) {
-            continue;
-        }
-        if (selected == nullptr) {
-            selected = &occurrence;
-            selectedState = occurrence.stateIndex;
-        } else if (occurrence.stateIndex != selectedState) {
-            return Status::ambiguousTarget;
-        }
-    }
-    if (selected == nullptr) {
-        return Status::targetUnavailable;
-    }
-    if (selected->stateIndex >= states.size() || selected->bubbleIndex >= bubbles.size()
-        || selected->objectIndex >= objects.size()) {
-        return Status::invalidView;
-    }
-    const format::State& state = states[selected->stateIndex];
-    const format::Bubble& bubble = bubbles[selected->bubbleIndex];
-    if (state.scenarioIndex != view.scenarioRow || bubble.scenarioIndex != view.scenarioRow
-        || state.bubbleIndex != selected->bubbleIndex || object.objectTag == 0
-        || object.objectKey == 0 || state.stateOrdinal >= tables::kSliceSetIndexFactor
-        || bubble.bubbleOrdinal >= layouts::kBubbleCapacity) {
-        return Status::invalidView;
-    }
-    output.target.objectTag = output.generatedRosterGroup.objectTag;
-    output.target.registryKey = output.generatedRosterGroup.registryKey;
-    output.target.authSchema = slot.authSchema;
-    output.target.rosterGroupIndex = host::kGeneratedRosterGroupIndex;
-    output.target.rosterSlotOffset = slotOffset;
-    output.target.slotIndex = static_cast<std::uint16_t>(slot.slotIndex);
-    output.target.sdkObjectIndex = selected->objectIndex;
-    output.target.stateLocalRegion = link.effectiveRegion;
-    output.target.slotType = static_cast<std::uint8_t>(slot.slotType);
-    output.target.stateLocalRoster = true;
-    output.stateRow = selected->stateIndex;
-    return finish_prepared(view, link, output);
-}
-
-/** Resolves the legacy type-23 device facade through the shared typed SDK Auth slot route. */
-[[nodiscard]] Status prepare(const sdk::BoundView& view,
-                             std::uint32_t slotRow,
-                             auth::Type23Channel channel,
-                             float value,
-                             PreparedDevice& output) noexcept {
-    (void)value;
-    const auto channelIndex = static_cast<std::size_t>(channel);
-    if (channelIndex >= auth::kType23ChannelCount) {
-        return Status::invalidChannel;
-    }
-    if (view.catalog == nullptr || slotRow >= view.catalog->slots().size()) {
-        return Status::invalidSlot;
-    }
-    const format::Slot& slot = view.catalog->slots()[slotRow];
-    if (slot.slotType != format::kDeviceSlotType
-        || slot.componentClass != format::kDeviceComponentClass
-        || slot.senseSchema != format::kDeviceSenseSchema
-        || slot.authSchema != format::kDeviceAuthSchema) {
-        return Status::invalidSlot;
-    }
-    return prepare_slot(view, slotRow, output);
-}
-
-/**
- * Resolves one exact type-31 trigger slot. Type 31 carries no caller value, so unlike the device
- * path there is no channel or range to check first.
- * @param view Pinned SDK view whose binding is revalidated.
- * @param slotRow Catalog row the pulse targets.
- * @param output Cleared, then receives the resolved target.
- * @return ready only for an exact type-31 slot on a live owned binding.
- */
-[[nodiscard]] Status prepare_trigger(const sdk::BoundView& view,
-                                     std::uint32_t slotRow,
-                                     PreparedDevice& output) noexcept {
-    if (view.catalog == nullptr || slotRow >= view.catalog->slots().size()) {
-        return Status::invalidSlot;
-    }
-    const format::Slot& slot = view.catalog->slots()[slotRow];
-    if (slot.slotType != auth::kType31SlotType || slot.authSchema != auth::kType31Schema) {
-        return Status::invalidSlot;
-    }
-    return prepare_slot(view, slotRow, output);
-}
-
-/** Resolves one package-owned object entry without accepting a caller transform. */
-[[nodiscard]] Status prepare_object(const sdk::BoundView& view,
-                                    std::uint32_t slotRow,
-                                    std::int32_t entryIndex,
-                                    PreparedDevice& output) noexcept {
-    if (view.catalog == nullptr || slotRow >= view.catalog->slots().size() || entryIndex < 0) {
-        return Status::invalidSlot;
-    }
-    const format::Slot& slot = view.catalog->slots()[slotRow];
-    if (slot.slotType != format::kObjectSlotType
-        || slot.componentClass != format::kObjectComponentClass
-        || slot.senseSchema != format::kObjectSenseSchema
-        || slot.authSchema != format::kObjectAuthSchema) {
-        return Status::invalidSlot;
-    }
-    return prepare_slot(view, slotRow, output);
-}
-
-/** Resolves one exact actor channel bridge. */
-[[nodiscard]] Status prepare_combatant(const sdk::BoundView& view,
-                                       std::uint32_t slotRow,
-                                       PreparedDevice& output) noexcept {
-    if (view.catalog == nullptr || slotRow >= view.catalog->slots().size()) {
-        return Status::invalidSlot;
-    }
-    const format::Slot& slot = view.catalog->slots()[slotRow];
-    if (slot.slotType != auth::kType2SlotType || slot.componentClass != auth::kType2ComponentClass
-        || slot.senseSchema != auth::kType2SenseSchema || slot.authSchema != auth::kType2Schema) {
-        return Status::invalidSlot;
-    }
-    return prepare_slot(view, slotRow, output);
-}
-
-/** Verifies exact SDK identity and schema-decodes one retained Auth body. */
-[[nodiscard]] Status validate_auth(const sdk::BoundView& view,
-                                   std::uint32_t slotRow,
-                                   std::uint32_t objectTag,
-                                   std::uint32_t registryKey,
-                                   std::uint32_t authSchema,
-                                   std::uint16_t slotIndex,
-                                   std::uint8_t slotType,
-                                   std::span<const std::byte> body,
-                                   std::uint16_t bitCount,
-                                   std::span<const std::byte> sdkBuildSha256) noexcept {
-    if (view.catalog == nullptr || sdkBuildSha256.size() != 32U
-        || view.catalog->sdk_build_sha256().size() != sdkBuildSha256.size()
-        || !std::equal(sdkBuildSha256.begin(),
-                       sdkBuildSha256.end(),
-                       view.catalog->sdk_build_sha256().begin())) {
-        return Status::wrongSdkBuild;
-    }
-    const auto slots = view.catalog->slots();
-    const auto objects = view.catalog->objects();
-    if (slotRow >= slots.size()) {
-        return Status::invalidSlot;
-    }
-    const format::Slot& slot = slots[slotRow];
-    if (slot.objectIndex >= objects.size() || objectTag == 0 || registryKey == 0 || authSchema == 0
-        || slotType > middleware::bap::activity_message::sensor_auth_update::kMaximumSlotType
-        || slotIndex > middleware::bap::activity_message::sensor_auth_update::kMaximumSlotIndex
-        || objects[slot.objectIndex].objectTag != objectTag
-        || objects[slot.objectIndex].objectKey != registryKey || slot.authSchema != authSchema
-        || slot.slotIndex != slotIndex || slot.slotType != slotType) {
-        return Status::invalidSlot;
-    }
-    if ((slot.flags & format::kSlotSchemaJoinExact) == 0) {
-        return Status::schemaJoinNotExact;
-    }
-    if (body.empty()
-        || body.size()
-               > middleware::bap::activity_message::sensor_auth_update::kAuthOverrideByteCapacity
-        || bitCount > body.size() * 8U || bitCount + 7U < body.size() * 8U) {
-        return Status::invalidBody;
-    }
-    const std::size_t trailingBits = bitCount % 8U;
-    if (trailingBits != 0) {
-        const std::uint8_t paddingMask =
-            static_cast<std::uint8_t>((std::uint16_t{1} << (8U - trailingBits)) - 1U);
-        if ((std::to_integer<std::uint8_t>(body.back()) & paddingMask) != 0) {
-            return Status::invalidBody;
-        }
-    }
-    constexpr std::size_t kOccupancyAuthBits = 87;
-    constexpr std::size_t kOccupancyAuthBytes = 11;
-    const bool objectFilter = slotType == 34 && slot.componentClass == 0x80809568U
-        && authSchema == 0x8080956AU && middleware::bap::activity_message::mission_effect::validate_filter(body,bitCount);
-    const bool missionEffect = slotType == 26 && slot.componentClass == 0x8080953FU
-        && authSchema == 0x8080954BU && middleware::bap::activity_message::mission_effect::validate(body,bitCount);
-    namespace music = middleware::bap::activity_message::music_section;
-    const bool musicSection = slotType == 11 && slot.componentClass == music::kClass
-        && authSchema == music::kSchema && music::validate(body, bitCount);
-    namespace scene = middleware::bap::activity_message::scene_events;
-    const bool sceneEvents = slotType == 43 && slot.componentClass == 0x80806382U
-        && authSchema == scene::kSchema && scene::validate(body, bitCount);
-    const bool damageMonitor = slotType == 20 && slot.componentClass == 0x80809560U
-        && authSchema == 0x80809563U && bitCount == 87 && body.size() == 11;
-    const bool occupancy = slotType == format::kOccupancySlotType
-                           && authSchema == format::kOccupancyAuthSchema
-                           && bitCount == kOccupancyAuthBits && body.size() == kOccupancyAuthBytes;
-    const bool directive =
-        slotType == middleware::bap::activity_message::scriptable_auth::kType68SlotType
-        && authSchema == middleware::bap::activity_message::scriptable_auth::kType68Schema
-        && middleware::bap::activity_message::scriptable_auth::validate_type68_body(body, bitCount);
-    const bool engagement =
-        slotType == middleware::bap::activity_message::scriptable_auth::kType70SlotType
-        && authSchema == middleware::bap::activity_message::scriptable_auth::kType70Schema
-        && middleware::bap::activity_message::scriptable_auth::validate_type70_body(body, bitCount);
-    const bool publicEvent =
-        slotType == middleware::bap::activity_message::scriptable_auth::kType71SlotType
-        && authSchema == middleware::bap::activity_message::scriptable_auth::kType71Schema
-        && middleware::bap::activity_message::scriptable_auth::validate_type71_body(body, bitCount);
-    const bool performance =
-        slotType == middleware::bap::activity_message::scriptable_auth::kType42SlotType
-        && authSchema == middleware::bap::activity_message::scriptable_auth::kType42Schema
-        && middleware::bap::activity_message::scriptable_auth::validate_type42_body(body, bitCount);
-    const bool combatant =
-        slotType == middleware::bap::activity_message::scriptable_auth::kType2SlotType
-        && authSchema == middleware::bap::activity_message::scriptable_auth::kType2Schema
-        && (middleware::bap::activity_message::scriptable_auth::validate_type2_body(body, bitCount)
-            || (slot.componentClass == auth::kType2ComponentClass
-                && (middleware::bap::activity_message::combatant_path::validate(body, bitCount)
-                    || middleware::bap::activity_message::combatant_delivery::validate(body, bitCount)
-                    || middleware::bap::activity_message::combatant_action::validate(body, bitCount)
-                    || middleware::bap::activity_message::combatant_retire::validate(body, bitCount))));
-    namespace darkness=middleware::bap::activity_message::darkness_zone;
-    bool darknessEnabled{};
-    const bool darknessZone=slotType==darkness::kSlotType&&authSchema==darkness::kSchema
-        &&slot.componentClass==darkness::kClass&&darkness::decode(body,bitCount,darknessEnabled);
-    namespace object = middleware::bap::activity_message::interactable_object;
-    const bool interactableObject = slotType == format::kObjectSlotType
-        && authSchema == object::kSchema && slot.componentClass == format::kObjectComponentClass
-        && object::validate(body,bitCount);
-    namespace ghost = middleware::bap::activity_message::ghost_link;
-    const bool ghostLink = slotType == ghost::kSlotType && authSchema == ghost::kAuthSchema
-                           && slot.componentClass == ghost::kComponentClass
-                           && ghost::validate(body, bitCount);
-    namespace objective = middleware::bap::activity_message::squad_objective;
-    const bool squadObjective = slotType == 1 && authSchema == objective::kSchema
-        && slot.componentClass == format::kSquadComponentClass && objective::validate(body, bitCount);
-    if (!musicSection && !objectFilter && !missionEffect && !damageMonitor && !darknessZone && !squadObjective && !occupancy && !directive && !engagement && !publicEvent && !performance && !combatant
-        && !ghostLink && !interactableObject && !sceneEvents) {
-        return Status::invalidBody;
-    }
-    return Status::ready;
-}
+using detail::binding_status;
+using detail::prepare;
+using detail::prepare_combatant;
+using detail::prepare_object;
+using detail::prepare_slot;
+using detail::prepare_trigger;
+using detail::PreparedDevice;
+using detail::validate_auth;
 
 } // namespace
 

+ 4 - 0
Sunrise/src/server/activity/activity_sdk_mission_internal.h

@@ -24,6 +24,10 @@ struct PreparedScene final {
 [[nodiscard]] Status binding_status(const state::activity_sdk::BoundView& view,
                                     server::bap::ActivityLinkView& link) noexcept;
 
+/** Maps the shared binding result to the authored-scene refusal surface. */
+[[nodiscard]] SceneStatus scene_binding_status(const state::activity_sdk::BoundView& view,
+                                               server::bap::ActivityLinkView& link) noexcept;
+
 /** Maps the transport lease's closed set of connection and refusal outcomes. */
 [[nodiscard]] Status lease_status(server::bap::ActivityMissionSeedLeaseStatus status) noexcept;
 

+ 36 - 33
Sunrise/src/server/activity/activity_sdk_mission_prepare.cpp

@@ -5,6 +5,7 @@
 
 #include "../../middleware/bap/activity_message/sensor_auth_update.h"
 #include "../../state/activity/runtime.h"
+#include "activity_sdk_behavior_scope.h"
 #include "activity_sdk_mission_internal.h"
 #include "activity_sdk_scriptable_route.h"
 
@@ -105,38 +106,6 @@ namespace {
 
 /** Materializes into static lock-owned storage so UI stack size stays bounded. */
 
-/** Maps the shared binding result to the authored-scene refusal surface. */
-[[nodiscard]] SceneStatus scene_binding_status(const sdk::BoundView& view,
-                                               server::bap::ActivityLinkView& link) noexcept {
-    switch (binding_status(view, link)) {
-    case Status::ready:
-        // A public-target link publishes the state-local groups of the public bubbles it hosts,
-        // so a scene, task or cue in one of them is reachable only through that link.
-        return link.effectiveRegion >= 0 ? SceneStatus::ready : SceneStatus::noActivityLink;
-    case Status::staleBinding:
-        return SceneStatus::staleBinding;
-    case Status::staleActivityClient:
-        return SceneStatus::staleActivityClient;
-    case Status::noActivityLink:
-        return SceneStatus::noActivityLink;
-    case Status::invalidView:
-    case Status::missingLiveSliceSet:
-    case Status::wrongScenario:
-    case Status::wrongSliceSet:
-    case Status::missingInitialState:
-    case Status::ambiguousInitialState:
-    case Status::invalidOccurrence:
-    case Status::schemaJoinNotExact:
-    case Status::invalidRosterGroup:
-    case Status::rosterKeyConflict:
-    case Status::groupCapacityExceeded:
-    case Status::outputBusy:
-    case Status::refused:
-        return SceneStatus::invalidView;
-    }
-    return SceneStatus::invalidView;
-}
-
 /** Checks the exact enabled and published mission-seed lease for one scene state. */
 [[nodiscard]] SceneStatus scene_lease_status(const sdk::BoundView& view,
                                              const server::bap::ActivityLinkView& link,
@@ -164,7 +133,9 @@ namespace {
     if (!lease.configured || lease.revision == 0 || lease.plan.scenarioRow != view.scenarioRow) {
         return SceneStatus::missionSeedUnavailable;
     }
-    if (lease.plan.stateRow != stateRow) {
+    if (lease.plan.stateRow != stateRow
+        && !behavior_scope::live_state(view.catalog->states(), view.catalog->bubbles(),
+            view.scenarioRow, stateRow, link.effectiveRegion)) {
         return SceneStatus::wrongState;
     }
     if (lease.publicationPending || lease.publishedRevision != lease.revision) {
@@ -175,6 +146,38 @@ namespace {
 
 } // namespace
 
+/** Maps the shared binding result to the authored-scene refusal surface. */
+[[nodiscard]] SceneStatus scene_binding_status(const sdk::BoundView& view,
+                                               server::bap::ActivityLinkView& link) noexcept {
+    switch (binding_status(view, link)) {
+    case Status::ready:
+        // A public-target link publishes the state-local groups of the public bubbles it hosts,
+        // so a scene, task or cue in one of them is reachable only through that link.
+        return link.effectiveRegion >= 0 ? SceneStatus::ready : SceneStatus::noActivityLink;
+    case Status::staleBinding:
+        return SceneStatus::staleBinding;
+    case Status::staleActivityClient:
+        return SceneStatus::staleActivityClient;
+    case Status::noActivityLink:
+        return SceneStatus::noActivityLink;
+    case Status::invalidView:
+    case Status::missingLiveSliceSet:
+    case Status::wrongScenario:
+    case Status::wrongSliceSet:
+    case Status::missingInitialState:
+    case Status::ambiguousInitialState:
+    case Status::invalidOccurrence:
+    case Status::schemaJoinNotExact:
+    case Status::invalidRosterGroup:
+    case Status::rosterKeyConflict:
+    case Status::groupCapacityExceeded:
+    case Status::outputBusy:
+    case Status::refused:
+        return SceneStatus::invalidView;
+    }
+    return SceneStatus::invalidView;
+}
+
 /** Maps exact SDK binding validation to this facade's stable refusal surface. */
 [[nodiscard]] Status binding_status(const sdk::BoundView& view,
                                     server::bap::ActivityLinkView& link) noexcept {

+ 14 - 617
Sunrise/src/server/activity/activity_sdk_mission_runtime.cpp

@@ -6,9 +6,9 @@
 
 #include "../../middleware/bap/activity_message/sensor_auth_update.h"
 #include "../../state/activity/runtime.h"
+#include "activity_sdk_behavior_scope.h"
 #include "activity_sdk_mission_internal.h"
 #include "activity_sdk_scriptable_route.h"
-#include "activity_sdk_behavior_scope.h"
 #include "host_runtime.h"
 
 namespace sunrise::server::activity::activity_sdk_mission {
@@ -19,622 +19,19 @@ namespace message = middleware::bap::activity_message::sensor_auth_update;
 namespace sdk = state::activity_sdk;
 namespace sdk_route = server::activity::activity_sdk_scriptables;
 
-SRWLOCK g_materializeLock{SRWLOCK_INIT};
-std::array<layouts::RosterGroup, message::kPublishedGroupCapacity> g_materializedGroups{};
-
-/** Exact private route retained while one authored-scene request is staged. */
-struct PreparedScene final {
-    host::ScriptableTarget target{};
-    layouts::RosterGroup rosterGroup{};
-    std::uint64_t activityClientGeneration{};
-    std::uint32_t scenarioRow{sdk::format::kAbsentIndex};
-    std::uint32_t stateRow{sdk::format::kAbsentIndex};
-    std::int32_t effectiveRegion{-1};
-};
-
-/** Maps exact SDK binding validation to this facade's stable refusal surface. */
-[[nodiscard]] Status binding_status(const sdk::BoundView& view,
-                                    server::bap::ActivityLinkView& link) noexcept {
-    if (view.catalog == nullptr || sdk::bound_activity(view) == nullptr
-        || sdk::bound_scenario(view) == nullptr) {
-        return Status::invalidView;
-    }
-    if (!state::activity::binding_matches(view.binding)) {
-        return Status::staleBinding;
-    }
-    (void)server::bap::activity_link_view(view.binding, link);
-    switch (
-        sdk::revalidate(view, view.binding, link.matchingLinks, link.activityClientGeneration)) {
-    case sdk::Status::ready:
-        return Status::ready;
-    case sdk::Status::missingClient:
-    case sdk::Status::ambiguousClient:
-        return Status::noActivityLink;
-    case sdk::Status::staleSession:
-        return Status::staleBinding;
-    case sdk::Status::staleActivityClient:
-        return Status::staleActivityClient;
-    case sdk::Status::notReady:
-    case sdk::Status::missing:
-    case sdk::Status::wrongSdkBuild:
-    case sdk::Status::catalogInvalid:
-    case sdk::Status::wrongActivity:
-    case sdk::Status::activityJoinNotExact:
-    case sdk::Status::missingScenarioLink:
-        return Status::invalidView;
-    }
-    return Status::invalidView;
-}
-
-/** Maps the SDK materializer's closed set of semantic failures. */
-[[nodiscard]] Status mission_status(sdk::MissionSeedStatus status) noexcept {
-    switch (status) {
-    case sdk::MissionSeedStatus::ready:
-        return Status::ready;
-    case sdk::MissionSeedStatus::invalidView:
-        return Status::invalidView;
-    case sdk::MissionSeedStatus::invalidSliceSet:
-        return Status::missingLiveSliceSet;
-    case sdk::MissionSeedStatus::missingInitialState:
-        return Status::missingInitialState;
-    case sdk::MissionSeedStatus::ambiguousInitialState:
-        return Status::ambiguousInitialState;
-    case sdk::MissionSeedStatus::invalidOccurrence:
-        return Status::invalidOccurrence;
-    case sdk::MissionSeedStatus::schemaJoinNotExact:
-        return Status::schemaJoinNotExact;
-    case sdk::MissionSeedStatus::invalidRosterGroup:
-        return Status::invalidRosterGroup;
-    case sdk::MissionSeedStatus::rosterKeyConflict:
-        return Status::rosterKeyConflict;
-    case sdk::MissionSeedStatus::groupCapacityExceeded:
-        return Status::groupCapacityExceeded;
-    }
-    return Status::refused;
-}
-
-/** Maps the transport lease's closed set of connection/refusal outcomes. */
-[[nodiscard]] Status lease_status(server::bap::ActivityMissionSeedLeaseStatus status) noexcept {
-    using LeaseStatus = server::bap::ActivityMissionSeedLeaseStatus;
-    switch (status) {
-    case LeaseStatus::ready:
-        return Status::ready;
-    case LeaseStatus::noActivityLink:
-        return Status::noActivityLink;
-    case LeaseStatus::staleActivityClient:
-        return Status::staleActivityClient;
-    case LeaseStatus::wrongScenario:
-        return Status::wrongScenario;
-    case LeaseStatus::missingLiveSliceSet:
-        return Status::missingLiveSliceSet;
-    case LeaseStatus::wrongSliceSet:
-        return Status::wrongSliceSet;
-    case LeaseStatus::outputBusy:
-        return Status::outputBusy;
-    case LeaseStatus::refused:
-        return Status::refused;
-    }
-    return Status::refused;
-}
-
-/** @return True when two plans leave the same objects out of their seed, in the same order. */
-[[nodiscard]] bool same_omissions(const server::bap::ActivityMissionSeedPlan& left,
-                                  const server::bap::ActivityMissionSeedPlan& right) noexcept {
-    if (left.omissionCount != right.omissionCount) {
-        return false;
-    }
-    for (std::uint32_t index = 0; index < left.omissionCount; ++index) {
-        if (left.omissions[index].objectTag != right.omissions[index].objectTag
-            || left.omissions[index].registryKey != right.omissions[index].registryKey) {
-            return false;
-        }
-    }
-    return true;
-}
-
-/** Copies the immutable scalar materializer output into a transport-owned plan. */
-[[nodiscard]] server::bap::ActivityMissionSeedPlan
-plan_from(const sdk::MissionSeedSummary& summary) noexcept {
-    server::bap::ActivityMissionSeedPlan plan{};
-    plan.activityRow = summary.activityRow;
-    plan.scenarioRow = summary.scenarioRow;
-    plan.stateRow = summary.stateRow;
-    plan.bubbleRow = summary.bubbleRow;
-    plan.bubbleOrdinal = summary.bubbleOrdinal;
-    plan.stateOrdinal = summary.stateOrdinal;
-    plan.entryIndex = summary.entryIndex;
-    plan.sliceSetIndex = summary.sliceSetIndex;
-    plan.effectiveRegion = summary.effectiveRegion;
-    plan.omissions = summary.omissions;
-    plan.omissionCount = summary.omissionCount;
-    plan.occurrenceCount = summary.occurrenceCount;
-    plan.groupCount = summary.groupCount;
-    plan.authMappingSlots = summary.authMappingSlots;
-    plan.authResetSlots = summary.authResetSlots;
-    plan.senseSuppressedSlots = summary.senseSuppressedSlots;
-    return plan;
-}
-
-/** @return True when two plans name the same immutable generated rows and counts. */
-[[nodiscard]] bool same_plan(const server::bap::ActivityMissionSeedPlan& left,
-                             const server::bap::ActivityMissionSeedPlan& right) noexcept {
-    return left.activityRow == right.activityRow && left.scenarioRow == right.scenarioRow
-           && left.stateRow == right.stateRow && left.bubbleRow == right.bubbleRow
-           && left.bubbleOrdinal == right.bubbleOrdinal && left.stateOrdinal == right.stateOrdinal
-           && left.entryIndex == right.entryIndex && left.sliceSetIndex == right.sliceSetIndex
-           && left.effectiveRegion == right.effectiveRegion && same_omissions(left, right)
-           && left.occurrenceCount == right.occurrenceCount && left.groupCount == right.groupCount
-           && left.authMappingSlots == right.authMappingSlots
-           && left.authResetSlots == right.authResetSlots
-           && left.senseSuppressedSlots == right.senseSuppressedSlots;
-}
-
-/** Materializes into static lock-owned storage so UI stack size stays bounded. */
-[[nodiscard]] Status materialize_plan(const sdk::BoundView& view,
-                                      std::int32_t effectiveRegion,
-                                      std::span<const sdk::MissionSeedOmission> omissions,
-                                      server::bap::ActivityMissionSeedPlan& output) noexcept {
-    output = {};
-    if (effectiveRegion < 0) {
-        return Status::missingLiveSliceSet;
-    }
-    sdk::MissionSeedSummary summary{};
-    AcquireSRWLockExclusive(&g_materializeLock);
-    const sdk::MissionSeedStatus materialized = sdk::materialize_initial_mission_seed(
-        view, effectiveRegion, omissions, std::span(g_materializedGroups), summary);
-    ReleaseSRWLockExclusive(&g_materializeLock);
-    const Status status = mission_status(materialized);
-    if (status == Status::ready) {
-        output = plan_from(summary);
-    }
-    return status;
-}
-
-/** Reads the transport lease after the binding has been revalidated. */
-[[nodiscard]] Status read_lease(const sdk::BoundView& view,
-                                const server::bap::ActivityLinkView& link,
-                                server::bap::ActivityMissionSeedLeaseView& output) noexcept {
-    return lease_status(server::bap::activity_mission_seed_lease(
-        view.binding, view.scenarioRow, link.activityClientGeneration, output));
-}
-
-/** Maps the shared binding result to the authored-scene refusal surface. */
-[[nodiscard]] SceneStatus scene_binding_status(const sdk::BoundView& view,
-                                               server::bap::ActivityLinkView& link) noexcept {
-    switch (binding_status(view, link)) {
-    case Status::ready:
-        // A public-target link publishes the state-local groups of the public bubbles it hosts,
-        // so a scene, task or cue in one of them is reachable only through that link.
-        return link.effectiveRegion >= 0 ? SceneStatus::ready : SceneStatus::noActivityLink;
-    case Status::staleBinding:
-        return SceneStatus::staleBinding;
-    case Status::staleActivityClient:
-        return SceneStatus::staleActivityClient;
-    case Status::noActivityLink:
-        return SceneStatus::noActivityLink;
-    case Status::invalidView:
-    case Status::missingLiveSliceSet:
-    case Status::wrongScenario:
-    case Status::wrongSliceSet:
-    case Status::missingInitialState:
-    case Status::ambiguousInitialState:
-    case Status::invalidOccurrence:
-    case Status::schemaJoinNotExact:
-    case Status::invalidRosterGroup:
-    case Status::rosterKeyConflict:
-    case Status::groupCapacityExceeded:
-    case Status::outputBusy:
-    case Status::refused:
-        return SceneStatus::invalidView;
-    }
-    return SceneStatus::invalidView;
-}
-
-/** Checks the exact enabled and published mission-seed lease for one scene state. */
-[[nodiscard]] SceneStatus scene_lease_status(const sdk::BoundView& view,
-                                             const server::bap::ActivityLinkView& link,
-                                             std::uint32_t stateRow) noexcept {
-    server::bap::ActivityMissionSeedLeaseView lease{};
-    switch (server::bap::activity_mission_seed_lease(
-        view.binding, view.scenarioRow, link.activityClientGeneration, lease)) {
-    case server::bap::ActivityMissionSeedLeaseStatus::ready:
-        break;
-    case server::bap::ActivityMissionSeedLeaseStatus::noActivityLink:
-        return SceneStatus::noActivityLink;
-    case server::bap::ActivityMissionSeedLeaseStatus::staleActivityClient:
-        return SceneStatus::staleActivityClient;
-    case server::bap::ActivityMissionSeedLeaseStatus::outputBusy:
-        return SceneStatus::outputBusy;
-    case server::bap::ActivityMissionSeedLeaseStatus::wrongScenario:
-    case server::bap::ActivityMissionSeedLeaseStatus::missingLiveSliceSet:
-    case server::bap::ActivityMissionSeedLeaseStatus::wrongSliceSet:
-    case server::bap::ActivityMissionSeedLeaseStatus::refused:
-        return SceneStatus::missionSeedUnavailable;
-    }
-    if (lease.activityClientGeneration != link.activityClientGeneration) {
-        return SceneStatus::staleActivityClient;
-    }
-    if (!lease.configured || lease.revision == 0 || lease.plan.scenarioRow != view.scenarioRow) {
-        return SceneStatus::missionSeedUnavailable;
-    }
-    if (lease.plan.stateRow != stateRow
-        && !behavior_scope::live_state(view.catalog->states(), view.catalog->bubbles(),
-            view.scenarioRow, stateRow, link.effectiveRegion)) {
-        return SceneStatus::wrongState;
-    }
-    if (lease.publicationPending || lease.publishedRevision != lease.revision) {
-        return SceneStatus::missionSeedPending;
-    }
-    return SceneStatus::ready;
-}
-
-/** Resolves one exact generated scene without changing transport state. */
-[[nodiscard]] SceneStatus prepare_scene(const sdk::BoundView& view,
-                                        std::uint32_t occurrenceRow,
-                                        std::uint32_t slotRow,
-                                        PreparedScene& output) noexcept {
-    output = {};
-    server::bap::ActivityLinkView link{};
-    const SceneStatus live = scene_binding_status(view, link);
-    if (live != SceneStatus::ready) {
-        return live;
-    }
-
-    const sdk::Catalog& catalog = *view.catalog;
-    const sdk::format::Scenario* const scenario = sdk::bound_scenario(view);
-    const auto occurrences = catalog.occurrences();
-    const auto objects = catalog.objects();
-    const auto slots = catalog.slots();
-    const auto states = catalog.states();
-    const auto bubbles = catalog.bubbles();
-    if (scenario == nullptr || occurrenceRow >= occurrences.size()) {
-        return SceneStatus::invalidOccurrence;
-    }
-    if (slotRow >= slots.size()) {
-        return SceneStatus::invalidSlot;
-    }
-
-    const sdk::format::Occurrence& occurrence = occurrences[occurrenceRow];
-    if (occurrence.scenarioIndex != view.scenarioRow) {
-        return SceneStatus::wrongScenario;
-    }
-    if (occurrence.objectIndex >= objects.size() || occurrence.stateIndex >= states.size()
-        || occurrence.bubbleIndex >= bubbles.size()) {
-        return SceneStatus::invalidOccurrence;
-    }
-    const sdk::format::Object& object = objects[occurrence.objectIndex];
-    const sdk::format::Slot& slot = slots[slotRow];
-    const sdk::format::State& state = states[occurrence.stateIndex];
-    const sdk::format::Bubble& bubble = bubbles[occurrence.bubbleIndex];
-    if (slot.objectIndex != occurrence.objectIndex || slotRow < object.slots.first
-        || slotRow - object.slots.first >= object.slots.count) {
-        return SceneStatus::invalidSlot;
-    }
-    if (state.scenarioIndex != view.scenarioRow || bubble.scenarioIndex != view.scenarioRow
-        || state.bubbleIndex != occurrence.bubbleIndex) {
-        return SceneStatus::invalidOccurrence;
-    }
-    if (slot.slotType != sdk::format::kAuthoredSceneSlotType
-        || slot.componentClass != sdk::format::kAuthoredSceneComponentClass
-        || slot.senseSchema != sdk::format::kAuthoredSceneSenseSchema
-        || slot.authSchema != sdk::format::kAuthoredSceneAuthSchema) {
-        return SceneStatus::invalidSlot;
-    }
-    if ((slot.flags & sdk::format::kSlotSchemaJoinExact) == 0) {
-        return SceneStatus::schemaJoinNotExact;
-    }
-    if (slot.slotIndex > (std::numeric_limits<std::uint16_t>::max)()
-        || slot.slotType > (std::numeric_limits<std::uint8_t>::max)()) {
-        return SceneStatus::invalidSlot;
-    }
-
-    const auto resources = sdk::slot_authored_scene_resources(catalog, slot);
-    if (resources.empty()) {
-        return SceneStatus::missingResource;
-    }
-    if (resources.size() != 1) {
-        return SceneStatus::ambiguousResource;
-    }
-    const sdk::format::AuthoredSceneResource& resource = resources.front();
-    if (resource.slotIndex != slotRow || resource.configTag == 0
-        || resource.configTag == sdk::format::kAbsentIndex || resource.resourceTag == 0
-        || resource.resourceTag == sdk::format::kAbsentIndex
-        || resource.resourceClass != sdk::format::kAuthoredSceneResourceClass
-        || resource.flags != sdk::format::kAuthoredSceneResourceExact
-        || resource.descriptorOffset > (std::numeric_limits<std::uint32_t>::max)()
-                                           - sdk::format::kAuthoredSceneResourceRelativeOffset
-        || resource.resourceFieldOffset
-               != resource.descriptorOffset + sdk::format::kAuthoredSceneResourceRelativeOffset) {
-        return SceneStatus::missingResource;
-    }
-
-    const SceneStatus lease = scene_lease_status(view, link, occurrence.stateIndex);
-    if (lease != SceneStatus::ready) {
-        return lease;
-    }
-
-    sdk_route::SourceIdentity source{};
-    source.scenarioTag = scenario->tag;
-    source.objectTag = object.objectTag;
-    source.registryKey = object.objectKey;
-    source.authSchema = slot.authSchema;
-    source.objectRow = occurrence.objectIndex;
-    source.stateRow = occurrence.stateIndex;
-    source.slotIndex = static_cast<std::uint16_t>(slot.slotIndex);
-    source.slotType = static_cast<std::uint8_t>(slot.slotType);
-
-    sdk_route::Resolution resolved{};
-    switch (sdk_route::resolve(view, source, link.effectiveRegion, resolved)) {
-    case sdk_route::Status::ready:
-        break;
-    case sdk_route::Status::invalidView:
-        return SceneStatus::invalidView;
-    case sdk_route::Status::invalidSource:
-        return SceneStatus::invalidSlot;
-    case sdk_route::Status::missingSource:
-        return SceneStatus::targetUnavailable;
-    case sdk_route::Status::ambiguousSource:
-        return SceneStatus::ambiguousTarget;
-    }
-    if (resolved.scenarioRow != view.scenarioRow || resolved.stateRow != occurrence.stateIndex
-        || resolved.region != link.effectiveRegion || !resolved.target.stateLocalRoster
-        || resolved.target.slotType != sdk::format::kAuthoredSceneSlotType
-        || resolved.target.authSchema != sdk::format::kAuthoredSceneAuthSchema) {
-        return SceneStatus::invalidSlot;
-    }
-
-    output.target = resolved.target;
-    output.rosterGroup = resolved.rosterGroup;
-    output.activityClientGeneration = link.activityClientGeneration;
-    output.scenarioRow = resolved.scenarioRow;
-    output.stateRow = resolved.stateRow;
-    output.effectiveRegion = resolved.region;
-    return SceneStatus::ready;
-}
-
-/** Resolves one exact SDK-bounded type-53 cue without changing transport state. */
-[[nodiscard]] SceneStatus prepare_dialogue(const sdk::BoundView& view,
-                                           std::uint32_t occurrenceRow,
-                                           std::uint32_t slotRow,
-                                           std::uint16_t cueIndex,
-                                           PreparedScene& output,
-                                           std::uint16_t& authoredCueCount) noexcept {
-    output = {};
-    authoredCueCount = 0;
-    server::bap::ActivityLinkView link{};
-    const SceneStatus live = scene_binding_status(view, link);
-    if (live != SceneStatus::ready) {
-        return live;
-    }
-    const sdk::Catalog& catalog = *view.catalog;
-    const sdk::format::Scenario* const scenario = sdk::bound_scenario(view);
-    const auto occurrences = catalog.occurrences();
-    const auto objects = catalog.objects();
-    const auto slots = catalog.slots();
-    const auto states = catalog.states();
-    const auto bubbles = catalog.bubbles();
-    if (scenario == nullptr || occurrenceRow >= occurrences.size()) {
-        return SceneStatus::invalidOccurrence;
-    }
-    if (slotRow >= slots.size()) {
-        return SceneStatus::invalidSlot;
-    }
-    const sdk::format::Occurrence& occurrence = occurrences[occurrenceRow];
-    if (occurrence.scenarioIndex != view.scenarioRow || occurrence.objectIndex >= objects.size()
-        || occurrence.stateIndex >= states.size() || occurrence.bubbleIndex >= bubbles.size()) {
-        return occurrence.scenarioIndex != view.scenarioRow ? SceneStatus::wrongScenario
-                                                            : SceneStatus::invalidOccurrence;
-    }
-    const sdk::format::Object& object = objects[occurrence.objectIndex];
-    const sdk::format::Slot& slot = slots[slotRow];
-    const sdk::format::State& state = states[occurrence.stateIndex];
-    const sdk::format::Bubble& bubble = bubbles[occurrence.bubbleIndex];
-    if (slot.objectIndex != occurrence.objectIndex || slotRow < object.slots.first
-        || slotRow - object.slots.first >= object.slots.count
-        || state.scenarioIndex != view.scenarioRow || bubble.scenarioIndex != view.scenarioRow
-        || state.bubbleIndex != occurrence.bubbleIndex) {
-        return SceneStatus::invalidSlot;
-    }
-    if (slot.slotType != sdk::format::kDialogueSlotType
-        || slot.componentClass != sdk::format::kDialogueComponentClass
-        || slot.authSchema != sdk::format::kDialogueAuthSchema
-        || (slot.flags & (sdk::format::kSlotSchemaJoinExact | sdk::format::kSlotDialogueCuesExact))
-               != (sdk::format::kSlotSchemaJoinExact | sdk::format::kSlotDialogueCuesExact)
-        || slot.reserved == 0 || slot.reserved > sdk::format::kDialogueMaximumCueCount
-        || cueIndex >= slot.reserved) {
-        return SceneStatus::invalidSlot;
-    }
-    authoredCueCount = static_cast<std::uint16_t>(slot.reserved);
-    const SceneStatus lease = scene_lease_status(view, link, occurrence.stateIndex);
-    if (lease != SceneStatus::ready) {
-        return lease;
-    }
-    sdk_route::SourceIdentity source{};
-    source.scenarioTag = scenario->tag;
-    source.objectTag = object.objectTag;
-    source.registryKey = object.objectKey;
-    source.authSchema = slot.authSchema;
-    source.objectRow = occurrence.objectIndex;
-    source.stateRow = occurrence.stateIndex;
-    source.slotIndex = static_cast<std::uint16_t>(slot.slotIndex);
-    source.slotType = static_cast<std::uint8_t>(slot.slotType);
-    sdk_route::Resolution resolved{};
-    switch (sdk_route::resolve(view, source, link.effectiveRegion, resolved)) {
-    case sdk_route::Status::ready:
-        break;
-    case sdk_route::Status::invalidView:
-        return SceneStatus::invalidView;
-    case sdk_route::Status::invalidSource:
-        return SceneStatus::invalidSlot;
-    case sdk_route::Status::missingSource:
-        return SceneStatus::targetUnavailable;
-    case sdk_route::Status::ambiguousSource:
-        return SceneStatus::ambiguousTarget;
-    }
-    if (resolved.scenarioRow != view.scenarioRow || resolved.stateRow != occurrence.stateIndex
-        || resolved.region != link.effectiveRegion || !resolved.target.stateLocalRoster
-        || resolved.target.slotType != sdk::format::kDialogueSlotType
-        || resolved.target.authSchema != sdk::format::kDialogueAuthSchema) {
-        return SceneStatus::invalidSlot;
-    }
-    output.target = resolved.target;
-    output.rosterGroup = resolved.rosterGroup;
-    output.activityClientGeneration = link.activityClientGeneration;
-    output.scenarioRow = resolved.scenarioRow;
-    output.stateRow = resolved.stateRow;
-    output.effectiveRegion = resolved.region;
-    return SceneStatus::ready;
-}
-
-/** Resolves one exact fixed-schema behavior slot without changing transport state. */
-[[nodiscard]] SceneStatus prepare_typed_behavior(const sdk::BoundView& view,
-                                                 std::uint32_t occurrenceRow,
-                                                 std::uint32_t slotRow,
-                                                 std::uint32_t expectedSlotType,
-                                                 std::uint32_t expectedComponentClass,
-                                                 std::uint32_t expectedAuthSchema,
-                                                 bool requireTaskTarget,
-                                                 PreparedScene& output) noexcept {
-    output = {};
-    server::bap::ActivityLinkView link{};
-    const SceneStatus live = scene_binding_status(view, link);
-    if (live != SceneStatus::ready) {
-        return live;
-    }
-    const sdk::Catalog& catalog = *view.catalog;
-    const sdk::format::Scenario* const scenario = sdk::bound_scenario(view);
-    const auto occurrences = catalog.occurrences();
-    const auto objects = catalog.objects();
-    const auto slots = catalog.slots();
-    const auto states = catalog.states();
-    const auto bubbles = catalog.bubbles();
-    if (scenario == nullptr || occurrenceRow >= occurrences.size()) {
-        return SceneStatus::invalidOccurrence;
-    }
-    if (slotRow >= slots.size()) {
-        return SceneStatus::invalidSlot;
-    }
-    const sdk::format::Occurrence& occurrence = occurrences[occurrenceRow];
-    if (occurrence.scenarioIndex != view.scenarioRow || occurrence.objectIndex >= objects.size()
-        || occurrence.stateIndex >= states.size() || occurrence.bubbleIndex >= bubbles.size()) {
-        return occurrence.scenarioIndex != view.scenarioRow ? SceneStatus::wrongScenario
-                                                            : SceneStatus::invalidOccurrence;
-    }
-    const sdk::format::Object& object = objects[occurrence.objectIndex];
-    const sdk::format::Slot& slot = slots[slotRow];
-    const sdk::format::State& state = states[occurrence.stateIndex];
-    const sdk::format::Bubble& bubble = bubbles[occurrence.bubbleIndex];
-    if (slot.objectIndex != occurrence.objectIndex || slotRow < object.slots.first
-        || slotRow - object.slots.first >= object.slots.count
-        || state.scenarioIndex != view.scenarioRow || bubble.scenarioIndex != view.scenarioRow
-        || state.bubbleIndex != occurrence.bubbleIndex) {
-        return SceneStatus::invalidSlot;
-    }
-    if (slot.slotType != expectedSlotType || slot.componentClass != expectedComponentClass
-        || slot.authSchema != expectedAuthSchema
-        || (slot.flags & sdk::format::kSlotSchemaJoinExact) == 0
-        || (requireTaskTarget && sdk::slot_task_targets(catalog, slot).empty())
-        || slot.slotIndex > (std::numeric_limits<std::uint16_t>::max)()) {
-        return SceneStatus::invalidSlot;
-    }
-    const SceneStatus lease = scene_lease_status(view, link, occurrence.stateIndex);
-    if (lease != SceneStatus::ready) {
-        return lease;
-    }
-    sdk_route::SourceIdentity source{};
-    source.scenarioTag = scenario->tag;
-    source.objectTag = object.objectTag;
-    source.registryKey = object.objectKey;
-    source.authSchema = slot.authSchema;
-    source.objectRow = occurrence.objectIndex;
-    source.stateRow = occurrence.stateIndex;
-    source.slotIndex = static_cast<std::uint16_t>(slot.slotIndex);
-    source.slotType = static_cast<std::uint8_t>(slot.slotType);
-    sdk_route::Resolution resolved{};
-    switch (sdk_route::resolve(view, source, link.effectiveRegion, resolved)) {
-    case sdk_route::Status::ready:
-        break;
-    case sdk_route::Status::invalidView:
-        return SceneStatus::invalidView;
-    case sdk_route::Status::invalidSource:
-        return SceneStatus::invalidSlot;
-    case sdk_route::Status::missingSource:
-        return SceneStatus::targetUnavailable;
-    case sdk_route::Status::ambiguousSource:
-        return SceneStatus::ambiguousTarget;
-    }
-    if (resolved.scenarioRow != view.scenarioRow || resolved.stateRow != occurrence.stateIndex
-        || resolved.region != link.effectiveRegion || !resolved.target.stateLocalRoster
-        || resolved.target.slotType != expectedSlotType
-        || resolved.target.authSchema != expectedAuthSchema) {
-        return SceneStatus::invalidSlot;
-    }
-    output.target = resolved.target;
-    output.rosterGroup = resolved.rosterGroup;
-    output.activityClientGeneration = link.activityClientGeneration;
-    output.scenarioRow = resolved.scenarioRow;
-    output.stateRow = resolved.stateRow;
-    output.effectiveRegion = resolved.region;
-    return SceneStatus::ready;
-}
-
-/** Resolves one exact SDK-linked type-38 task without changing transport state. */
-[[nodiscard]] SceneStatus prepare_task(const sdk::BoundView& view,
-                                       std::uint32_t occurrenceRow,
-                                       std::uint32_t slotRow,
-                                       PreparedScene& output) noexcept {
-    return prepare_typed_behavior(view,
-                                  occurrenceRow,
-                                  slotRow,
-                                  sdk::format::kTaskSlotType,
-                                  sdk::format::kTaskComponentClass,
-                                  sdk::format::kTaskAuthSchema,
-                                  true,
-                                  output);
-}
-
-/** Resolves one exact type-68 HUD state without changing transport state. */
-[[nodiscard]] SceneStatus prepare_directive(const sdk::BoundView& view,
-                                            std::uint32_t occurrenceRow,
-                                            std::uint32_t slotRow,
-                                            std::uint32_t nameHash,
-                                            std::int32_t elementIndex,
-                                            PreparedScene& output) noexcept {
-    const SceneStatus status = prepare_typed_behavior(view,
-                                                      occurrenceRow,
-                                                      slotRow,
-                                                      sdk::format::kDirectiveSlotType,
-                                                      sdk::format::kDirectiveComponentClass,
-                                                      sdk::format::kDirectiveAuthSchema,
-                                                      false,
-                                                      output);
-    if (status != SceneStatus::ready) {
-        return status;
-    }
-    std::size_t matches = 0;
-    for (const sdk::format::DirectiveElement& row : view.catalog->directive_elements()) {
-        matches +=
-            row.slotIndex == slotRow && row.nameHash == nameHash && row.elementIndex == elementIndex
-                ? 1U
-                : 0U;
-    }
-    return matches == 1 ? SceneStatus::ready : SceneStatus::invalidSlot;
-}
-
-/** Resolves one exact type-3 objective sensor without changing transport state. */
-[[nodiscard]] SceneStatus prepare_objective(const sdk::BoundView& view,
-                                            std::uint32_t occurrenceRow,
-                                            std::uint32_t slotRow,
-                                            PreparedScene& output) noexcept {
-    return prepare_typed_behavior(view,
-                                  occurrenceRow,
-                                  slotRow,
-                                  sdk::format::kObjectiveSlotType,
-                                  sdk::format::kObjectiveComponentClass,
-                                  sdk::format::kObjectiveAuthSchema,
-                                  false,
-                                  output);
-}
+using detail::binding_status;
+using detail::lease_status;
+using detail::materialize_plan;
+using detail::prepare_dialogue;
+using detail::prepare_directive;
+using detail::prepare_objective;
+using detail::prepare_scene;
+using detail::prepare_task;
+using detail::prepare_typed_behavior;
+using detail::PreparedScene;
+using detail::read_lease;
+using detail::same_plan;
+using detail::scene_binding_status;
 
 } // namespace
 

+ 5 - 788
Sunrise/src/server/activity/host_runtime.cpp

@@ -1,5 +1,8 @@
-#include "../../middleware/bap/activity_message/sense_observation_packet.h"
-#include <algorithm>
+/**
+ * The Activity Host instance table, event ring, reducer queue and Auth-state lane.
+ * Helpers here need the Host runtime lock; the entry points take it themselves.
+ */
+
 #include <array>
 #include <cstddef>
 #include <cstdio>
@@ -189,524 +192,6 @@ void cancel_output(Instance& instance, std::uint64_t now) noexcept {
     instance.view.lastEventSequence = g_sequence;
 }
 
-/** @return True when one decoded object has the exact retained observation key. */
-[[nodiscard]] bool same_sense_key(
-    const SenseObservationKey& key,
-    const middleware::bap::activity_message::sense_update::DecodedObject& object) noexcept {
-    return key.registryKey == object.registryKey && key.objectTag == object.objectTag
-           && key.slotType == object.slotType && key.slotIndex == object.slotIndex
-           && key.senseSchema == object.senseSchema && key.schemaRow == object.schemaRow;
-}
-
-/** @return True when the packet carries this key at or after the selected object. */
-[[nodiscard]] bool
-packet_has_sense_key(const middleware::bap::activity_message::sense_update::DecodedPacket& packet,
-                     const SenseObservationKey& key,
-                     std::size_t first) noexcept {
-    for (std::size_t index = first; index < packet.objectCount; ++index) {
-        if (packet.objects[index].status == middleware::bap::activity_message::sense_update::ObjectStatus::decoded
-            && packet.objects[index].hasGeneration && same_sense_key(key, packet.objects[index])) {
-            return true;
-        }
-    }
-    return false;
-}
-
-/** @return True when the envelope closed and each decoded object's storage is bounded. */
-[[nodiscard]] bool valid_sense_observation_input(const SenseInput& input) noexcept {
-    namespace sense = middleware::bap::activity_message::sense_update;
-    const auto& packet = input.decoded;
-    return input.sourceGeneration != 0 && input.clientMessageSequence != 0
-        && input.verdict == state::activity::receipts::Verdict::framed
-        && input.decodeStatus == packet.status && sense::observation_packet(packet)
-        && input.groupsSeen == packet.groupsSeen && input.groupsDecoded == packet.groupsDecoded
-        && input.groupsSkipped == packet.groupsSkipped && input.objectsSeen == packet.objectsSeen
-        && input.objectsDecoded == packet.objectsDecoded;
-}
-
-/** Retains one accepted client input independently from panel and output events. */
-void append_mission_input(
-    const Event& event,
-    const middleware::bap::activity_message::sense_update::DecodedPacket* sense,
-    const ClientMessageSnapshot* clientMessage = nullptr) noexcept {
-    if (event.missionSequence == 0) {
-        return;
-    }
-    g_missionInputSequence = next_nonzero(g_missionInputSequence);
-    MissionInputRecord record{};
-    record.view.event = event;
-    record.view.sequence = g_missionInputSequence;
-    if (record.view.event.sequence == 0) {
-        record.view.event.sequence = g_missionInputSequence;
-    }
-    if (sense != nullptr) {
-        record.sense = *sense;
-        record.hasSense = true;
-    }
-    if (clientMessage != nullptr) {
-        record.clientMessage = *clientMessage;
-        record.hasClientMessage = true;
-    }
-    // The slot was reserved when the durable sequence was issued, so this never reallocates.
-    g_missionInputs.push_back(std::move(record));
-}
-
-/** Mixes one fixed-width value into the local scene change guard. */
-void mix_scene_fingerprint(std::uint64_t& fingerprint, std::uint64_t value) noexcept {
-    constexpr std::uint64_t kPrime = 1'099'511'628'211ULL;
-    for (std::uint8_t shift = 0; shift < 64; shift += 8) {
-        fingerprint ^= (value >> shift) & 0xFFU;
-        fingerprint *= kPrime;
-    }
-}
-
-/** @return A run-local change guard over one decoded object and every retained value. */
-[[nodiscard]] std::uint64_t
-scene_fingerprint(const middleware::bap::activity_message::sense_update::DecodedObject& object,
-                  std::span<const middleware::bap::activity_message::sense_update::DecodedValue>
-                      values) noexcept {
-    constexpr std::uint64_t kOffset = 14'695'981'039'346'656'037ULL;
-    std::uint64_t fingerprint = kOffset;
-    mix_scene_fingerprint(fingerprint, object.objectRow);
-    mix_scene_fingerprint(fingerprint, object.slotRow);
-    mix_scene_fingerprint(fingerprint, object.schemaRow);
-    mix_scene_fingerprint(fingerprint, object.generationPlusOne);
-    mix_scene_fingerprint(fingerprint, object.deltaBits);
-    mix_scene_fingerprint(fingerprint, object.hasGeneration ? 1U : 0U);
-    mix_scene_fingerprint(fingerprint, values.size());
-    for (const auto& value : values) {
-        mix_scene_fingerprint(fingerprint, value.unsignedValue);
-        mix_scene_fingerprint(fingerprint, static_cast<std::uint64_t>(value.signedValue));
-        mix_scene_fingerprint(fingerprint, std::bit_cast<std::uint32_t>(value.realValue));
-        mix_scene_fingerprint(fingerprint, value.schemaRow);
-        mix_scene_fingerprint(fingerprint, value.fieldRow);
-        mix_scene_fingerprint(fingerprint, value.occurrence);
-        mix_scene_fingerprint(fingerprint, value.bitOffset);
-        mix_scene_fingerprint(fingerprint, value.fieldOrdinal);
-        mix_scene_fingerprint(fingerprint, value.width);
-        mix_scene_fingerprint(fingerprint, static_cast<std::uint8_t>(value.kind));
-        mix_scene_fingerprint(fingerprint, value.present ? 1U : 0U);
-    }
-    return fingerprint;
-}
-
-/** Writes one bounded type-43 diagnostic event. */
-void report_scene_sense(const char* format, ...) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    va_list arguments;
-    va_start(arguments, format);
-    const int written = std::vsnprintf(line.data(), line.size(), format, arguments);
-    va_end(arguments);
-    if (written <= 0) {
-        return;
-    }
-    const auto length = static_cast<std::size_t>(written) < line.size()
-                            ? static_cast<std::size_t>(written)
-                            : line.size() - 1;
-    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
-}
-
-/** @return The trace row for one exact ClientRef, or null when it has not been seen. */
-[[nodiscard]] SceneSenseTraceRecord* find_scene_trace_record(
-    SceneSenseTrace& trace,
-    const middleware::bap::activity_message::sense_update::DecodedObject& object) noexcept {
-    for (SceneSenseTraceRecord& record : trace.records) {
-        if (record.occupied && same_sense_key(record.key, object)) {
-            return &record;
-        }
-    }
-    return nullptr;
-}
-
-/** @return One unused trace row, or null when the bounded table is full. */
-[[nodiscard]] SceneSenseTraceRecord* reserve_scene_trace_record(SceneSenseTrace& trace) noexcept {
-    for (SceneSenseTraceRecord& record : trace.records) {
-        if (!record.occupied) {
-            return &record;
-        }
-    }
-    return nullptr;
-}
-
-/** Reports one typed scalar with its exact reflected rows and wire position. */
-void report_scene_value(
-    const Instance& instance,
-    const SenseInput& input,
-    const middleware::bap::activity_message::sense_update::DecodedObject& object,
-    const middleware::bap::activity_message::sense_update::DecodedValue& value,
-    std::size_t valueIndex) noexcept {
-    using ValueKind = middleware::bap::activity_message::sense_update::ValueKind;
-    constexpr const char* kPrefix =
-        "ev=scene_sense kind=value activity=%d session=0x%llX binding_rev=%llu "
-        "source_gen=%llu msg_seq=%llu key=0x%08X tag=0x%08X type=%u index=%u "
-        "sense_schema=0x%08X gen_plus_one=%u value_index=%zu schema_row=%u "
-        "field_row=%u ordinal=%u occurrence=%u bit=%u width=%u present=%u";
-    std::array<char, core::log::kLineCapacity> prefix{};
-    const int written =
-        std::snprintf(prefix.data(),
-                      prefix.size(),
-                      kPrefix,
-                      static_cast<int>(instance.view.binding.destination.activityIndex),
-                      static_cast<unsigned long long>(instance.view.binding.sessionId),
-                      static_cast<unsigned long long>(instance.view.binding.createdRevision),
-                      static_cast<unsigned long long>(input.sourceGeneration),
-                      static_cast<unsigned long long>(input.clientMessageSequence),
-                      object.registryKey,
-                      object.objectTag,
-                      static_cast<unsigned>(object.slotType),
-                      static_cast<unsigned>(object.slotIndex),
-                      object.senseSchema,
-                      object.generationPlusOne,
-                      valueIndex,
-                      value.schemaRow,
-                      value.fieldRow,
-                      static_cast<unsigned>(value.fieldOrdinal),
-                      value.occurrence,
-                      value.bitOffset,
-                      static_cast<unsigned>(value.width),
-                      value.present ? 1U : 0U);
-    if (written <= 0 || static_cast<std::size_t>(written) >= prefix.size()) {
-        return;
-    }
-    if (!value.present) {
-        report_scene_sense("%s domain=%s value=absent",
-                           prefix.data(),
-                           value.kind == ValueKind::unsignedInteger ? "uint"
-                           : value.kind == ValueKind::signedInteger ? "int"
-                           : value.kind == ValueKind::boolean       ? "bool"
-                                                                    : "real32");
-        return;
-    }
-    switch (value.kind) {
-    case ValueKind::unsignedInteger:
-        report_scene_sense("%s domain=uint value=0x%llX",
-                           prefix.data(),
-                           static_cast<unsigned long long>(value.unsignedValue));
-        break;
-    case ValueKind::signedInteger:
-        report_scene_sense("%s domain=int raw=0x%llX value=%lld",
-                           prefix.data(),
-                           static_cast<unsigned long long>(value.unsignedValue),
-                           static_cast<long long>(value.signedValue));
-        break;
-    case ValueKind::boolean:
-        report_scene_sense("%s domain=bool raw=0x%llX value=%u",
-                           prefix.data(),
-                           static_cast<unsigned long long>(value.unsignedValue),
-                           value.unsignedValue != 0 ? 1U : 0U);
-        break;
-    case ValueKind::real32:
-        report_scene_sense("%s domain=real32 raw=0x%llX value_bits=0x%08X value=%.9g",
-                           prefix.data(),
-                           static_cast<unsigned long long>(value.unsignedValue),
-                           std::bit_cast<std::uint32_t>(value.realValue),
-                           static_cast<double>(value.realValue));
-        break;
-    }
-}
-
-/** Reports complete changed type-43 objects without changing retained activity state. */
-void trace_scene_sense(Instance& instance, const SenseInput& input) noexcept {
-    namespace sense = middleware::bap::activity_message::sense_update;
-    if (!core::log::accepts(core::log::Channel::server, core::log::Level::debug)) {
-        return;
-    }
-    SceneSenseTrace& trace = instance.sceneSenseTrace;
-    if (trace.sourceGeneration != input.sourceGeneration) {
-        trace = {};
-        trace.sourceGeneration = input.sourceGeneration;
-    }
-    const sense::DecodedPacket& packet = input.decoded;
-    bool hasScene = false;
-    const std::size_t retainedObjectCount = (std::min)(packet.objectCount, packet.objects.size());
-    for (std::size_t index = 0; index < retainedObjectCount; ++index) {
-        if (packet.objects[index].slotType
-            == static_cast<std::uint8_t>(state::activity_sdk::format::kAuthoredSceneSlotType)) {
-            hasScene = true;
-            break;
-        }
-    }
-    if (!valid_sense_observation_input(input)) {
-        if (!trace.incompleteReported && (hasScene || packet.objectsTruncated)) {
-            trace.incompleteReported = true;
-            report_scene_sense(
-                "ev=scene_sense kind=packet result=skip reason=incomplete activity=%d "
-                "session=0x%llX binding_rev=%llu source_gen=%llu msg_seq=%llu "
-                "status=%s objects_seen=%u objects_kept=%zu values_kept=%zu "
-                "objects_truncated=%u values_truncated=%u",
-                static_cast<int>(instance.view.binding.destination.activityIndex),
-                static_cast<unsigned long long>(instance.view.binding.sessionId),
-                static_cast<unsigned long long>(instance.view.binding.createdRevision),
-                static_cast<unsigned long long>(input.sourceGeneration),
-                static_cast<unsigned long long>(input.clientMessageSequence),
-                sense::decode_status_name(packet.status),
-                packet.objectsSeen,
-                packet.objectCount,
-                packet.valueCount,
-                packet.objectsTruncated ? 1U : 0U,
-                packet.valuesTruncated ? 1U : 0U);
-        }
-        return;
-    }
-    for (std::size_t index = 0; index < packet.objectCount; ++index) {
-        const sense::DecodedObject& object = packet.objects[index];
-        if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration || object.slotType
-            != static_cast<std::uint8_t>(state::activity_sdk::format::kAuthoredSceneSlotType)
-            || packet_has_sense_key(packet,
-                                    {object.registryKey,
-                                     object.objectTag,
-                                     object.senseSchema,
-                                     object.schemaRow,
-                                     object.slotIndex,
-                                     object.slotType},
-                                    index + 1)) {
-            continue;
-        }
-        const std::span values(packet.values.data() + object.firstValue, object.valueCount);
-        const std::uint64_t fingerprint = scene_fingerprint(object, values);
-        SceneSenseTraceRecord* record = find_scene_trace_record(trace, object);
-        const bool known = record != nullptr;
-        if (known && record->fingerprint == fingerprint
-            && record->generationPlusOne == object.generationPlusOne
-            && record->valueCount == object.valueCount
-            && record->hasGeneration == object.hasGeneration) {
-            continue;
-        }
-        if (record == nullptr) {
-            record = reserve_scene_trace_record(trace);
-        }
-        if (record == nullptr) {
-            if (!trace.capacityReported) {
-                trace.capacityReported = true;
-                report_scene_sense(
-                    "ev=scene_sense kind=packet result=skip reason=trace_capacity activity=%d "
-                    "session=0x%llX binding_rev=%llu source_gen=%llu capacity=%zu",
-                    static_cast<int>(instance.view.binding.destination.activityIndex),
-                    static_cast<unsigned long long>(instance.view.binding.sessionId),
-                    static_cast<unsigned long long>(instance.view.binding.createdRevision),
-                    static_cast<unsigned long long>(input.sourceGeneration),
-                    trace.records.size());
-            }
-            continue;
-        }
-        record->key = {object.registryKey,
-                       object.objectTag,
-                       object.senseSchema,
-                       object.schemaRow,
-                       object.slotIndex,
-                       object.slotType};
-        record->fingerprint = fingerprint;
-        record->generationPlusOne = object.generationPlusOne;
-        record->valueCount = object.valueCount;
-        record->hasGeneration = object.hasGeneration;
-        record->occupied = true;
-        report_scene_sense("ev=scene_sense kind=object change=%s activity=%d session=0x%llX "
-                           "binding_rev=%llu source_gen=%llu msg_seq=%llu key=0x%08X tag=0x%08X "
-                           "object_row=%u type=%u index=%u slot_row=%u sense_schema=0x%08X "
-                           "schema_row=%u gen_plus_one=%u has_gen=%u delta_bits=%u values=%u",
-                           known ? "update" : "new",
-                           static_cast<int>(instance.view.binding.destination.activityIndex),
-                           static_cast<unsigned long long>(instance.view.binding.sessionId),
-                           static_cast<unsigned long long>(instance.view.binding.createdRevision),
-                           static_cast<unsigned long long>(input.sourceGeneration),
-                           static_cast<unsigned long long>(input.clientMessageSequence),
-                           object.registryKey,
-                           object.objectTag,
-                           object.objectRow,
-                           static_cast<unsigned>(object.slotType),
-                           static_cast<unsigned>(object.slotIndex),
-                           object.slotRow,
-                           object.senseSchema,
-                           object.schemaRow,
-                           object.generationPlusOne,
-                           object.hasGeneration ? 1U : 0U,
-                           object.deltaBits,
-                           object.valueCount);
-        for (std::size_t valueIndex = 0; valueIndex < values.size(); ++valueIndex) {
-            report_scene_value(instance, input, object, values[valueIndex], valueIndex);
-        }
-    }
-}
-
-/** Appends one observation and its complete owned value range. */
-[[nodiscard]] bool append_sense_observation(
-    SenseObservationSnapshot& output,
-    SenseObservation observation,
-    std::span<const middleware::bap::activity_message::sense_update::DecodedValue>
-        values) noexcept {
-    if (output.observationCount == output.observations.size()
-        || values.size() > output.values.size() - output.valueCount) {
-        return false;
-    }
-    observation.firstValue = static_cast<std::uint32_t>(output.valueCount);
-    observation.valueCount = static_cast<std::uint32_t>(values.size());
-    output.observations[output.observationCount++] = observation;
-    std::copy(values.begin(), values.end(), output.values.begin() + output.valueCount);
-    output.valueCount += values.size();
-    return true;
-}
-
-/** Replaces only fully decoded keys in an accepted packet; omitted or unsupported keys stay retained. */
-[[nodiscard]] bool
-retain_sense_observations(Instance& instance, const SenseInput& input, std::uint64_t now) noexcept {
-    namespace sense = middleware::bap::activity_message::sense_update;
-    if (!valid_sense_observation_input(input)) {
-        return false;
-    }
-    const sense::DecodedPacket& packet = input.decoded;
-    SenseObservationSnapshot next{};
-    next.revision = next_nonzero(instance.senseObservations.revision);
-    next.sourceGeneration = input.sourceGeneration;
-    for (std::size_t index = 0; index < packet.objectCount; ++index) {
-        const sense::DecodedObject& object = packet.objects[index];
-        if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration) continue;
-        const SenseObservationKey key{object.registryKey,
-                                      object.objectTag,
-                                      object.senseSchema,
-                                      object.schemaRow,
-                                      object.slotIndex,
-                                      object.slotType};
-        if (packet_has_sense_key(packet, key, index + 1)) {
-            continue;
-        }
-        SenseObservation observation{};
-        observation.binding = input.binding;
-        observation.key = key;
-        observation.sequence = next.revision;
-        observation.tick = now;
-        observation.sourceGeneration = input.sourceGeneration;
-        observation.clientMessageSequence = input.clientMessageSequence;
-        observation.generationPlusOne = object.generationPlusOne;
-        observation.hasGeneration = object.hasGeneration;
-        const std::span values(packet.values.data() + object.firstValue, object.valueCount);
-        if (!append_sense_observation(next, observation, values)) {
-            return false;
-        }
-    }
-    if (instance.senseObservations.sourceGeneration == input.sourceGeneration) {
-        const SenseObservationSnapshot& current = instance.senseObservations;
-        for (std::size_t index = 0; index < current.observationCount; ++index) {
-            const SenseObservation& observation = current.observations[index];
-            if (packet_has_sense_key(packet, observation.key, 0)
-                || observation.firstValue > current.valueCount
-                || observation.valueCount > current.valueCount - observation.firstValue) {
-                continue;
-            }
-            const std::span values(current.values.data() + observation.firstValue,
-                                   observation.valueCount);
-            static_cast<void>(append_sense_observation(next, observation, values));
-        }
-    }
-    instance.senseObservations = next;
-    instance.view.senseObservationCount = static_cast<std::uint32_t>(next.observationCount);
-    instance.view.senseObservationValueCount = static_cast<std::uint32_t>(next.valueCount);
-    instance.view.senseObservationRevision = next.revision;
-    instance.view.senseObservationSourceGeneration = next.sourceGeneration;
-    return true;
-}
-
-/** Applies one copied msg-6 decode summary. */
-void apply_sense(const SenseInput& input, std::uint64_t now) noexcept {
-    Instance* const instance = find_instance(input.binding);
-    if (instance == nullptr || !instance->view.active) {
-        ++g_droppedIngress;
-        return;
-    }
-    touch(*instance);
-    ++instance->view.senseCount;
-    trace_scene_sense(*instance, input);
-    static_cast<void>(retain_sense_observations(*instance, input, now));
-    Event event{};
-    event.binding = input.binding;
-    event.tick = now;
-    event.kind = EventKind::senseUpdate;
-    event.epochFirst = input.epochFirst;
-    event.epochSecond = input.epochSecond;
-    event.payloadBytes = input.payloadBytes;
-    event.peerHeardMask = input.peerHeardMask;
-    event.tailBits = input.tailBits;
-    event.consumedBits = input.consumedBits;
-    event.firstGroupBits = input.firstGroupBits;
-    event.firstRegistryKey = input.firstRegistryKey;
-    event.groupsSeen = input.groupsSeen;
-    event.groupsDecoded = input.groupsDecoded;
-    event.groupsSkipped = input.groupsSkipped;
-    event.objectsSeen = input.objectsSeen;
-    event.objectsDecoded = input.objectsDecoded;
-    event.firstSlotIndex = input.firstSlotIndex;
-    event.firstSlotType = input.firstSlotType;
-    // The event names one slot only: the first decoded ClientRef of the packet.
-    if (input.decoded.objectCount != 0) {
-        event.slotObjectTag = input.decoded.objects.front().objectTag;
-        event.slotSenseSchema = input.decoded.objects.front().senseSchema;
-    }
-    event.senseDecodeStatus = input.decodeStatus;
-    event.senseSnapshotRetained = valid_sense_observation_input(input);
-    event.hasFirstObject = input.hasFirstObject;
-    event.stateRevision = instance->view.stateRevision;
-    event.sourceGeneration = input.sourceGeneration;
-    event.clientMessageSequence = input.clientMessageSequence;
-    event.lifetimeState = instance->view.lifetimeState;
-    event.verdict = input.verdict;
-    append_event(event);
-    append_mission_input(event, event.senseSnapshotRetained ? &input.decoded : nullptr);
-    instance->view.lastEventSequence = g_sequence;
-}
-
-/** Retains one outer-valid client incident for inspection and parsed-field replay. */
-void apply_incident(const IncidentInput& input, std::uint64_t now) noexcept {
-    Instance* const instance = find_instance(input.binding);
-    if (instance == nullptr || !instance->view.active) {
-        ++g_droppedIngress;
-        ++g_droppedIncidents;
-        return;
-    }
-    touch(*instance);
-    ++instance->view.incidentsReceived;
-    Event event{};
-    event.binding = input.binding;
-    event.tick = now;
-    event.kind = EventKind::incidentReceived;
-    event.sourceGeneration = input.sourceGeneration;
-    event.clientMessageSequence = input.clientMessageSequence;
-    event.payloadBytes = input.payloadBytes;
-    fill_incident_event(event, input.incident, 0);
-    event.hasPlayerTrigger = input.hasPlayerTrigger;
-    if (input.hasPlayerTrigger) {
-        event.playerTriggerRegistryKey = input.playerTrigger.registryKey;
-        event.playerTriggerSlotType = input.playerTrigger.slotType;
-        event.playerTriggerSlotIndex = input.playerTrigger.slotIndex;
-        event.playerTriggerResolvedObjectId = input.playerTrigger.resolvedObjectId;
-    }
-    event.hasCinematic = input.hasCinematic;
-    if (input.hasCinematic) {
-        event.cinematicRegistryKey = input.cinematic.registryKey;
-        event.cinematicSlotType = input.cinematic.slotType;
-        event.cinematicSlotIndex = input.cinematic.slotIndex;
-        event.cinematicRuntimeObjectId = input.cinematic.runtimeObjectId;
-        event.cinematicEventValue = input.cinematic.eventValue;
-        event.cinematicSignal = input.cinematicSignal;
-    }
-    append_event(event);
-    append_mission_input(event, nullptr);
-    instance->view.lastEventSequence = g_sequence;
-
-    IncidentRecord* const record = reserve_incident_record();
-    if (record == nullptr) {
-        ++g_droppedIncidents;
-        return;
-    }
-    *record = {};
-    record->binding = input.binding;
-    record->incident = input.incident;
-    record->sequence = g_sequence;
-    record->tick = now;
-    record->lastSourceGeneration = input.sourceGeneration;
-    record->clientMessageSequence = input.clientMessageSequence;
-    record->payloadBytes = input.payloadBytes;
-    record->status = IncidentStatus::received;
-}
-
 /** Retains one safe committed client State change in Host and ordered mission histories. */
 void apply_client_state_change(const ClientStateChangeInput& input, std::uint64_t now) noexcept {
     Instance* const instance = find_instance(input.binding);
@@ -1020,274 +505,6 @@ void read_events_after(EventCursor after, EventRead& output) noexcept {
     ReleaseSRWLockShared(&g_lock);
 }
 
-/** Reads the current accepted-mission-input position without replaying retained history. */
-MissionInputCursor current_mission_input_cursor() noexcept {
-    AcquireSRWLockShared(&g_lock);
-    const MissionInputCursor cursor{g_eventGeneration, g_missionInputSequence};
-    ReleaseSRWLockShared(&g_lock);
-    return cursor;
-}
-
-/** @return True when durable mission State still owes this retained accepted row. */
-[[nodiscard]] bool mission_input_owed(const MissionInputRecord& record) noexcept {
-    state::activity::mission::InputSequenceSnapshot cursors{};
-    // A faulted binding owes nothing. Its program is skipped, so it never commits, and its rows
-    // would otherwise be retained for the life of the process.
-    if (!state::activity::mission::input_sequence_snapshot(record.view.event.binding, cursors)
-        || cursors.faulted) {
-        return false;
-    }
-    const std::uint64_t sequence = record.view.event.missionSequence;
-    return sequence > cursors.committed && sequence <= cursors.issued;
-}
-
-/** Drops every retained row that durable mission State no longer owes. */
-void retire_settled_mission_inputs() noexcept {
-    static_cast<void>(std::erase_if(g_missionInputs, [](const MissionInputRecord& record) noexcept {
-        return !mission_input_owed(record);
-    }));
-}
-
-/** Copies accepted client mission inputs after one cursor. */
-void read_mission_inputs_after(MissionInputCursor after, MissionInputRead& output) noexcept {
-    output = {};
-    AcquireSRWLockExclusive(&g_lock);
-    output.reset = after.generation != g_eventGeneration;
-    const std::uint64_t afterSequence = output.reset ? 0 : after.sequence;
-    retire_settled_mission_inputs();
-    output.cursor = {g_eventGeneration, afterSequence};
-    if (!g_missionInputs.empty()) {
-        const std::uint64_t retainedPredecessor = g_missionInputs.front().view.sequence - 1;
-        if (afterSequence < retainedPredecessor) {
-            output.gap = true;
-            output.missed = retainedPredecessor - afterSequence;
-        }
-        for (const MissionInputRecord& record : g_missionInputs) {
-            if (record.view.sequence <= afterSequence) {
-                continue;
-            }
-            if (output.count == output.events.size()) {
-                break;
-            }
-            output.events[output.count++] = record.view;
-        }
-        if (output.count != 0) {
-            output.cursor.sequence = output.events[output.count - 1].sequence;
-        }
-    } else if (afterSequence < g_missionInputSequence) {
-        output.gap = true;
-        output.missed = g_missionInputSequence - afterSequence;
-        output.cursor.sequence = g_missionInputSequence;
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-}
-
-/** Copies the exact Sense values owned by one retained accepted-input row. */
-bool mission_input_sense_snapshot(std::uint64_t sequence,
-                                  SenseObservationSnapshot& output) noexcept {
-    namespace sense = middleware::bap::activity_message::sense_update;
-    output = {};
-    if (sequence == 0) {
-        return false;
-    }
-    AcquireSRWLockShared(&g_lock);
-    const MissionInputRecord* selected = nullptr;
-    for (std::size_t offset = g_missionInputs.size(); offset != 0; --offset) {
-        const MissionInputRecord& candidate = g_missionInputs[offset - 1];
-        if (candidate.view.sequence == sequence) {
-            selected = &candidate;
-            break;
-        }
-    }
-    bool copied = selected != nullptr && selected->hasSense
-                  && selected->view.event.kind == EventKind::senseUpdate;
-    if (copied) {
-        const Event& event = selected->view.event;
-        const sense::DecodedPacket& packet = selected->sense;
-        copied = sense::observation_packet(packet);
-        if (copied) {
-            output.revision = event.sequence;
-            output.sourceGeneration = event.sourceGeneration;
-        }
-        for (std::size_t index = 0; copied && index < packet.objectCount; ++index) {
-            const sense::DecodedObject& object = packet.objects[index];
-            if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration) continue;
-            if (object.firstValue > packet.valueCount
-                || object.valueCount > packet.valueCount - object.firstValue) {
-                copied = false;
-                break;
-            }
-            SenseObservation observation{};
-            observation.binding = event.binding;
-            observation.key = {object.registryKey,
-                               object.objectTag,
-                               object.senseSchema,
-                               object.schemaRow,
-                               object.slotIndex,
-                               object.slotType};
-            observation.sequence = event.sequence;
-            observation.tick = event.tick;
-            observation.sourceGeneration = event.sourceGeneration;
-            observation.clientMessageSequence = event.clientMessageSequence;
-            observation.generationPlusOne = object.generationPlusOne;
-            observation.hasGeneration = object.hasGeneration;
-            copied = append_sense_observation(
-                output, observation, {packet.values.data() + object.firstValue, object.valueCount});
-        }
-    }
-    if (!copied) {
-        output = {};
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return copied;
-}
-
-/** Copies one exact generic client-message snapshot owned by the mission-input feed. */
-bool mission_input_client_message_snapshot(std::uint64_t sequence,
-                                           ClientMessageSnapshot& output) noexcept {
-    output = {};
-    if (sequence == 0) {
-        return false;
-    }
-    AcquireSRWLockShared(&g_lock);
-    const MissionInputRecord* selected = nullptr;
-    for (std::size_t offset = g_missionInputs.size(); offset != 0; --offset) {
-        const MissionInputRecord& candidate = g_missionInputs[offset - 1];
-        if (candidate.view.sequence == sequence) {
-            selected = &candidate;
-            break;
-        }
-    }
-    const bool copied = selected != nullptr && selected->hasClientMessage
-                        && selected->view.event.kind == EventKind::clientMessageReceived;
-    if (copied) {
-        output = selected->clientMessage;
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return copied;
-}
-
-/** Copies the latest complete Sense observations for one exact activity generation. */
-bool snapshot_sense_observations(const state::activity::SessionBinding& binding,
-                                 SenseObservationSnapshot& output) noexcept {
-    output = {};
-    AcquireSRWLockShared(&g_lock);
-    const Instance* const instance = find_instance(binding);
-    const bool found = instance != nullptr;
-    if (found) {
-        output = instance->senseObservations;
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return found;
-}
-
-/** Selects one exact reflected scalar without changing the retained snapshot. */
-SenseScalarStatus select_sense_scalar(const SenseObservationSnapshot& snapshot,
-                                      const state::activity::SessionBinding& binding,
-                                      const SenseScalarIdentity& identity,
-                                      SenseScalarSample& output) noexcept {
-    namespace sense = middleware::bap::activity_message::sense_update;
-    output = {};
-    if (binding.sessionId == state::activity::kAbsentSessionId
-        || binding.createdRevision == state::activity::kInvalidRevision
-        || identity.object.schemaRow == sense::kAbsentRuntimeRow
-        || identity.fieldSchemaRow == sense::kAbsentRuntimeRow
-        || identity.fieldRow == sense::kAbsentRuntimeRow
-        || identity.fieldSchemaRow != identity.object.schemaRow) {
-        return SenseScalarStatus::invalidIdentity;
-    }
-    if (snapshot.sourceGeneration == 0 || snapshot.observationCount > snapshot.observations.size()
-        || snapshot.valueCount > snapshot.values.size()) {
-        return SenseScalarStatus::invalidSnapshot;
-    }
-    const auto sameKey = [&identity](const SenseObservationKey& candidate) noexcept {
-        const SenseObservationKey& expected = identity.object;
-        return candidate.registryKey == expected.registryKey
-               && candidate.objectTag == expected.objectTag
-               && candidate.senseSchema == expected.senseSchema
-               && candidate.schemaRow == expected.schemaRow
-               && candidate.slotIndex == expected.slotIndex
-               && candidate.slotType == expected.slotType;
-    };
-    bool found = false;
-    for (std::size_t observationIndex = 0; observationIndex < snapshot.observationCount;
-         ++observationIndex) {
-        const SenseObservation& observation = snapshot.observations[observationIndex];
-        if (!same_binding(observation.binding, binding) || !sameKey(observation.key)) {
-            continue;
-        }
-        if (observation.sourceGeneration != snapshot.sourceGeneration
-            || observation.firstValue > snapshot.valueCount
-            || observation.valueCount > snapshot.valueCount - observation.firstValue) {
-            output = {};
-            return SenseScalarStatus::invalidSnapshot;
-        }
-        for (std::size_t valueIndex = 0; valueIndex < observation.valueCount; ++valueIndex) {
-            const sense::DecodedValue& value = snapshot.values[observation.firstValue + valueIndex];
-            if (value.schemaRow != identity.fieldSchemaRow || value.fieldRow != identity.fieldRow
-                || value.fieldOrdinal != identity.fieldOrdinal
-                || value.occurrence != identity.occurrence || value.kind != identity.kind) {
-                continue;
-            }
-            if (found) {
-                output = {};
-                return SenseScalarStatus::ambiguous;
-            }
-            output.binding = observation.binding;
-            output.identity = identity;
-            output.value = value;
-            output.observationRevision = observation.sequence;
-            output.tick = observation.tick;
-            output.sourceGeneration = observation.sourceGeneration;
-            output.clientMessageSequence = observation.clientMessageSequence;
-            output.generationPlusOne = observation.generationPlusOne;
-            output.hasGeneration = observation.hasGeneration;
-            found = true;
-        }
-    }
-    return found ? SenseScalarStatus::ready : SenseScalarStatus::notFound;
-}
-
-/** @return True when two selected scalars have the same presence and raw typed value. */
-bool same_sense_scalar_value(const SenseScalarSample& left,
-                             const SenseScalarSample& right) noexcept {
-    if (left.value.kind != right.value.kind || left.value.present != right.value.present) {
-        return false;
-    }
-    if (!left.value.present) {
-        return true;
-    }
-    return left.value.unsignedValue == right.value.unsignedValue
-           && left.value.signedValue == right.value.signedValue
-           && std::bit_cast<std::uint32_t>(left.value.realValue)
-                  == std::bit_cast<std::uint32_t>(right.value.realValue);
-}
-
-/** @return True when both samples name the same ActivityClient and reported object generations. */
-bool same_sense_scalar_generations(const SenseScalarSample& left,
-                                   const SenseScalarSample& right) noexcept {
-    return left.sourceGeneration != 0 && left.sourceGeneration == right.sourceGeneration
-           && left.hasGeneration && right.hasGeneration
-           && left.generationPlusOne == right.generationPlusOne;
-}
-
-/** @return Stable text for one exact scalar-selection result. */
-const char* sense_scalar_status_name(SenseScalarStatus status) noexcept {
-    switch (status) {
-    case SenseScalarStatus::ready:
-        return "ready";
-    case SenseScalarStatus::invalidIdentity:
-        return "invalid_identity";
-    case SenseScalarStatus::invalidSnapshot:
-        return "invalid_snapshot";
-    case SenseScalarStatus::notFound:
-        return "not_found";
-    case SenseScalarStatus::ambiguous:
-        return "ambiguous";
-    }
-    return "invalid_status";
-}
-
 /** Reads the committed Auth state for one exact activity generation. */
 bool auth_state(const state::activity::SessionBinding& binding, AuthState& output) noexcept {
     output = {};

+ 34 - 11
Sunrise/src/server/activity/host_runtime.h

@@ -42,6 +42,8 @@ inline constexpr std::size_t kSenseObservationCapacity = 128;
 inline constexpr std::size_t kSenseObservationValueCapacity = 1024;
 /** Per-slot counts the squad Sense body can carry. Its nested array is eight elements. */
 inline constexpr std::size_t kSquadSlotCapacity = 8;
+/** Objective task groups a squad publishes one cost for. */
+inline constexpr std::size_t kSquadObjectiveGroupCount = 24;
 /** Distinct exact ClientRef counters retained for one activity generation. */
 inline constexpr std::size_t kScriptableGuardCapacity =
     state::build_data::scenarios::kRosterSlotCapacity;
@@ -122,13 +124,17 @@ enum class EventKind : std::uint8_t {
     cinematicTerminated = 30,
     /** Accepted native Ghost interaction progress for one exact type-65 slot. */
     ghostLinkState = 31,
+    /** A named actor's movement or delivery level changed. */
     actorPathState = 32,
+    /** The client accepted a player's use of one interactable object. */
     objectInteracted = 33,
     /** Native cinematic_skip incident for an exact Type-6 source. */
     cinematicSkipRequested = 34,
-    /** Death-with-Ghost observations for the current private activity's joined party. */
+    /** Life counts of the private activity's joined party changed. */
     fireteamState = 35,
+    /** An interactable object's presence, alive or ownership level changed. */
     objectState = 36,
+    /** A damage monitor published new health, shield or revision values. */
     damageState = 37,
 };
 
@@ -453,7 +459,10 @@ struct Event final {
     std::uint64_t peerSessionGeneration{};
     /** Peer client key bound by its join, or zero before one. */
     std::uint64_t peerMemberKey{};
-    std::uint16_t fireteamAlive{}, fireteamDead{}, fireteamUnknown{};
+    /** Party life counts, for fireteamState events. */
+    std::uint16_t fireteamAlive{};
+    std::uint16_t fireteamDead{};
+    std::uint16_t fireteamUnknown{};
     /** Committed mission phase, for phaseEntered events. */
     std::uint32_t missionPhase{};
     /** Mission phase this commit replaced. */
@@ -468,22 +477,34 @@ struct Event final {
     std::int32_t triggerValue{};
     /** True when the whole watched set is inside the volume. */
     bool triggerAll{};
-    float damageHealth{-1.0F}, damageShield{-1.0F};
+    /** Health and shield fractions, for damageState events. Negative until published. */
+    float damageHealth{-1.0F};
+    float damageShield{-1.0F};
     std::int32_t damageRevision{};
+    /** Object level, for objectState and objectInteracted events. */
     std::int32_t objectGeneration{};
-    bool objectPresent{}, objectAlive{}, objectOwnerKnown{}, objectHasOwner{};
+    bool objectPresent{};
+    bool objectAlive{};
+    bool objectOwnerKnown{};
+    bool objectHasOwner{};
     std::uint64_t objectOwnerKey{};
+    /** Ghost-link level, for ghostLinkState events. */
     std::int32_t ghostGeneration{};
     float ghostProgress{};
     bool ghostActive{};
-    std::int32_t actorGeneration{}, actorPathRevision{}, actorPathState{};
-    std::int32_t actorDeliveryRevision{}, actorDeliveryState{};
+    /** Named actor level, for actorPathState events. */
+    std::int32_t actorGeneration{};
+    std::int32_t actorPathRevision{};
+    std::int32_t actorPathState{};
+    std::int32_t actorDeliveryRevision{};
+    std::int32_t actorDeliveryState{};
     bool actorDeliveryKnown{};
     bool actorDead{};
-    /** Per-slot member counts the client published, for squadState events. */
-    std::array<float, 24> squadObjectiveCosts{};
+    /** Objective costs the squad published, one per task group, with a bit per known cost. */
+    std::array<float, kSquadObjectiveGroupCount> squadObjectiveCosts{};
     std::uint32_t squadObjectiveCostMask{};
     std::uint32_t squadObjectiveRevision{};
+    /** Per-slot member counts the client published, for squadState events. */
     std::array<std::int32_t, kSquadSlotCapacity> squadSlotCounts{};
     /** Alive members the client published. Six bits on the wire, so 0 through 63. */
     std::int32_t squadAliveCount{};
@@ -587,7 +608,8 @@ struct Event final {
     middleware::bap::activity_message::sense_update::DecodeStatus senseDecodeStatus{
         middleware::bap::activity_message::sense_update::DecodeStatus::malformed};
     state::activity::receipts::Verdict verdict{state::activity::receipts::Verdict::absent};
-    bool senseSnapshotRetained{}; // Only bounded, fully owned observations may enter Lua.
+    /** The packet's observations were retained, so a Lua snapshot may be built from them. */
+    bool senseSnapshotRetained{};
     bool hasFirstObject{};
     bool clientStateHasRegion{};
     bool clientStateHasCurrentRegion{};
@@ -595,12 +617,13 @@ struct Event final {
     bool clientStateHasTeleport{};
     bool hasPlayerTrigger{};
     bool hasCinematic{};
+
+    /** @return True when this Sense event carries observations a script may read. */
     [[nodiscard]] bool has_sense_observations() const noexcept {
         using Status = middleware::bap::activity_message::sense_update::DecodeStatus;
         return kind == EventKind::senseUpdate && senseSnapshotRetained
-            && (senseDecodeStatus == Status::complete || senseDecodeStatus == Status::partial);
+               && (senseDecodeStatus == Status::complete || senseDecodeStatus == Status::partial);
     }
-
 };
 
 /** Position after one event in one reset generation. */

+ 3 - 3
Sunrise/src/server/activity/host_runtime_mission_inputs.cpp

@@ -12,6 +12,7 @@
 #include <vector>
 
 #include "../../core/logging/log.h"
+#include "../../middleware/bap/activity_message/sense_observation_packet.h"
 #include "../../state/activity/mission/runtime.h"
 #include "../../state/activity/runtime.h"
 #include "host_runtime_internal.h"
@@ -227,15 +228,14 @@ bool mission_input_sense_snapshot(std::uint64_t sequence,
     if (copied) {
         const Event& event = selected->view.event;
         const sense::DecodedPacket& packet = selected->sense;
-        copied = packet.status == sense::DecodeStatus::complete && !packet.objectsTruncated
-                 && !packet.valuesTruncated && packet.objectCount <= packet.objects.size()
-                 && packet.valueCount <= packet.values.size();
+        copied = sense::observation_packet(packet);
         if (copied) {
             output.revision = event.sequence;
             output.sourceGeneration = event.sourceGeneration;
         }
         for (std::size_t index = 0; copied && index < packet.objectCount; ++index) {
             const sense::DecodedObject& object = packet.objects[index];
+            if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration) continue;
             if (object.firstValue > packet.valueCount
                 || object.valueCount > packet.valueCount - object.firstValue) {
                 copied = false;

+ 9 - 6
Sunrise/src/server/activity/host_runtime_names.cpp

@@ -67,14 +67,17 @@ const char* event_name(EventKind kind) noexcept {
     case EventKind::cinematicStarted:
         return "cinematic started";
     case EventKind::actorPathState:
-        return "actor_path_state";
-    case EventKind::fireteamState: return "fireteam_state";
-    case EventKind::damageState: return "damage_state";
-    case EventKind::objectState: return "object_state";
+        return "actor path state";
+    case EventKind::fireteamState:
+        return "fireteam state";
+    case EventKind::damageState:
+        return "damage state";
+    case EventKind::objectState:
+        return "object state";
     case EventKind::objectInteracted:
-        return "object_interacted";
+        return "object interacted";
     case EventKind::ghostLinkState:
-        return "ghost_link_state";
+        return "ghost link state";
     case EventKind::cinematicSkipRequested:
         return "cinematic skip requested";
     case EventKind::cinematicTerminated:

+ 29 - 789
Sunrise/src/server/activity/host_runtime_scriptable.cpp

@@ -3,15 +3,9 @@
 #include <limits>
 #include <new>
 
-#include "../../middleware/bap/activity_message/sensor_auth_update.h"
 #include "../../middleware/bap/activity_message/mission_auth_patch.h"
-#include "../../middleware/bap/activity_message/squad_objective_auth.h"
-#include "../../middleware/bap/activity_message/combatant_path_auth.h"
-#include "../../middleware/bap/activity_message/combatant_action_auth.h"
-#include "../../middleware/bap/activity_message/combatant_delivery_auth.h"
-#include "../../middleware/bap/activity_message/combatant_retire_auth.h"
+#include "../../middleware/bap/activity_message/sensor_auth_update.h"
 #include "../../state/activity/mission/runtime.h"
-#include "../../core/logging/log.h"
 #include "../../state/activity/runtime.h"
 #include "../gameplay/squad_entity_retirement.h"
 #include "host_runtime_internal.h"
@@ -233,22 +227,6 @@ valid_state_local_group(const ScriptableTarget& target,
     if (pending.byteCount == 0 || pending.byteCount > pending.body.size()) {
         return false;
     }
-    // Record the exact transported Ember ship body, including path revision and manifest.
-    // Four fixed targets and short bodies keep this diagnostic bounded during flight.
-    if (pending.target.registryKey == 0xF6FFB59EU && pending.target.slotType == 2
-        && (pending.target.slotIndex == 22 || pending.target.slotIndex == 28
-            || pending.target.slotIndex == 30 || pending.target.slotIndex == 32)
-        && pending.byteCount <= 128) {
-        std::array<char,257> hex{};
-        constexpr char digits[] = "0123456789abcdef";
-        for (std::size_t i=0;i<pending.byteCount;++i) {
-            const auto byte=std::to_integer<unsigned>(pending.body[i]);
-            hex[i*2]=digits[byte>>4]; hex[i*2+1]=digits[byte&15];
-        }
-        core::log::writef(core::log::Channel::server,core::log::Level::info,
-            "ev=ember_transport stage=auth_staged slot=%u bits=%u body=%s",
-            static_cast<unsigned>(pending.target.slotIndex),static_cast<unsigned>(pending.bitCount),hex.data());
-    }
     PendingScriptableOverride owned = pending;
     if (owned.expectedActivityClientGeneration == 0) {
         owned.expectedActivityClientGeneration = sourceGeneration;
@@ -547,35 +525,40 @@ void apply_scriptable_control(const ScriptableRequest& request, std::uint64_t no
     } else {
         encoded = false;
     }
-    // These mission APIs emit root-field patches; native overrides are complete Auth
-    // replacements. Keep the last transported state before publishing another command.
-    namespace message = middleware::bap::activity_message;
-    const auto patch = std::span(pending.body).first(written);
-    const bool missionPatch = encoded && (request.kind == ScriptableOverrideKind::squad
-        || (request.kind == ScriptableOverrideKind::sdkAuth
-            && ((request.target.slotType == 1 && request.target.authSchema == squad::kSchema
-                 && message::squad_objective::validate(patch, pending.bitCount))
-                || (request.target.slotType == 2 && request.target.authSchema == 0x80807DA1U
-                    && (message::combatant_path::validate(patch, pending.bitCount)
-                        || message::combatant_delivery::validate(patch, pending.bitCount)
-                        || message::combatant_action::validate(patch, pending.bitCount)
-                        || message::combatant_retire::validate(patch, pending.bitCount))))));
-    if (missionPatch) {
+    // A mission API body carries only the root fields it sets, and the native override replaces
+    // the whole object. Compose it over the last transported body for the same ClientRef first.
+    namespace patching = middleware::bap::activity_message::mission_auth_patch;
+    const std::span<const std::byte> patch = std::span(pending.body).first(written);
+    patching::Layout layout{};
+    const bool rootPatch =
+        encoded
+        && (request.kind == ScriptableOverrideKind::squad
+            || request.kind == ScriptableOverrideKind::sdkAuth)
+        && patching::parse(request.target.authSchema, patch, pending.bitCount, layout);
+    if (rootPatch) {
         std::span<const std::byte> previous{};
-        std::size_t previousBits=0;
-        // A tail cannot contain the same ClientRef as another pending body, so its
-        // predecessor is always in the transported estate, not an uncommitted row.
+        std::size_t previousBits = 0;
+        // A tail never holds the same ClientRef as another pending body, so the predecessor is
+        // always in the transported estate.
         for (const auto& retained : instance->scriptableAuthEstate) {
             if (same_client_ref(retained.target, request.target)) {
-                previous=std::span(retained.body).first(retained.byteCount);
-                previousBits=retained.bitCount;
+                previous = std::span(retained.body).first(retained.byteCount);
+                previousBits = retained.bitCount;
                 break;
             }
         }
-        std::size_t composedBits{};
-        encoded=message::mission_auth_patch::compose(request.target.authSchema,
-            previous,previousBits,patch,pending.bitCount,pending.body,written,composedBits);
-        if (encoded) pending.bitCount=static_cast<std::uint16_t>(composedBits);
+        std::size_t composedBits = 0;
+        encoded = patching::compose(request.target.authSchema,
+                                    previous,
+                                    previousBits,
+                                    patch,
+                                    pending.bitCount,
+                                    pending.body,
+                                    written,
+                                    composedBits);
+        if (encoded) {
+            pending.bitCount = static_cast<std::uint16_t>(composedBits);
+        }
     }
     if (!encoded || written > (std::numeric_limits<std::uint16_t>::max)()) {
         ++g_refusedControls;
@@ -714,747 +697,4 @@ ScriptableWithdrawStatus withdraw_scriptable_output(const state::activity::Sessi
     return ScriptableWithdrawStatus::absent;
 }
 
-/** Queues one generation-bound type-23 update for an exact package-derived ClientRef. */
-bool request_type23_override(const state::activity::SessionBinding& binding,
-                             const ScriptableTarget& target,
-                             auth::Type23Channel channel,
-                             float value,
-                             bool snap,
-                             std::uint64_t expectedActivityClientGeneration,
-                             const ScriptableOutputReservation* reservation) noexcept {
-    const auto channelIndex = static_cast<std::size_t>(channel);
-    if (target.slotType != auth::kType23SlotType || target.authSchema != auth::kType23Schema
-        || channelIndex >= auth::kType23ChannelCount || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.channel = channel;
-    request.value = value;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::type23;
-    request.snap = snap;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one package-owned type-4 entry transition. */
-bool request_state_local_type4_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::int32_t entryIndex,
-    bool active,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation,
-    const ScriptableOutputReservation* burstHead) noexcept {
-    if (target.slotType != auth::kType4SlotType || target.authSchema != auth::kType4Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup) || entryIndex < 0
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.entryIndex = entryIndex;
-    request.active = active;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::object;
-    if (burstHead != nullptr) {
-        // Every body in one burst answers under the revision the head reserved for all of them.
-        request.burstMember = true;
-        request.expectedRevision = burstHead->revision;
-        request.expectedIntentSequence = burstHead->intentSequence;
-    }
-    return enqueue_request(request, burstHead != nullptr ? nullptr : reservation);
-}
-
-/** Queues one retained named actor-channel write. */
-bool request_state_local_type2_channel_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint32_t channelHash,
-    float value,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType2SlotType || target.authSchema != auth::kType2Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup) || !std::isfinite(value)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.channelHash = channelHash;
-    request.value = value;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::combatantChannel;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues squad-member binding for one exact generated type-2 combatant. */
-bool request_state_local_type2_squad_binding(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType2SlotType || target.authSchema != auth::kType2Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::combatantBinding;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one generation-bound type-23 update from an exact generated roster group. */
-bool request_state_local_type23_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    auth::Type23Channel channel,
-    float value,
-    bool snap,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    const auto channelIndex = static_cast<std::size_t>(channel);
-    if (target.slotType != auth::kType23SlotType || target.authSchema != auth::kType23Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || channelIndex >= auth::kType23ChannelCount || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.channel = channel;
-    request.value = value;
-    request.kind = ScriptableOverrideKind::type23;
-    request.snap = snap;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one type-31 pulse for an exact package-derived ClientRef. */
-bool request_type31_override(const state::activity::SessionBinding& binding,
-                             const ScriptableTarget& target,
-                             const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType31SlotType || target.authSchema != auth::kType31Schema) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.kind = ScriptableOverrideKind::type31;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one generation-bound type-31 pulse from an exact generated roster group. */
-bool request_state_local_type31_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType31SlotType || target.authSchema != auth::kType31Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::type31;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one state-local sequence override, but only for an exact type-5 target. */
-bool request_state_local_sequence_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType5SlotType || target.authSchema != auth::kType5Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::sequence;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one state-local cinematic override, but only for an exact type-5 target. */
-bool request_state_local_cinematic_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    bool active,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType6SlotType || target.authSchema != auth::kType6Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::cinematic;
-    request.active = active;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one type-42 performance start; the encoder assigns the rising generation. */
-bool request_state_local_performance_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint32_t stateNameHash,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType42SlotType || target.authSchema != auth::kType42Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0 || stateNameHash == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::performance;
-    request.nameHash = stateNameHash;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one objective reset from an exact generated roster group. */
-bool request_state_local_objective_reset(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType3SlotType || target.authSchema != auth::kType3Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::objectiveReset;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one generation-bound authored-task change from an exact generated roster group. */
-bool request_state_local_task_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType38SlotType || target.authSchema != auth::kType38Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::task;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one generation-bound authored-scene activation from an exact generated roster group. */
-bool request_state_local_authored_scene_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != scene::kAuthoredSceneSlotType
-        || target.authSchema != scene::kAuthoredSceneAuthSchema
-        || !valid_state_local_group(target, stateLocalRosterGroup)
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::authoredScene;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one SDK-bounded dialogue line pulse from an exact generated roster group. */
-bool request_state_local_dialogue_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup& stateLocalRosterGroup,
-    std::uint16_t cueIndex,
-    std::uint16_t authoredCueCount,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (target.slotType != auth::kType53SlotType || target.authSchema != auth::kType53Schema
-        || !valid_state_local_group(target, stateLocalRosterGroup) || authoredCueCount == 0
-        || authoredCueCount > auth::kType53EntryCount || cueIndex >= authoredCueCount
-        || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    request.stateLocalRosterGroup = stateLocalRosterGroup;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.dialogueCue = cueIndex;
-    request.kind = ScriptableOverrideKind::dialogue;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one squad placement intent for an exact package-derived ClientRef. */
-bool request_squad_override(const state::activity::SessionBinding& binding,
-                            const ScriptableTarget& target,
-                            const state::build_data::scenarios::RosterGroup* stateLocalRosterGroup,
-                            std::span<const std::int32_t> requestedCounts,
-                            squad::Mode mode,
-                            std::uint64_t expectedActivityClientGeneration,
-                            std::optional<std::uint32_t> nameHash,
-                            const ScriptableOutputReservation* reservation,
-                            std::array<std::int8_t, 4> authoredProfile) noexcept {
-    if (target.slotType != squad::kSlotType || target.authSchema != squad::kSchema
-        || (target.stateLocalRoster
-                ? stateLocalRosterGroup == nullptr
-                      || !valid_state_local_group(target, *stateLocalRosterGroup)
-                : stateLocalRosterGroup != nullptr)
-        || requestedCounts.size() < squad::kMinimumRequestedCountLength
-        || requestedCounts.size() > squad::kMaximumRequestedCountLength
-        || expectedActivityClientGeneration == 0
-        || !squad::valid_mode(mode)
-        || !std::ranges::all_of(requestedCounts, [](std::int32_t count) { return count >= 0; })) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    if (stateLocalRosterGroup != nullptr) {
-        request.stateLocalRosterGroup = *stateLocalRosterGroup;
-    }
-    std::ranges::copy(requestedCounts, request.requestedCounts.begin());
-    request.requestedCountLength = requestedCounts.size();
-    request.squadAuthoredProfile = authoredProfile;
-    request.nameHash = nameHash;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.squadMode = mode;
-    request.kind = ScriptableOverrideKind::squad;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one generation-bound activity lifetime state through the serialized output slot. */
-bool request_lifetime_override(const state::activity::SessionBinding& binding,
-                               std::uint8_t lifetimeState,
-                               std::uint64_t expectedActivityClientGeneration,
-                               const ScriptableOutputReservation* reservation) noexcept {
-    // Above the highest jump-table entry the client's spawn gate jumps out of its image.
-    if (lifetimeState > kMaximumLifetimeState || expectedActivityClientGeneration == 0) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::lifetime;
-    request.lifetimeState = lifetimeState;
-    return enqueue_request(request, reservation);
-}
-
-/** Queues one exact SDK-compiled Auth body for a generation-bound ClientRef. */
-bool request_sdk_auth_override(
-    const state::activity::SessionBinding& binding,
-    const ScriptableTarget& target,
-    const state::build_data::scenarios::RosterGroup* stateLocalRosterGroup,
-    std::span<const std::byte> body,
-    std::uint16_t bitCount,
-    std::uint64_t expectedActivityClientGeneration,
-    const ScriptableOutputReservation* reservation) noexcept {
-    if (body.empty() || body.size() > scene::kAuthOverrideByteCapacity
-        || body.size() > (std::numeric_limits<std::uint16_t>::max)()
-        || expectedActivityClientGeneration == 0 || !valid_auth_storage(body, bitCount)
-        || (target.stateLocalRoster
-                ? stateLocalRosterGroup == nullptr
-                      || !valid_state_local_group(target, *stateLocalRosterGroup)
-                : stateLocalRosterGroup != nullptr)) {
-        return false;
-    }
-    ScriptableRequest request{};
-    request.binding = binding;
-    request.target = target;
-    if (stateLocalRosterGroup != nullptr) {
-        request.stateLocalRosterGroup = *stateLocalRosterGroup;
-    }
-    std::copy(body.begin(), body.end(), request.authBody.begin());
-    request.authBitCount = bitCount;
-    request.authByteCount = static_cast<std::uint16_t>(body.size());
-    request.expectedActivityClientGeneration = expectedActivityClientGeneration;
-    request.kind = ScriptableOverrideKind::sdkAuth;
-    return enqueue_request(request, reservation);
-}
-
-/** Reads the one pending typed ClientRef body without changing its counter. */
-bool pending_scriptable_override(const state::activity::SessionBinding& binding,
-                                 PendingScriptableOverride& output) noexcept {
-    output = {};
-    AcquireSRWLockShared(&g_lock);
-    const Instance* const instance = find_instance(binding);
-    const bool pending = instance != nullptr && instance->view.active
-                         && instance->view.outputPending
-                         && instance->view.outputKind == OutputKind::scriptableOverride
-                         && instance->pendingScriptable.revision != 0;
-    if (pending) {
-        output = instance->pendingScriptable;
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return pending;
-}
-
-/** Reads one pending body only for the ActivityClient generation that authorized it. */
-bool pending_scriptable_override_for_activity_client(const state::activity::SessionBinding& binding,
-                                                     std::uint64_t activityClientGeneration,
-                                                     PendingScriptableOverride& output) noexcept {
-    output = {};
-    AcquireSRWLockExclusive(&g_lock);
-    Instance* const instance = find_instance(binding);
-    bool pending = instance != nullptr && instance->view.active && instance->view.outputPending
-                   && instance->view.outputKind == OutputKind::scriptableOverride
-                   && instance->pendingScriptable.revision != 0;
-    if (pending && instance->pendingScriptable.expectedActivityClientGeneration != 0
-        && instance->pendingScriptable.expectedActivityClientGeneration
-               != activityClientGeneration) {
-        const std::uint64_t revision = instance->pendingScriptable.revision;
-        cancel_pending(*instance, binding, revision);
-        pending = false;
-    }
-    if (pending) {
-        output = instance->pendingScriptable;
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-    return pending;
-}
-
-/** Cancels one exact unstaged typed override revision without advancing its slot counter. */
-bool cancel_pending_scriptable_override(const state::activity::SessionBinding& binding,
-                                        std::uint64_t expectedRevision) noexcept {
-    if (expectedRevision == 0) {
-        return false;
-    }
-    AcquireSRWLockExclusive(&g_lock);
-    Instance* const instance = find_instance(binding);
-    const bool canceled = instance != nullptr && instance->view.active
-                          && instance->view.outputPending
-                          && instance->view.outputKind == OutputKind::scriptableOverride
-                          && instance->pendingScriptable.revision == expectedRevision;
-    if (canceled) {
-        cancel_pending(*instance, binding, expectedRevision);
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-    return canceled;
-}
-
-/** Records one refused typed-body attempt without consuming its sequence or generation. */
-void note_scriptable_attempt(const state::activity::SessionBinding& binding,
-                             std::uint64_t sourceGeneration,
-                             const PendingScriptableOverride& pending,
-                             OutputStatus status) noexcept {
-    if (pending.revision == 0 || status == OutputStatus::idle || status == OutputStatus::pending
-        || status == OutputStatus::transportStaged || status == OutputStatus::canceled) {
-        return;
-    }
-    AcquireSRWLockExclusive(&g_lock);
-    Instance* const instance = find_instance(binding);
-    if (instance != nullptr && instance->view.outputPending
-        && instance->view.outputKind == OutputKind::scriptableOverride
-        && same_pending(instance->pendingScriptable, pending)) {
-        instance->view.lastOutputAttemptTick = GetTickCount64();
-        instance->view.lastOutputSourceGeneration = sourceGeneration;
-        ++instance->view.outputAttempts;
-        instance->view.outputStatus = status;
-        const bool terminal = status == OutputStatus::noLayout || status == OutputStatus::noGroups
-                              || status == OutputStatus::noOverrideTarget
-                              || status == OutputStatus::ambiguousLinks
-                              || status == OutputStatus::frameRefused;
-        if (terminal) {
-            const std::uint64_t revision = instance->pendingScriptable.revision;
-            cancel_pending(*instance, binding, revision);
-            instance->view.outputStatus = status;
-        }
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-}
-
-/** @return True when this body carries the exact next counter its committed guard expects. */
-[[nodiscard]] bool staged_counter_matches(const ScriptableGuard* guard,
-                                          const PendingScriptableOverride& pending) noexcept {
-    bool nextCounter = false;
-    if (pending.kind == ScriptableOverrideKind::lifetime) {
-        nextCounter = true;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::squad
-               && pending.generation <= squad::kMaximumGeneration) {
-        std::uint32_t next = 0;
-        nextCounter = squad::next_generation(guard->squad, next) && next == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::combatantChannel) {
-        auth::Type2ChannelState candidate = guard->type2;
-        std::uint32_t revision = 0;
-        nextCounter =
-            auth::next_type2_revision(candidate, revision) && revision == pending.generation;
-        candidate.revision = revision;
-        nextCounter =
-            nextCounter
-            && auth::set_type2_channel(candidate, pending.channelHash, pending.channelValue);
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::combatantBinding) {
-        auth::Type2ChannelState candidate = guard->type2;
-        std::uint32_t revision = 0;
-        nextCounter =
-            auth::next_type2_revision(candidate, revision) && revision == pending.generation;
-        candidate.revision = revision;
-        candidate.actorBinding = auth::Type2ActorBinding::squadMember;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::object) {
-        std::int32_t next = 0;
-        nextCounter = auth::next_type4_generation(guard->type4, next)
-                      && static_cast<std::uint64_t>(next) == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::sequence) {
-        std::uint8_t next = 0;
-        nextCounter = auth::next_type5_revision(guard->type5, next) && next == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::cinematic) {
-        std::uint32_t next = 0;
-        nextCounter = auth::next_type6_generation(guard->type6, next) && next == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::performance) {
-        std::int32_t next = 0;
-        nextCounter = auth::next_type42_generation(guard->type42, next)
-                      && static_cast<std::uint64_t>(next) == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::type23) {
-        std::int16_t next = 0;
-        nextCounter = auth::next_type23_sequence(guard->type23, pending.channel, next)
-                      && next == pending.sequence;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::type31) {
-        std::uint64_t next = 0;
-        nextCounter =
-            auth::next_type31_generation(guard->type31, next) && next == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::objectiveReset) {
-        std::int32_t next = 0;
-        nextCounter = auth::next_type3_generation(guard->type3, next)
-                      && static_cast<std::uint64_t>(next) == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::task) {
-        std::int32_t next = 0;
-        nextCounter = auth::next_type38_generation(guard->type38, next)
-                      && static_cast<std::uint64_t>(next) == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::authoredScene) {
-        std::uint32_t next = 0;
-        nextCounter = next_authored_scene_generation(guard->authoredSceneGeneration, next)
-                      && next == pending.generation;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::dialogue) {
-        std::int32_t next = 0;
-        nextCounter = auth::next_type53_sequence(guard->type53, pending.dialogueCue, next)
-                      && next == pending.dialogueSequence;
-    } else if (guard != nullptr && pending.kind == ScriptableOverrideKind::sdkAuth
-               && pending.sdkCompiled) {
-        nextCounter = true;
-    }
-    return nextCounter;
-}
-
-/** Advances one committed guard to the body that has just reached transport. */
-void advance_staged_guard(ScriptableGuard* guard,
-                          const PendingScriptableOverride& pending) noexcept {
-    if (guard == nullptr) {
-        return;
-    }
-    if (pending.kind == ScriptableOverrideKind::squad) {
-        guard->squad.last = static_cast<std::uint32_t>(pending.generation);
-        guard->squad.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::combatantChannel) {
-        guard->type2.revision = static_cast<std::uint32_t>(pending.generation);
-        static_cast<void>(
-            auth::set_type2_channel(guard->type2, pending.channelHash, pending.channelValue));
-    } else if (pending.kind == ScriptableOverrideKind::combatantBinding) {
-        guard->type2.revision = static_cast<std::uint32_t>(pending.generation);
-        guard->type2.actorBinding = auth::Type2ActorBinding::squadMember;
-    } else if (pending.kind == ScriptableOverrideKind::object) {
-        guard->type4.last = static_cast<std::int32_t>(pending.generation);
-        guard->type4.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::sequence) {
-        guard->type5.last = static_cast<std::uint8_t>(pending.generation);
-        guard->type5.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::cinematic) {
-        guard->type6.last = static_cast<std::uint32_t>(pending.generation);
-        guard->type6.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::performance) {
-        guard->type42.last = static_cast<std::int32_t>(pending.generation);
-        guard->type42.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::type23) {
-        guard->type23.last[static_cast<std::size_t>(pending.channel)] = pending.sequence;
-    } else if (pending.kind == ScriptableOverrideKind::type31) {
-        guard->type31.last = pending.generation;
-        guard->type31.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::objectiveReset) {
-        guard->type3.last = static_cast<std::int32_t>(pending.generation);
-        guard->type3.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::task) {
-        guard->type38.last = static_cast<std::int32_t>(pending.generation);
-        guard->type38.hasLast = true;
-    } else if (pending.kind == ScriptableOverrideKind::authoredScene) {
-        guard->authoredSceneGeneration = static_cast<std::uint32_t>(pending.generation);
-    } else if (pending.kind == ScriptableOverrideKind::dialogue) {
-    }
-}
-
-/** Records that one pending body reached the transport, so its retained estate can advance. */
-void note_scriptable_transport_staged(const state::activity::SessionBinding& binding,
-                                      std::uint64_t sourceGeneration,
-                                      const PendingScriptableOverride& pending) noexcept {
-    if (pending.revision == 0
-        || (pending.expectedActivityClientGeneration != 0
-            && pending.expectedActivityClientGeneration != sourceGeneration)) {
-        return;
-    }
-    AcquireSRWLockExclusive(&g_lock);
-    Instance* const instance = find_instance(binding);
-    ScriptableGuard* guard = instance != nullptr ? find_guard(*instance, pending.target) : nullptr;
-    const bool nextCounter = staged_counter_matches(guard, pending);
-    if (instance != nullptr && nextCounter && instance->view.outputPending
-        && instance->view.outputKind == OutputKind::scriptableOverride
-        && same_pending(instance->pendingScriptable, pending)) {
-        const bool retained = retain_scriptable_auth(*instance, pending, sourceGeneration);
-        if (!retained) {
-            ++g_refusedControls;
-            ReleaseSRWLockExclusive(&g_lock);
-            return;
-        }
-        advance_staged_guard(guard, pending);
-        if (pending.kind == ScriptableOverrideKind::lifetime) {
-            // Latch the state so every later msg 5 keeps reporting it.
-            instance->view.lifetimeState = pending.lifetimeState;
-        }
-        instance->view.lastOutputAttemptTick = GetTickCount64();
-        Event event{};
-        event.binding = binding;
-        event.tick = instance->view.lastOutputAttemptTick;
-        event.kind = EventKind::scriptableOverrideTransportStaged;
-        event.sourceGeneration = sourceGeneration;
-        // The tail rode out on this same body, so it stages with the head or not at all.
-        for (std::size_t index = 0; index < instance->pendingScriptableTailCount; ++index) {
-            const PendingScriptableOverride& queued = instance->pendingScriptableTail[index];
-            ScriptableGuard* const queuedGuard = find_guard(*instance, queued.target);
-            if (!staged_counter_matches(queuedGuard, queued)
-                || !retain_scriptable_auth(*instance, queued, sourceGeneration)) {
-                ++g_refusedControls;
-                continue;
-            }
-            advance_staged_guard(queuedGuard, queued);
-            event.scriptableRevision = queued.revision;
-            append_event(event);
-        }
-        instance->pendingScriptableTail.fill({});
-        instance->pendingScriptableTailCount = 0;
-        instance->view.scriptableTransportRevision = instance->view.scriptableRevision;
-        instance->view.lastOutputSourceGeneration = sourceGeneration;
-        ++instance->view.outputAttempts;
-        instance->view.outputStatus = OutputStatus::transportStaged;
-        instance->view.outputPending = false;
-        instance->view.outputKind = OutputKind::none;
-        instance->pendingScriptable = {};
-        event.scriptableRevision = pending.revision;
-        append_event(event);
-        instance->view.lastEventSequence = g_sequence;
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-}
-
-/** Copies the pending overrides that have no output yet. @return How many were written. */
-std::size_t pending_scriptable_tail(const state::activity::SessionBinding& binding,
-                                    std::span<PendingScriptableOverride> output) noexcept {
-    AcquireSRWLockShared(&g_lock);
-    const Instance* const instance = find_instance(binding);
-    std::size_t written = 0;
-    if (instance != nullptr && instance->view.active && instance->view.outputPending
-        && instance->view.outputKind == OutputKind::scriptableOverride) {
-        const std::size_t count = (std::min)(instance->pendingScriptableTailCount, output.size());
-        for (; written < count; ++written) {
-            output[written] = instance->pendingScriptableTail[written];
-        }
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return written;
-}
-
-/** @return True when any instance still owes a Host output. */
-bool any_output_pending() noexcept {
-    AcquireSRWLockShared(&g_lock);
-    bool pending = false;
-    for (const Instance& instance : g_instances) {
-        pending =
-            pending
-            || (instance.occupied && instance.view.active
-                && (instance.view.outputPending || has_queued_control(instance.view.binding)));
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return pending;
-}
-
-/** Copies the retained Auth estate for one exact ActivityClient generation. */
-bool scriptable_auth_estate(const state::activity::SessionBinding& binding,
-                            std::uint64_t activityClientGeneration,
-                            std::vector<PendingScriptableOverride>& output) noexcept {
-    output.clear();
-    if (activityClientGeneration == 0) {
-        return false;
-    }
-    AcquireSRWLockShared(&g_lock);
-    const Instance* instance = nullptr;
-    for (const Instance& candidate : g_instances) {
-        if (candidate.occupied && same_binding(candidate.view.binding, binding)) {
-            instance = &candidate;
-            break;
-        }
-    }
-    bool copied = true;
-    if (instance != nullptr && instance->view.active) {
-        try {
-            output.reserve(instance->scriptableAuthEstate.size());
-            for (const PendingScriptableOverride& retained : instance->scriptableAuthEstate) {
-                // The ActivityClient generation is a transport revision that advances on ordinary
-                // region advertisements, and the SessionBinding already owns estate lifetime.
-                // Filtering retained mission state by it erases every non-squad Auth lane.
-                output.push_back(retained);
-            }
-        } catch (const std::bad_alloc&) {
-            output.clear();
-            copied = false;
-        }
-    }
-    ReleaseSRWLockShared(&g_lock);
-    return copied;
-}
-
 } // namespace sunrise::server::activity::host

+ 1 - 3
Sunrise/src/server/activity/host_runtime_scriptable_requests.cpp

@@ -348,7 +348,6 @@ bool request_squad_override(
     const ScriptableOutputReservation* reservation,
     std::array<std::int8_t, 4> authoredProfile,
     state::gameplay::squad_entity_retirement::Eligibility squadRetirement) noexcept {
-    const auto rawMode = static_cast<std::uint8_t>(mode);
     if (squadRetirement.enabled
         && (squadRetirement.squad.key != target.registryKey
             || squadRetirement.squad.index != target.slotIndex
@@ -365,8 +364,7 @@ bool request_squad_override(
         || requestedCounts.size() < squad::kMinimumRequestedCountLength
         || requestedCounts.size() > squad::kMaximumRequestedCountLength
         || expectedActivityClientGeneration == 0
-        || (rawMode != static_cast<std::uint8_t>(squad::Mode::mode0)
-            && rawMode != static_cast<std::uint8_t>(squad::Mode::mode2))
+        || !squad::valid_mode(mode)
         || !std::ranges::all_of(requestedCounts, [](std::int32_t count) { return count >= 0; })) {
         return false;
     }

+ 20 - 33
Sunrise/src/server/activity/host_runtime_sense.cpp

@@ -13,6 +13,7 @@
 #include <span>
 
 #include "../../core/logging/log.h"
+#include "../../middleware/bap/activity_message/sense_observation_packet.h"
 #include "../../state/activity/runtime.h"
 #include "../../state/activity_sdk/format.h"
 #include "host_runtime_internal.h"
@@ -37,40 +38,24 @@ packet_has_sense_key(const middleware::bap::activity_message::sense_update::Deco
                      const SenseObservationKey& key,
                      std::size_t first) noexcept {
     for (std::size_t index = first; index < packet.objectCount; ++index) {
-        if (same_sense_key(key, packet.objects[index])) {
+        if (packet.objects[index].status == middleware::bap::activity_message::sense_update::ObjectStatus::decoded
+            && packet.objects[index].hasGeneration && same_sense_key(key, packet.objects[index])) {
             return true;
         }
     }
     return false;
 }
 
-/** @return True when every retained object and value came from one complete decode. */
-[[nodiscard]] bool complete_sense_observation_input(const SenseInput& input) noexcept {
+/** @return True when the envelope closed and each decoded object's storage is bounded. */
+[[nodiscard]] bool valid_sense_observation_input(const SenseInput& input) noexcept {
     namespace sense = middleware::bap::activity_message::sense_update;
-    const sense::DecodedPacket& packet = input.decoded;
-    if (input.sourceGeneration == 0 || input.clientMessageSequence == 0
-        || input.verdict != state::activity::receipts::Verdict::framed
-        || input.decodeStatus != sense::DecodeStatus::complete
-        || packet.status != sense::DecodeStatus::complete || packet.objectsTruncated
-        || packet.valuesTruncated || packet.groupsSkipped != 0 || input.groupsSkipped != 0
-        || packet.objectCount > packet.objects.size() || packet.valueCount > packet.values.size()
-        || packet.objectsDecoded != packet.objectCount || packet.objectsSeen != packet.objectCount
-        || input.groupsSeen != packet.groupsSeen || input.groupsDecoded != packet.groupsDecoded
-        || input.objectsSeen != packet.objectsSeen
-        || input.objectsDecoded != packet.objectsDecoded) {
-        return false;
-    }
-    std::size_t expectedValue = 0;
-    for (std::size_t index = 0; index < packet.objectCount; ++index) {
-        const sense::DecodedObject& object = packet.objects[index];
-        if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration
-            || object.firstValue != expectedValue
-            || object.valueCount > packet.valueCount - expectedValue) {
-            return false;
-        }
-        expectedValue += object.valueCount;
-    }
-    return expectedValue == packet.valueCount;
+    const auto& packet = input.decoded;
+    return input.sourceGeneration != 0 && input.clientMessageSequence != 0
+        && input.verdict == state::activity::receipts::Verdict::framed
+        && input.decodeStatus == packet.status && sense::observation_packet(packet)
+        && input.groupsSeen == packet.groupsSeen && input.groupsDecoded == packet.groupsDecoded
+        && input.groupsSkipped == packet.groupsSkipped && input.objectsSeen == packet.objectsSeen
+        && input.objectsDecoded == packet.objectsDecoded;
 }
 
 /** Mixes one fixed-width value into the local scene change guard. */
@@ -251,7 +236,7 @@ void trace_scene_sense(Instance& instance, const SenseInput& input) noexcept {
             break;
         }
     }
-    if (!complete_sense_observation_input(input)) {
+    if (!valid_sense_observation_input(input)) {
         if (!trace.incompleteReported && (hasScene || packet.objectsTruncated)) {
             trace.incompleteReported = true;
             report_scene_sense(
@@ -275,8 +260,8 @@ void trace_scene_sense(Instance& instance, const SenseInput& input) noexcept {
     }
     for (std::size_t index = 0; index < packet.objectCount; ++index) {
         const sense::DecodedObject& object = packet.objects[index];
-        if (object.slotType
-                != static_cast<std::uint8_t>(state::activity_sdk::format::kAuthoredSceneSlotType)
+        if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration || object.slotType
+            != static_cast<std::uint8_t>(state::activity_sdk::format::kAuthoredSceneSlotType)
             || packet_has_sense_key(packet,
                                     {object.registryKey,
                                      object.objectTag,
@@ -353,11 +338,11 @@ void trace_scene_sense(Instance& instance, const SenseInput& input) noexcept {
     }
 }
 
-/** Replaces only keys present in one complete packet and keeps every omitted key. */
+/** Replaces only fully decoded keys in an accepted packet; omitted or unsupported keys stay retained. */
 [[nodiscard]] bool
 retain_sense_observations(Instance& instance, const SenseInput& input, std::uint64_t now) noexcept {
     namespace sense = middleware::bap::activity_message::sense_update;
-    if (!complete_sense_observation_input(input)) {
+    if (!valid_sense_observation_input(input)) {
         return false;
     }
     const sense::DecodedPacket& packet = input.decoded;
@@ -366,6 +351,7 @@ retain_sense_observations(Instance& instance, const SenseInput& input, std::uint
     next.sourceGeneration = input.sourceGeneration;
     for (std::size_t index = 0; index < packet.objectCount; ++index) {
         const sense::DecodedObject& object = packet.objects[index];
+        if (object.status != sense::ObjectStatus::decoded || !object.hasGeneration) continue;
         const SenseObservationKey key{object.registryKey,
                                       object.objectTag,
                                       object.senseSchema,
@@ -520,6 +506,7 @@ void apply_sense(const SenseInput& input, std::uint64_t now) noexcept {
         event.slotSenseSchema = input.decoded.objects.front().senseSchema;
     }
     event.senseDecodeStatus = input.decodeStatus;
+    event.senseSnapshotRetained = valid_sense_observation_input(input);
     event.hasFirstObject = input.hasFirstObject;
     event.stateRevision = instance->view.stateRevision;
     event.sourceGeneration = input.sourceGeneration;
@@ -527,7 +514,7 @@ void apply_sense(const SenseInput& input, std::uint64_t now) noexcept {
     event.lifetimeState = instance->view.lifetimeState;
     event.verdict = input.verdict;
     append_event(event);
-    append_mission_input(event, complete_sense_observation_input(input) ? &input.decoded : nullptr);
+    append_mission_input(event, event.senseSnapshotRetained ? &input.decoded : nullptr);
     instance->view.lastEventSequence = g_sequence;
 }
 

+ 80 - 22
Sunrise/src/server/activity/mission/mission_script_actor_path_sense.h

@@ -1,37 +1,95 @@
 #pragma once
+
 #include <cstdint>
 #include <span>
+
 #include "../../../middleware/bap/activity_message/sense_update.h"
+
 namespace sunrise::server::activity::mission {
-struct ActorPathLevel {
-    std::int32_t generation{}, revision{}, state{};
-    std::int32_t deliveryRevision{}, deliveryState{};
-    std::uint8_t present{};
+
+/** Nested combatant program record: field 0 is the program state, field 1 its revision. */
+inline constexpr std::uint32_t kCombatantProgramSchema = 0x80807F6EU;
+/** Root ordinals of the type-2 Sense body. */
+inline constexpr std::uint16_t kActorGenerationOrdinal = 0;
+inline constexpr std::uint16_t kActorDeliveryRevisionOrdinal = 5;
+inline constexpr std::uint16_t kActorDeliveryStateOrdinal = 6;
+inline constexpr std::uint16_t kActorDeadOrdinal = 10;
+/** Which fields a level has seen. */
+inline constexpr std::uint8_t kActorSeenGeneration = 0x01;
+inline constexpr std::uint8_t kActorSeenRevision = 0x02;
+inline constexpr std::uint8_t kActorSeenState = 0x04;
+inline constexpr std::uint8_t kActorSeenDead = 0x08;
+inline constexpr std::uint8_t kActorSeenDeliveryRevision = 0x10;
+inline constexpr std::uint8_t kActorSeenDeliveryState = 0x20;
+/** A level reports once the generation, revision, state and death flag have all arrived. */
+inline constexpr std::uint8_t kActorSeenCore =
+    kActorSeenGeneration | kActorSeenRevision | kActorSeenState | kActorSeenDead;
+inline constexpr std::uint8_t kActorSeenDelivery =
+    kActorSeenDeliveryRevision | kActorSeenDeliveryState;
+
+/** Last movement and delivery levels seen for one named actor. */
+struct ActorPathLevel final {
+    std::int32_t generation{};
+    std::int32_t revision{};
+    std::int32_t state{};
+    std::int32_t deliveryRevision{};
+    std::int32_t deliveryState{};
+    std::uint8_t seen{};
     bool dead{};
     bool operator==(const ActorPathLevel&) const = default;
 };
-[[nodiscard]] inline bool update_actor_path_level(ActorPathLevel& level,
+
+/**
+ * Merges one decoded type-2 body into the retained level.
+ * @param root Schema row of the body's root.
+ * @return True when the level changed and its core fields are all known.
+ */
+[[nodiscard]] inline bool update_actor_path_level(
+    ActorPathLevel& level,
     std::span<const middleware::bap::activity_message::sense_update::DecodedValue> values,
     std::uint32_t root) noexcept {
-    auto next = level;
-    for (const auto& v : values) {
-        if (!v.present) continue;
-        // Movement is Auth .6 / Sense .3. Delivery has its own revision and state.
-        if (v.schemaRow == 0x80807F6EU) {
-            if (v.fieldOrdinal == 0) { next.state = static_cast<std::int32_t>(v.signedValue); next.present |= 4; }
-            if (v.fieldOrdinal == 1) { next.revision = static_cast<std::int32_t>(v.signedValue); next.present |= 2; }
+    ActorPathLevel next = level;
+    for (const auto& value : values) {
+        if (!value.present) {
             continue;
         }
-        if (v.schemaRow != root) continue;
-        switch (v.fieldOrdinal) {
-        case 0: next.generation = static_cast<std::int32_t>(v.signedValue); next.present |= 1; break;
-        case 5: next.deliveryRevision = static_cast<std::int32_t>(v.signedValue); next.present |= 16; break;
-        case 6: next.deliveryState = static_cast<std::int32_t>(v.signedValue); next.present |= 32; break;
-        case 10: next.dead = v.unsignedValue != 0; next.present |= 8; break;
-        default: break;
+        if (value.schemaRow == kCombatantProgramSchema) {
+            if (value.fieldOrdinal == 0) {
+                next.state = static_cast<std::int32_t>(value.signedValue);
+                next.seen |= kActorSeenState;
+            } else if (value.fieldOrdinal == 1) {
+                next.revision = static_cast<std::int32_t>(value.signedValue);
+                next.seen |= kActorSeenRevision;
+            }
+            continue;
+        }
+        if (value.schemaRow != root) {
+            continue;
+        }
+        switch (value.fieldOrdinal) {
+        case kActorGenerationOrdinal:
+            next.generation = static_cast<std::int32_t>(value.signedValue);
+            next.seen |= kActorSeenGeneration;
+            break;
+        case kActorDeliveryRevisionOrdinal:
+            next.deliveryRevision = static_cast<std::int32_t>(value.signedValue);
+            next.seen |= kActorSeenDeliveryRevision;
+            break;
+        case kActorDeliveryStateOrdinal:
+            next.deliveryState = static_cast<std::int32_t>(value.signedValue);
+            next.seen |= kActorSeenDeliveryState;
+            break;
+        case kActorDeadOrdinal:
+            next.dead = value.unsignedValue != 0;
+            next.seen |= kActorSeenDead;
+            break;
+        default:
+            break;
         }
     }
-    const bool changed = next != level; level = next;
-    return changed && (next.present & 15) == 15;
-}
+    const bool changed = next != level;
+    level = next;
+    return changed && (next.seen & kActorSeenCore) == kActorSeenCore;
 }
+
+} // namespace sunrise::server::activity::mission

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

@@ -164,9 +164,8 @@ bool arena_initialize(Arena& arena) noexcept {
     arena.highWater = 0;
     arena.initialized = false;
     if (arena.bytes == nullptr) {
-        // Call the allocation function directly: Clang's optimized Windows build folds the
-        // nothrow byte-array new-expression into null, preventing every mission from opening.
-        // The matching byte-array deleter still releases this storage through operator delete[].
+        // The allocation function is called directly. Clang folds the nothrow array
+        // new-expression to null in the optimized build; delete[] still matches this storage.
         auto* const block =
             static_cast<std::byte*>(::operator new[](kArenaByteCapacity, std::nothrow));
         if (block == nullptr) {

+ 14 - 6
Sunrise/src/server/activity/mission/mission_script_event_batch.h

@@ -1,17 +1,25 @@
 #pragma once
+
 #include <cstddef>
 
 namespace sunrise::server::activity::mission {
-// Stop at the first asynchronous output. Callbacks after it must observe its
-// committed result, but callbacks producing no output need not spend a tick each.
-template<class Ready, class Dispatch>
+
+/** Derived events one instance may dispatch per service slice. */
+inline constexpr std::size_t kScriptEventBatchLimit = 64;
+
+/**
+ * Dispatches queued events while `ready` allows it, up to the batch limit. The ready test stops
+ * at the first asynchronous output, because the callbacks after it must see its committed result.
+ * @return Events dispatched.
+ */
+template <class Ready, class Dispatch>
 std::size_t drain_script_event_batch(Ready ready, Dispatch dispatch) {
-    constexpr std::size_t limit = 64;
     std::size_t count = 0;
-    while (count < limit && ready()) {
+    while (count < kScriptEventBatchLimit && ready()) {
         dispatch();
         ++count;
     }
     return count;
 }
-}
+
+} // namespace sunrise::server::activity::mission

+ 47 - 14
Sunrise/src/server/activity/mission/mission_script_ghost_sense.h

@@ -1,35 +1,68 @@
 #pragma once
+
 #include <cmath>
 #include <cstdint>
 #include <span>
+
 #include "../../../middleware/bap/activity_message/sense_update.h"
+
 namespace sunrise::server::activity::mission {
+
+/** Type-65 Sense root ordinals: active, elapsed over required duration, accepted generation. */
+inline constexpr std::uint16_t kGhostActiveOrdinal = 0;
+inline constexpr std::uint16_t kGhostProgressOrdinal = 1;
+inline constexpr std::uint16_t kGhostGenerationOrdinal = 2;
+inline constexpr std::uint8_t kGhostSeenActive = 0x01;
+inline constexpr std::uint8_t kGhostSeenProgress = 0x02;
+inline constexpr std::uint8_t kGhostSeenGeneration = 0x04;
+inline constexpr std::uint8_t kGhostSeenAll =
+    kGhostSeenActive | kGhostSeenProgress | kGhostSeenGeneration;
+
+/** Last Ghost-link level seen for one slot. */
 struct GhostLevel final {
     std::int32_t generation{};
     float progress{};
-    std::uint8_t present{};
+    std::uint8_t seen{};
     bool active{};
+    bool operator==(const GhostLevel&) const = default;
 };
-// E4A590 publishes active, elapsed/required duration, and the accepted Auth generation.
+
+/**
+ * Merges one decoded type-65 body into the retained level.
+ * @return True when the level changed and all three fields are known.
+ */
 [[nodiscard]] inline bool update_ghost_level(
     GhostLevel& level,
     std::span<const middleware::bap::activity_message::sense_update::DecodedValue> values,
     std::uint32_t root) noexcept {
     GhostLevel next = level;
-    for (const auto& v : values) {
-        if (!v.present || v.schemaRow != root) continue;
-        switch (v.fieldOrdinal) {
-        case 0: next.active = v.unsignedValue != 0; next.present |= 1; break;
-        case 1:
-            if (!std::isfinite(v.realValue) || v.realValue < 0) return false;
-            next.progress = v.realValue; next.present |= 2; break;
-        case 2: next.generation = static_cast<std::int32_t>(v.signedValue); next.present |= 4; break;
-        default: break;
+    for (const auto& value : values) {
+        if (!value.present || value.schemaRow != root) {
+            continue;
+        }
+        switch (value.fieldOrdinal) {
+        case kGhostActiveOrdinal:
+            next.active = value.unsignedValue != 0;
+            next.seen |= kGhostSeenActive;
+            break;
+        case kGhostProgressOrdinal:
+            if (!std::isfinite(value.realValue) || value.realValue < 0) {
+                return false;
+            }
+            next.progress = value.realValue;
+            next.seen |= kGhostSeenProgress;
+            break;
+        case kGhostGenerationOrdinal:
+            next.generation = static_cast<std::int32_t>(value.signedValue);
+            next.seen |= kGhostSeenGeneration;
+            break;
+        default:
+            break;
         }
     }
-    const bool changed = next.present != level.present || next.active != level.active
-                         || next.progress != level.progress || next.generation != level.generation;
+    const bool changed = !(next == level);
     level = next;
-    return changed && next.present == 7;
+    return changed && next.seen == kGhostSeenAll;
 }
+
 } // namespace sunrise::server::activity::mission

+ 33 - 56
Sunrise/src/server/activity/mission/mission_script_lua_context_api.cpp

@@ -1,11 +1,11 @@
-#include <cstddef>
 #include <charconv>
+#include <cstddef>
 #include <cstdint>
 #include <limits>
 #include <string_view>
 
+#include "../../../state/activity/membership/definition.h"
 #include "mission_script_lua_internal.h"
-#include "../../../client/hooks/ember_movies/ember_movies.h"
 #include "mission_script_lua_names.h"
 #include "mission_script_lua_peer_internal.h"
 #include "mission_script_lua_resolve.h"
@@ -25,29 +25,39 @@ namespace {
     return 1;
 }
 
+/**
+ * Arms a native hard wipe at an authored spawn set, or releases one with its request key.
+ * The Lua caller passes `release_request` as the decimal string of the original key.
+ */
 [[nodiscard]] int context_restart_checkpoint(lua_State* state) {
-    static_cast<void>(luaL_checkudata(state,1,kContextMetatable));
-    static constexpr std::array<std::string_view,3> fields{"region","spawn_set_hash","release_request"};
-    refuse_unknown_arguments(state,fields);
-    const auto region=optional_integer_argument(state,"region",-1);
-    const auto hash=optional_integer_argument(state,"spawn_set_hash",0);
-    if (region<0 || region>511 || hash<=0 || hash>=UINT32_MAX)
-        return luaL_error(state,"checkpoint requires an authored region and spawn-set hash");
-    std::uint64_t release{};
-    lua_getfield(state,2,"release_request");
-    if (!lua_isnil(state,-1)) {
-        std::size_t length{};
-        const char* value=luaL_checklstring(state,-1,&length);
-        const auto parsed=std::from_chars(value,value+length,release);
-        if (parsed.ec!=std::errc{} || parsed.ptr!=value+length || release==0)
-            return luaL_error(state,"checkpoint release requires the original RequestKey.value");
+    static_cast<void>(luaL_checkudata(state, 1, kContextMetatable));
+    static constexpr std::array<std::string_view, 3> kDeclared{
+        "region", "spawn_set_hash", "release_request"};
+    refuse_unknown_arguments(state, kDeclared);
+    const lua_Integer region = optional_integer_argument(state, "region", -1);
+    const lua_Integer hash = optional_integer_argument(state, "spawn_set_hash", 0);
+    if (region < 0 || region > ::sunrise::state::activity::membership::kMaximumSliceSetIndex
+        || hash <= 0
+        || hash >= (std::numeric_limits<std::uint32_t>::max)()) {
+        return luaL_error(state, "checkpoint requires an authored region and spawn-set hash");
+    }
+    std::uint64_t release = 0;
+    lua_getfield(state, 2, "release_request");
+    if (!lua_isnil(state, -1)) {
+        std::size_t length = 0;
+        const char* const value = luaL_checklstring(state, -1, &length);
+        const auto parsed = std::from_chars(value, value + length, release);
+        if (parsed.ec != std::errc{} || parsed.ptr != value + length || release == 0) {
+            return luaL_error(state, "checkpoint release requires the original RequestKey.value");
+        }
     }
-    lua_pop(state,1);
-    Intent intent{};intent.kind=IntentKind::restartCheckpoint;
-    intent.checkpointReleaseRequest=static_cast<std::uint64_t>(release);
-    intent.effectiveRegion=static_cast<std::int32_t>(region);
-    intent.checkpointSpawnHash=static_cast<std::uint32_t>(hash);
-    return queue_intent(state,active_frame(state),intent);
+    lua_pop(state, 1);
+    Intent intent{};
+    intent.kind = IntentKind::restartCheckpoint;
+    intent.checkpointReleaseRequest = release;
+    intent.effectiveRegion = static_cast<std::int32_t>(region);
+    intent.checkpointSpawnHash = static_cast<std::uint32_t>(hash);
+    return queue_intent(state, active_frame(state), intent);
 }
 
 [[nodiscard]] int context_scene(lua_State* state) {
@@ -193,35 +203,6 @@ resolve_message_name(lua_State* state, std::string_view name, ActivityMessageDef
     return queue_intent(state, frame, intent);
 }
 
-/** Ember's exact packaged movie pair; work is committed as an ordinary mission intent. */
-int context_play_prerendered_movie(lua_State* state) {
-    static_cast<void>(luaL_checkudata(state,1,kContextMetatable));
-    const auto* impl=impl_from_state(state);
-    if (impl->identity.publicTarget || impl->identity.definitionHash!=0x38F926B2U)
-        return luaL_error(state,"pre-rendered movie bridge is scoped to mission_ember");
-    static constexpr std::array<std::string_view,2> fields{"index","stop"};
-    refuse_unknown_arguments(state,fields);
-    const auto index=optional_integer_argument(state,"index",0);
-    if (index<1 || index>2) return luaL_error(state,"unknown Ember bookend index");
-    lua_getfield(state,2,"stop"); const bool stop=lua_toboolean(state,-1); lua_pop(state,1);
-    Intent intent{};intent.kind=IntentKind::playPrerenderedMovie;
-    intent.firstRow=static_cast<std::uint32_t>(index);intent.active=!stop;
-    return queue_intent(state,active_frame(state),intent);
-}
-int context_prerendered_movie_status(lua_State* state) {
-    static_cast<void>(luaL_checkudata(state,1,kContextMetatable));
-    const auto index=luaL_checkinteger(state,2);
-    auto& frame=active_frame(state);
-    const auto* impl=impl_from_state(state);
-    if (impl->identity.definitionHash!=0x38F926B2U || impl->identity.publicTarget
-        || !frame.event || index<1 || index>2) { lua_pushliteral(state,"absent");return 1; }
-    namespace movies=client::hooks::ember_movies;
-    const auto result=movies::status({frame.event->binding.sessionId,frame.event->sourceGeneration},
-        static_cast<unsigned>(index));
-    constexpr std::array<const char*,6> names{"absent","queued","preparing","playing","complete","failed"};
-    lua_pushstring(state,names[static_cast<unsigned>(result)]);return 1;
-}
-
 /** Lua index for the mission context: its collections, phase, variables and timers. */
 [[nodiscard]] int context_index(lua_State* state) {
     static_cast<void>(luaL_checkudata(state, 1, kContextMetatable));
@@ -251,10 +232,6 @@ int context_prerendered_movie_status(lua_State* state) {
         lua_pushcfunction(state, &context_scene);
     } else if (key == "slot") {
         lua_pushcfunction(state, &context_slot);
-    } else if (key == "play_prerendered_movie") {
-        lua_pushcfunction(state, &context_play_prerendered_movie);
-    } else if (key == "prerendered_movie_status") {
-        lua_pushcfunction(state, &context_prerendered_movie_status);
     } else if (key == "select_state") {
         lua_pushcfunction(state, &context_select_state);
     } else if (key == "restart_checkpoint") {

+ 0 - 2
Sunrise/src/server/activity/mission/mission_script_lua_event_delivery.cpp

@@ -62,8 +62,6 @@ push_incident_revision_member(lua_State* state, const host::Event& event, std::s
         return "slot.advance_task";
     case ActionKind::playDialogueCue:
         return "slot.play_dialogue_cue";
-    case ActionKind::playPrerenderedMovie:
-        return "mission.play_prerendered_movie";
     case ActionKind::selectMissionState:
         return "mission.select_state";
     }

+ 7 - 5
Sunrise/src/server/activity/mission/mission_script_lua_event_derived.cpp

@@ -64,11 +64,13 @@ push_squad_state_member(lua_State* state, const host::Event& event, std::string_
     } else if (key == "objective_revision") {
         lua_pushinteger(state, event.squadObjectiveRevision);
     } else if (key == "task_costs") {
-        lua_createtable(state, 24, 0);
-        for (unsigned i=0; i<24; ++i) {
-            if (!(event.squadObjectiveCostMask & (1U << i))) continue;
-            lua_pushnumber(state, event.squadObjectiveCosts[i]);
-            lua_rawseti(state, -2, i+1);
+        lua_createtable(state, static_cast<int>(host::kSquadObjectiveGroupCount), 0);
+        for (unsigned group = 0; group < host::kSquadObjectiveGroupCount; ++group) {
+            if ((event.squadObjectiveCostMask & (1U << group)) == 0) {
+                continue;
+            }
+            lua_pushnumber(state, event.squadObjectiveCosts[group]);
+            lua_rawseti(state, -2, static_cast<lua_Integer>(group) + 1);
         }
     } else if (key == "previous_alive_count") {
         lua_pushinteger(state, event.squadPreviousAliveCount);

+ 2 - 1
Sunrise/src/server/activity/mission/mission_script_lua_names.h

@@ -75,7 +75,8 @@ inline constexpr char kEntitySlotsRequestedEventMetatable[] =
 inline constexpr char kSessionJoinedEventMetatable[] = "sunrise.mission.event.session_joined";
 inline constexpr char kSessionLeftEventMetatable[] = "sunrise.mission.event.session_left";
 inline constexpr char kActorPathEventMetatable[] = "sunrise.mission.event.actor_path";
-inline constexpr char kObjectInteractionEventMetatable[] = "sunrise.mission.event.object_interaction";
+inline constexpr char kObjectInteractionEventMetatable[] =
+    "sunrise.mission.event.object_interaction";
 inline constexpr char kGhostLinkEventMetatable[] = "sunrise.mission.event.ghost_link";
 inline constexpr char kFireteamStateEventMetatable[] = "sunrise.mission.event.fireteam_state";
 inline constexpr char kPlayerTriggerEventMetatable[] = "sunrise.mission.event.player_trigger";

+ 464 - 275
Sunrise/src/server/activity/mission/mission_script_lua_slot_api.cpp

@@ -1,14 +1,3 @@
-#include "../../../middleware/bap/activity_message/music_section_auth.h"
-#include "../../../middleware/bap/activity_message/mission_effect_auth.h"
-#include "../../../middleware/bap/activity_message/darkness_zone_auth.h"
-#include "../../../middleware/bap/activity_message/squad_objective_auth.h"
-#include "../../../middleware/bap/activity_message/combatant_path_auth.h"
-#include "../../../middleware/bap/activity_message/combatant_action_auth.h"
-#include "../../../middleware/bap/activity_message/combatant_delivery_auth.h"
-#include "../../../middleware/bap/activity_message/combatant_retire_auth.h"
-#include "../../../middleware/bap/activity_message/ghost_link_auth.h"
-#include "../../../middleware/bap/activity_message/interactable_object_auth.h"
-#include "../../../middleware/bap/activity_message/scene_events_auth.h"
 #include <algorithm>
 #include <array>
 #include <bit>
@@ -21,7 +10,16 @@
 #include <string_view>
 
 #include "../../../middleware/bap/activity_message/auth_schema_catalog.h"
+#include "../../../middleware/bap/activity_message/combatant_auth.h"
+#include "../../../middleware/bap/activity_message/damage_monitor_auth.h"
+#include "../../../middleware/bap/activity_message/darkness_zone_auth.h"
+#include "../../../middleware/bap/activity_message/ghost_link_auth.h"
+#include "../../../middleware/bap/activity_message/interactable_object_auth.h"
+#include "../../../middleware/bap/activity_message/mission_effect_auth.h"
+#include "../../../middleware/bap/activity_message/music_section_auth.h"
+#include "../../../middleware/bap/activity_message/scene_events_auth.h"
 #include "../../../middleware/bap/activity_message/sensor_auth_update.h"
+#include "../../../middleware/bap/activity_message/squad_objective_auth.h"
 #include "../../../middleware/encoding/bit_writer.h"
 #include "../../../state/activity_sdk/format.h"
 #include "../../../state/activity_sdk/runtime.h"
@@ -33,6 +31,7 @@ namespace format = state::activity_sdk::format;
 namespace slot_transport = middleware::bap::activity_message::sensor_auth_update;
 namespace scriptable_auth = middleware::bap::activity_message::scriptable_auth;
 namespace auth_catalog = middleware::bap::activity_message::auth_schema_catalog;
+namespace auth_fields = middleware::bap::activity_message::auth_fields;
 
 namespace {
 /** @return True when one live Slot row is an exact type-23 device. */
@@ -44,6 +43,39 @@ namespace {
            && (definition.flags & format::kSlotSchemaJoinExact) != 0;
 }
 
+/** Positive 31-bit generations and revisions are the counters every typed Auth body accepts. */
+[[nodiscard]] bool valid_counter(lua_Integer value) noexcept {
+    return value > 0 && value <= auth_fields::kMaximumCounter;
+}
+
+/**
+ * Reads one optional slot-handle argument into a ClientRef.
+ * @param slotType Slot type the referenced slot must have.
+ * @param output Left unset when the argument is nil.
+ * @return False when the argument is present but stale or of another type.
+ */
+[[nodiscard]] bool optional_slot_reference(lua_State* state,
+                                           const char* name,
+                                           std::uint32_t slotType,
+                                           scriptable_auth::Type2LaneClientRef& output) {
+    lua_getfield(state, 2, name);
+    bool valid = true;
+    if (!lua_isnil(state, -1)) {
+        const auto* const handle =
+            static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
+        SlotDefinition definition{};
+        valid = current_slot(state, *handle, definition) && definition.slotType == slotType
+                && definition.slotIndex <= auth_fields::kMaximumClientRefIndex;
+        if (valid) {
+            output = {definition.registryKey,
+                      static_cast<std::int8_t>(slotType),
+                      static_cast<std::int16_t>(definition.slotIndex)};
+        }
+    }
+    lua_pop(state, 1);
+    return valid;
+}
+
 /** @return True when one live Slot row is an exact type-4 authored object. */
 [[nodiscard]] bool exact_object_slot(const SlotDefinition& definition) noexcept {
     return definition.slotType == format::kObjectSlotType
@@ -229,7 +261,9 @@ constexpr std::size_t kOccupancyAuthByteCount = 11;
 [[nodiscard]] int slot_set_directive(lua_State* state) {
     const auto* const handle =
         static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 4> kDeclared{"directive", "state", "navpoint", "audience"};
+    // Named arguments this call accepts. Any other key is refused.
+    static constexpr std::array<std::string_view, 4> kDeclared{
+        "directive", "state", "navpoint", "audience"};
     refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
     if (!current_slot(state, *handle, slot)) {
@@ -266,26 +300,18 @@ constexpr std::size_t kOccupancyAuthByteCount = 11;
                                          .elementIndex = resolved.elementIndex,
                                          .state = static_cast<std::int8_t>(directiveState),
                                          .visible = true};
-    lua_getfield(state, 2, "audience");
-    if (!lua_isnil(state, -1)) {
-        const auto* ref = static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
-        SlotDefinition filter{};
-        if (!current_slot(state, *ref, filter) || filter.slotType != 70 || filter.slotIndex < 0
-            || filter.slotIndex > INT16_MAX)
-            return luaL_error(state, "directive audience requires an authored type-70 engagement sensor");
-        preset.audience = {filter.registryKey, 70, static_cast<std::int16_t>(filter.slotIndex)};
+    if (!optional_slot_reference(state,
+                                 "audience",
+                                 scriptable_auth::kType70SlotType,
+                                 preset.audience)) {
+        return luaL_error(state, "directive audience requires an authored type-70 engagement sensor");
     }
-    lua_pop(state, 1);
-    lua_getfield(state, 2, "navpoint");
-    if (!lua_isnil(state, -1)) {
-        const auto* targetHandle = static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
-        SlotDefinition target{};
-        if (!current_slot(state, *targetHandle, target) || target.slotType != 47
-            || target.slotIndex < 0 || target.slotIndex > INT16_MAX)
-            return luaL_error(state, "directive navpoint requires a current authored type-47 slot");
-        preset.navpoint = {target.registryKey, 47, static_cast<std::int16_t>(target.slotIndex)};
+    if (!optional_slot_reference(state,
+                                 "navpoint",
+                                 scriptable_auth::kType47SlotType,
+                                 preset.navpoint)) {
+        return luaL_error(state, "directive navpoint requires a current authored type-47 slot");
     }
-    lua_pop(state, 1);
     std::array<std::byte, scriptable_auth::kType68ByteCount> body{};
     std::size_t written = 0;
     if (!scriptable_auth::encode_type68(preset, body, written) || written != body.size()) {
@@ -347,168 +373,239 @@ constexpr std::size_t kOccupancyAuthByteCount = 11;
         state, slot, scriptable_auth::kType70Schema, scriptable_auth::kType70BitCount, body);
 }
 
-/** Enables the native restriction; roster assembly supplies its matching bubble. */
+/** Enables the native darkness restriction; roster assembly supplies its matching bubble. */
 [[nodiscard]] int slot_set_darkness_zone(lua_State* state) {
-    namespace darkness=middleware::bap::activity_message::darkness_zone;
-    const auto* handle=static_cast<const SlotHandle*>(luaL_checkudata(state,1,kSlotMetatable));
-    static constexpr std::array<std::string_view,2> declared{"enabled","wipe_seconds"};
-    refuse_unknown_arguments(state,declared);
+    namespace darkness = middleware::bap::activity_message::darkness_zone;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 2> kDeclared{"enabled", "wipe_seconds"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
-    if (!current_slot(state,*handle,slot)||slot.slotType!=darkness::kSlotType
-        ||slot.componentClass!=darkness::kClass||slot.authSchema!=darkness::kSchema
-        ||(slot.flags&format::kSlotSchemaJoinExact)==0)
-        return luaL_error(state,"darkness zone requires the exact hard-wipe globals sensor");
-    std::array<std::byte,darkness::kBytes> body{};
-    const auto wipe=optional_integer_argument(state,"wipe_seconds",-1);
-    if (wipe < -1 || wipe>3
-        || !darkness::encode(optional_boolean_argument(state,"enabled",false),body,static_cast<int>(wipe)))
-        return luaL_error(state,"darkness zone encoder failed");
-    return queue_slot_auth(state,slot,darkness::kSchema,darkness::kBits,body);
+    if (!current_slot(state, *handle, slot) || slot.slotType != darkness::kSlotType
+        || slot.componentClass != darkness::kComponentClass || slot.authSchema != darkness::kSchema
+        || (slot.flags & format::kSlotSchemaJoinExact) == 0) {
+        return luaL_error(state, "darkness zone requires the exact hard-wipe globals sensor");
+    }
+    const lua_Integer wipe = optional_integer_argument(state, "wipe_seconds", darkness::kNoWipe);
+    std::array<std::byte, darkness::kBytes> body{};
+    if (wipe < darkness::kNoWipe || wipe > darkness::kMaximumWipeSeconds
+        || !darkness::encode(
+            optional_boolean_argument(state, "enabled", false), body, static_cast<int>(wipe))) {
+        return luaL_error(state, "darkness zone encoder failed");
+    }
+    return queue_slot_auth(state, slot, darkness::kSchema, darkness::kBits, body);
 }
 
+/** Volumes one filter may test; leaves room for the players, target and inside predicates. */
+constexpr std::size_t kMaximumFilterVolumes = 5;
+/** Type-34 predicate modes: 0 tests the flag or reference as given, 1 tests inside a volume. */
+constexpr std::int8_t kFilterModeDirect = 0;
+constexpr std::int8_t kFilterModeInside = 1;
+
 /** Native typed object filters: players, one object, and volume intersection. */
 [[nodiscard]] int slot_set_object_filter(lua_State* state) {
-    namespace auth = middleware::bap::activity_message::scriptable_auth;
-    const auto* handle=static_cast<const SlotHandle*>(luaL_checkudata(state,1,kSlotMetatable));
-    static constexpr std::array<std::string_view,4> fields{"players","target","inside","inside_any"};
-    refuse_unknown_arguments(state,fields);
+    namespace auth = scriptable_auth;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 4> kDeclared{
+        "players", "target", "inside", "inside_any"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
-    if (!current_slot(state,*handle,slot) || slot.slotType!=34 || slot.authSchema!=auth::kType34Schema)
-        return luaL_error(state,"object filter requires an authored type-34 sensor");
+    if (!current_slot(state, *handle, slot) || slot.slotType != auth::kType34SlotType
+        || slot.authSchema != auth::kType34Schema) {
+        return luaL_error(state, "object filter requires an authored type-34 sensor");
+    }
     auth::Type34Body body{};
     lua_getfield(state, 2, "inside_any");
     const bool volumes = !lua_isnil(state, -1);
     if (volumes) {
         luaL_checktype(state, -1, LUA_TTABLE);
-        const auto count = lua_rawlen(state, -1);
-        if (count == 0 || count > 5) return luaL_error(state, "inside_any requires 1..5 authored volumes");
+        const std::size_t count = lua_rawlen(state, -1);
+        if (count == 0 || count > kMaximumFilterVolumes) {
+            return luaL_error(state, "inside_any volume count is outside the filter capacity");
+        }
         for (std::size_t index = 1; index <= count; ++index) {
             lua_rawgeti(state, -1, static_cast<lua_Integer>(index));
-            const auto* ref = static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
+            const auto* const volumeHandle =
+                static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
             SlotDefinition volume{};
-            if (!current_slot(state, *ref, volume) || volume.slotType != 60)
+            if (!current_slot(state, *volumeHandle, volume)
+                || volume.slotType != auth::kType60SlotType) {
                 return luaL_error(state, "inside_any requires authored type-60 volumes");
-            body.predicates[body.count++] = auth::Type34ModeFlagSlotRef{0, true,
-                {volume.registryKey, 60, static_cast<std::int16_t>(volume.slotIndex)}};
+            }
+            body.predicates[body.count++] = auth::Type34ModeFlagSlotRef{
+                kFilterModeDirect,
+                true,
+                {volume.registryKey,
+                 static_cast<std::int8_t>(auth::kType60SlotType),
+                 static_cast<std::int16_t>(volume.slotIndex)}};
             lua_pop(state, 1);
         }
     }
     lua_pop(state, 1);
-    if (optional_boolean_argument(state,"players",false))
-        body.predicates[body.count++]=auth::Type34ModeOnlyB{static_cast<std::int8_t>(volumes ? 1 : 0)};
-    for (const auto name : {"target", "inside"}) {
-        lua_getfield(state,2,name);
-        if (!lua_isnil(state,-1)) {
-            const auto* ref=static_cast<const SlotHandle*>(luaL_checkudata(state,-1,kSlotMetatable));
-            SlotDefinition target{};
-            if (!current_slot(state,*ref,target) || target.slotType != (std::string_view(name)=="target" ? 4 : 60))
-                return luaL_error(state,"filter target must be type 4; inside must be type 60");
-            const auth::Type2LaneClientRef client{target.registryKey,static_cast<std::int8_t>(target.slotType),static_cast<std::int16_t>(target.slotIndex)};
-            if (std::string_view(name)=="target") body.predicates[body.count++]=auth::Type34ModeSlotRefC{0,client};
-            else body.predicates[body.count++]=auth::Type34ModeFlagSlotRef{1,false,client};
-        }
-        lua_pop(state,1);
+    if (optional_boolean_argument(state, "players", false)) {
+        body.predicates[body.count++] =
+            auth::Type34ModeOnlyB{static_cast<std::int8_t>(volumes ? 1 : 0)};
+    }
+    auth::Type2LaneClientRef target{};
+    if (!optional_slot_reference(state, "target", auth::kType4SlotType, target)) {
+        return luaL_error(state, "filter target must be an authored type-4 object");
+    }
+    if (target.slotIndex >= 0) {
+        body.predicates[body.count++] = auth::Type34ModeSlotRefC{kFilterModeDirect, target};
+    }
+    auth::Type2LaneClientRef inside{};
+    if (!optional_slot_reference(state, "inside", auth::kType60SlotType, inside)) {
+        return luaL_error(state, "filter inside must be an authored type-60 volume");
+    }
+    if (inside.slotIndex >= 0) {
+        body.predicates[body.count++] = auth::Type34ModeFlagSlotRef{kFilterModeInside, false, inside};
+    }
+    std::array<std::byte, auth::kType34MaximumByteCount> bytes{};
+    std::size_t written = 0;
+    std::size_t bits = 0;
+    if (!auth::encode_type34(body, bytes, written, bits)) {
+        return luaL_error(state, "object filter encoder failed");
     }
-    std::array<std::byte,auth::kType34MaximumByteCount> bytes{};std::size_t written{},bits{};
-    if (!auth::encode_type34(body,bytes,written,bits)) return luaL_error(state,"object filter encoder failed");
-    return queue_slot_auth(state,slot,auth::kType34Schema,bits,std::span(bytes).first(written));
+    return queue_slot_auth(state, slot, auth::kType34Schema, bits, std::span(bytes).first(written));
 }
+/** Attaches the authored hop-on effect to the entities a type-34 filter selects. */
 [[nodiscard]] int slot_set_mission_effect(lua_State* state) {
-    namespace effect=middleware::bap::activity_message::mission_effect;
-    namespace auth=middleware::bap::activity_message::scriptable_auth;
-    const auto* handle=static_cast<const SlotHandle*>(luaL_checkudata(state,1,kSlotMetatable));
-    static constexpr std::array<std::string_view,3> fields{"filter","enabled","revision"};
-    refuse_unknown_arguments(state,fields);
+    namespace effect = middleware::bap::activity_message::mission_effect;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 3> kDeclared{"filter", "enabled", "revision"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
-    if (!current_slot(state,*handle,slot) || slot.slotType!=26 || slot.authSchema!=0x8080954BU)
-        return luaL_error(state,"mission effect requires an authored type-26 hop-on");
-    const bool enabled=optional_boolean_argument(state,"enabled",true);
-    auth::Type2LaneClientRef client{};
+    if (!current_slot(state, *handle, slot) || slot.slotType != effect::kSlotType
+        || slot.authSchema != effect::kSchema) {
+        return luaL_error(state, "mission effect requires an authored type-26 hop-on");
+    }
+    const bool enabled = optional_boolean_argument(state, "enabled", true);
+    scriptable_auth::Type2LaneClientRef filter{};
     if (enabled) {
-        lua_getfield(state,2,"filter");
-        const auto* ref=static_cast<const SlotHandle*>(luaL_checkudata(state,-1,kSlotMetatable));
-        SlotDefinition filter{};
-        if (!current_slot(state,*ref,filter) || filter.slotType!=34) return luaL_error(state,"mission effect requires a type-34 filter");
-        client={filter.registryKey,34,static_cast<std::int16_t>(filter.slotIndex)};lua_pop(state,1);
-    }
-    const auto revision=optional_integer_argument(state,"revision",1);
-    if (revision<=0 || revision>INT32_MAX) return luaL_error(state,"effect revision must be positive");
-    std::array<std::byte,effect::kBytes> body{};std::size_t written{};
-    if (!effect::encode(client,enabled,static_cast<std::int32_t>(revision),body,written)) return luaL_error(state,"mission effect encoder failed");
-    return queue_slot_auth(state,slot,0x8080954BU,effect::kBits,body);
+        lua_getfield(state, 2, "filter");
+        const bool present = !lua_isnil(state, -1);
+        lua_pop(state, 1);
+        if (!present
+            || !optional_slot_reference(state, "filter", scriptable_auth::kType34SlotType, filter)) {
+            return luaL_error(state, "mission effect requires a type-34 filter");
+        }
+    }
+    const lua_Integer revision = optional_integer_argument(state, "revision", 1);
+    if (!valid_counter(revision)) {
+        return luaL_error(state, "effect revision must be positive");
+    }
+    std::array<std::byte, effect::kBytes> body{};
+    std::size_t written = 0;
+    if (!effect::encode(filter, enabled, static_cast<std::int32_t>(revision), body, written)) {
+        return luaL_error(state, "mission effect encoder failed");
+    }
+    return queue_slot_auth(state, slot, effect::kSchema, effect::kBits, body);
 }
 
-/** Binds an authored damage monitor to its exact object, with a retry revision. */
+/** Binds an authored damage monitor to one exact object; a new revision re-binds it. */
 [[nodiscard]] int slot_watch_damage(lua_State* state) {
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state,1,kSlotMetatable));
-    static constexpr std::array<std::string_view,2> fields{"target", "revision"};
-    refuse_unknown_arguments(state,fields);
-    SlotDefinition slot{}, target{};
-    if (!current_slot(state,*handle,slot) || slot.slotType != 20 || slot.authSchema != 0x80809563U)
-        return luaL_error(state,"damage watch requires an authored type-20 monitor");
-    lua_getfield(state,2,"target");
-    const auto* ref = static_cast<const SlotHandle*>(luaL_checkudata(state,-1,kSlotMetatable));
-    if (!current_slot(state,*ref,target) || !exact_object_slot(target))
-        return luaL_error(state,"damage target must be an authored type-4 object");
-    lua_pop(state,1);
-    const auto revision = optional_integer_argument(state,"revision",1);
-    if (revision <= 0 || revision > INT32_MAX) return luaL_error(state,"damage revision must be positive");
-    std::array<std::byte,11> body{}; std::size_t written{};
-    middleware::encoding::bits::Writer writer(body);
-    if (!writer.write(target.registryKey,32) || !writer.write(5,7)
-        || !writer.write(static_cast<std::uint16_t>(target.slotIndex) + 32768U,16)
-        || !writer.write(static_cast<std::uint32_t>(revision) + 0x80000000U,32)
-        || !writer.finish(written)) return luaL_error(state,"damage monitor encoder failed");
-    return queue_slot_auth(state,slot,0x80809563U,87,body);
+    namespace damage = middleware::bap::activity_message::damage_monitor;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 2> kDeclared{"target", "revision"};
+    refuse_unknown_arguments(state, kDeclared);
+    SlotDefinition slot{};
+    if (!current_slot(state, *handle, slot) || slot.slotType != damage::kSlotType
+        || slot.authSchema != damage::kAuthSchema) {
+        return luaL_error(state, "damage watch requires an authored type-20 monitor");
+    }
+    lua_getfield(state, 2, "target");
+    const auto* const targetHandle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
+    SlotDefinition target{};
+    const bool exact = current_slot(state, *targetHandle, target) && exact_object_slot(target);
+    lua_pop(state, 1);
+    if (!exact) {
+        return luaL_error(state, "damage target must be an authored type-4 object");
+    }
+    const lua_Integer revision = optional_integer_argument(state, "revision", 1);
+    if (!valid_counter(revision)) {
+        return luaL_error(state, "damage revision must be positive");
+    }
+    std::array<std::byte, damage::kBytes> body{};
+    std::size_t written = 0;
+    if (!damage::encode(target.registryKey,
+                        static_cast<std::uint16_t>(target.slotIndex),
+                        static_cast<std::int32_t>(revision),
+                        body,
+                        written)) {
+        return luaL_error(state, "damage monitor encoder failed");
+    }
+    return queue_slot_auth(state, slot, damage::kAuthSchema, damage::kBits, body);
 }
 
-/** Selects one authored section in a native music sensor's 128-bit mask. */
+/** Selects one authored section in a native music sensor's selection mask. */
 [[nodiscard]] int slot_set_music_section(lua_State* state) {
     namespace music = middleware::bap::activity_message::music_section;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 2> declared{"section", "enabled"};
-    refuse_unknown_arguments(state, declared);
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 2> kDeclared{"section", "enabled"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
-    const auto section = checked_integer_argument(state, "section");
-    if (!current_slot(state, *handle, slot) || slot.slotType != 11 || slot.componentClass != music::kClass
-        || slot.authSchema != music::kSchema || !(slot.flags & format::kSlotSchemaJoinExact)
-        || section < 0 || section >= 128)
-        return luaL_error(state, "music requires an exact type-11 sensor and section index 0..127");
+    const lua_Integer section = checked_integer_argument(state, "section");
+    if (!current_slot(state, *handle, slot) || slot.slotType != music::kSlotType
+        || slot.componentClass != music::kComponentClass || slot.authSchema != music::kSchema
+        || (slot.flags & format::kSlotSchemaJoinExact) == 0 || section < 0
+        || section >= static_cast<lua_Integer>(music::kSectionCount)) {
+        return luaL_error(state, "music requires an exact type-11 sensor and a section index");
+    }
     std::array<std::byte, music::kBytes> body{};
-    std::size_t written{};
-    if (!music::encode(static_cast<std::uint8_t>(section), optional_boolean_argument(state, "enabled", true), body, written))
+    std::size_t written = 0;
+    if (!music::encode(static_cast<std::uint8_t>(section),
+                       optional_boolean_argument(state, "enabled", true),
+                       body,
+                       written)) {
         return luaL_error(state, "music section encoder failed");
+    }
     return queue_slot_auth(state, slot, music::kSchema, music::kBits, body);
 }
 
 /** Spawns authored entry zero and subscribes to accepted native player use. */
 [[nodiscard]] int slot_set_interactable_object(lua_State* state) {
     namespace object = middleware::bap::activity_message::interactable_object;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state,1,kSlotMetatable));
-    static constexpr std::array<std::string_view,3> declared{"generation", "track_owner", "active"};
-    refuse_unknown_arguments(state,declared);
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 3> kDeclared{"generation", "track_owner", "active"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
-    if (!current_slot(state,*handle,slot) || !exact_object_slot(slot))
-        return luaL_error(state,"interaction requires an exact authored object");
-    const auto generation = optional_integer_argument(state,"generation",1);
-    if (generation <= 0 || generation > 0x7FFFFFFF)
-        return luaL_error(state,"object generation must be a positive int32");
-    const bool trackOwner = optional_boolean_argument(state,"track_owner",false);
-    std::array<std::byte,object::kOwnerBytes> body{};
-    std::size_t written{};
-    if (!object::encode(static_cast<std::int32_t>(generation),body,written,trackOwner,
-        optional_boolean_argument(state,"active",true)))
-        return luaL_error(state,"interactable object encoder failed");
-    return queue_slot_auth(state,slot,object::kSchema,trackOwner ? object::kOwnerBits : object::kBits,
-        std::span(body).first(written));
+    if (!current_slot(state, *handle, slot) || !exact_object_slot(slot)) {
+        return luaL_error(state, "interaction requires an exact authored object");
+    }
+    const lua_Integer generation = optional_integer_argument(state, "generation", 1);
+    if (!valid_counter(generation)) {
+        return luaL_error(state, "object generation must be a positive int32");
+    }
+    const bool trackOwner = optional_boolean_argument(state, "track_owner", false);
+    std::array<std::byte, object::kOwnerBytes> body{};
+    std::size_t written = 0;
+    if (!object::encode(static_cast<std::int32_t>(generation),
+                        body,
+                        written,
+                        trackOwner,
+                        optional_boolean_argument(state, "active", true))) {
+        return luaL_error(state, "interactable object encoder failed");
+    }
+    return queue_slot_auth(state,
+                           slot,
+                           object::kSchema,
+                           trackOwner ? object::kOwnerBits : object::kBits,
+                           std::span(body).first(written));
 }
 
 /** Enables the authored Ghost interaction without replacing its action hash. */
 [[nodiscard]] int slot_set_ghost_link(lua_State* state) {
     namespace ghost = middleware::bap::activity_message::ghost_link;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 2> declared{"generation", "enabled"};
-    refuse_unknown_arguments(state, declared);
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 2> kDeclared{"generation", "enabled"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
     if (!current_slot(state, *handle, slot) || slot.slotType != ghost::kSlotType
         || slot.componentClass != ghost::kComponentClass || slot.authSchema != ghost::kAuthSchema
@@ -516,7 +613,7 @@ constexpr std::size_t kOccupancyAuthByteCount = 11;
         return luaL_error(state, "activity slot is not an exact Ghost-link sensor");
     }
     const lua_Integer generation = checked_integer_argument(state, "generation");
-    if (generation <= 0 || generation > 0x7FFFFFFF) {
+    if (!valid_counter(generation)) {
         return luaL_error(state, "Ghost-link generation must be a positive int32");
     }
     const bool enabled = optional_boolean_argument(state, "enabled", true);
@@ -590,154 +687,227 @@ constexpr std::size_t kOccupancyAuthByteCount = 11;
         state, slot, scriptable_auth::kType71Schema, scriptable_auth::kType71BitCount, bytes);
 }
 
+/** @return True when one live Slot row is an exact squad in the given registry. */
+[[nodiscard]] bool exact_squad_of(const SlotDefinition& squad, const SlotDefinition& owner) noexcept {
+    return squad.slotType == format::kSquadSlotType
+           && squad.componentClass == format::kSquadComponentClass
+           && (squad.flags & format::kSlotSchemaJoinExact) != 0
+           && squad.registryKey == owner.registryKey && squad.objectTag == owner.objectTag
+           && squad.slotIndex <= auth_fields::kMaximumClientRefIndex;
+}
+
 /** Assigns a squad to a native combat objective and its selected task group. */
 [[nodiscard]] int slot_assign_combat_objective(lua_State* state) {
     namespace objective = middleware::bap::activity_message::squad_objective;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 4> declared{"objective", "revision", "task_group", "reserved"};
-    refuse_unknown_arguments(state, declared);
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 4> kDeclared{
+        "objective", "revision", "task_group", "reserved"};
+    refuse_unknown_arguments(state, kDeclared);
     const auto reference = checked_argument<SlotHandle>(state, "objective", kSlotMetatable);
-    const auto revision = checked_integer_argument(state, "revision");
-    const auto group = checked_integer_argument(state, "task_group");
+    const lua_Integer revision = checked_integer_argument(state, "revision");
+    const lua_Integer group = checked_integer_argument(state, "task_group");
     const bool reserved = optional_boolean_argument(state, "reserved", false);
-    SlotDefinition squad{}, target{};
-    if (!current_slot(state, *handle, squad) || squad.slotType != 1
+    SlotDefinition squad{};
+    SlotDefinition target{};
+    if (!current_slot(state, *handle, squad) || squad.slotType != format::kSquadSlotType
         || squad.componentClass != format::kSquadComponentClass
-        || squad.authSchema != objective::kSchema || !(squad.flags & format::kSlotSchemaJoinExact)
+        || squad.authSchema != objective::kSchema
+        || (squad.flags & format::kSlotSchemaJoinExact) == 0
         || !current_slot(state, reference, target) || !exact_objective_reset_slot(target)
-        || target.componentClass != 0x80808348 || squad.registryKey != target.registryKey)
-        return luaL_error(state, "combat objective requires exact squad/objective slots in the same registry");
-    if (revision <= 0 || revision > 0x7fffffff || group < -1 || group >= 24 || target.slotIndex > 32767)
-        return luaL_error(state, "combat objective revision, group or index is outside its native range");
+        || target.componentClass != format::kObjectiveComponentClass
+        || squad.registryKey != target.registryKey) {
+        return luaL_error(
+            state, "combat objective requires exact squad/objective slots in the same registry");
+    }
+    if (!valid_counter(revision) || group < objective::kNoTaskGroup
+        || group >= objective::kTaskGroupCount
+        || target.slotIndex > auth_fields::kMaximumClientRefIndex) {
+        return luaL_error(
+            state, "combat objective revision, group or index is outside its native range");
+    }
     std::array<std::byte, objective::kBytes> body{};
-    if (!objective::encode({target.registryKey, static_cast<std::uint32_t>(revision),
-                           static_cast<std::uint16_t>(target.slotIndex), static_cast<std::int32_t>(group), reserved}, body))
+    if (!objective::encode({target.registryKey,
+                            static_cast<std::uint32_t>(revision),
+                            static_cast<std::uint16_t>(target.slotIndex),
+                            static_cast<std::int32_t>(group),
+                            reserved},
+                           body)) {
         return luaL_error(state, "combat objective encoder failed");
+    }
     return queue_slot_auth(state, squad, objective::kSchema, objective::kBits, body);
 }
 
 /** Creates a named actor and starts one package-authored movement path. */
 [[nodiscard]] int slot_play_actor_path(lua_State* state) {
-    namespace path = middleware::bap::activity_message::combatant_path;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 3> declared{"generation", "revision", "path"};
-    refuse_unknown_arguments(state, declared);
-    const auto generation = checked_integer_argument(state, "generation");
-    const auto revision = checked_integer_argument(state, "revision");
+    namespace combatant = middleware::bap::activity_message::combatant_auth;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 3> kDeclared{"generation", "revision", "path"};
+    refuse_unknown_arguments(state, kDeclared);
+    const lua_Integer generation = checked_integer_argument(state, "generation");
+    const lua_Integer revision = checked_integer_argument(state, "revision");
     const auto reference = checked_argument<SlotHandle>(state, "path", kSlotMetatable);
-    SlotDefinition actor{}, sequence{};
+    SlotDefinition actor{};
+    SlotDefinition path{};
     if (!current_slot(state, *handle, actor) || !exact_combatant_slot(actor)
-        || !current_slot(state, reference, sequence) || sequence.slotType != 58
-        || sequence.componentClass != 0x80807D9B || actor.objectTag != sequence.objectTag
-        || actor.registryKey != sequence.registryKey)
-        return luaL_error(state, "actor path requires an exact member and same-registry type-58 path");
-    if (generation <= 0 || generation > 0x7fffffff || revision <= 0 || revision > 0x7fffffff
-        || sequence.slotIndex > 32767)
-        return luaL_error(state, "actor path generation, revision or index is outside its native range");
-    std::array<std::byte, path::kBytes> body{};
-    if (!path::encode({static_cast<std::uint32_t>(generation), static_cast<std::uint32_t>(revision),
-                      sequence.registryKey, static_cast<std::uint16_t>(sequence.slotIndex)}, body))
+        || !current_slot(state, reference, path) || path.slotType != combatant::kPathSlotType
+        || path.componentClass != combatant::kPathComponentClass
+        || actor.objectTag != path.objectTag || actor.registryKey != path.registryKey) {
+        return luaL_error(state,
+                          "actor path requires an exact member and same-registry type-58 path");
+    }
+    if (!valid_counter(generation) || !valid_counter(revision)
+        || path.slotIndex > auth_fields::kMaximumClientRefIndex) {
+        return luaL_error(
+            state, "actor path generation, revision or index is outside its native range");
+    }
+    std::array<std::byte, combatant::kPathBytes> body{};
+    if (!combatant::encode_path({static_cast<std::uint32_t>(generation),
+                                 static_cast<std::uint32_t>(revision),
+                                 path.registryKey,
+                                 static_cast<std::uint16_t>(path.slotIndex)},
+                                body)) {
         return luaL_error(state, "actor path encoder failed");
-    return queue_slot_auth(state, actor, path::kSchema, path::kBits, body);
+    }
+    return queue_slot_auth(state, actor, combatant::kSchema, combatant::kPathBits, body);
 }
 
 /** Runs an authored native custom action without recreating its actor. */
 [[nodiscard]] int slot_play_actor_action(lua_State* state) {
-    namespace action = middleware::bap::activity_message::combatant_action;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 4> declared{"generation", "revision", "group", "action"};
-    refuse_unknown_arguments(state, declared);
-    const auto generation = checked_integer_argument(state, "generation");
-    const auto revision = checked_integer_argument(state, "revision");
-    const auto group = checked_integer_argument(state, "group");
-    const auto name = checked_integer_argument(state, "action");
+    namespace combatant = middleware::bap::activity_message::combatant_auth;
+    constexpr lua_Integer kMaximumHash = (std::numeric_limits<std::uint32_t>::max)();
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 4> kDeclared{
+        "generation", "revision", "group", "action"};
+    refuse_unknown_arguments(state, kDeclared);
+    const lua_Integer generation = checked_integer_argument(state, "generation");
+    const lua_Integer revision = checked_integer_argument(state, "revision");
+    const lua_Integer group = checked_integer_argument(state, "group");
+    const lua_Integer action = checked_integer_argument(state, "action");
     SlotDefinition actor{};
     if (!current_slot(state, *handle, actor) || !exact_combatant_slot(actor)
-        || generation <= 0 || generation > 0x7fffffff || revision <= 0 || revision > 0x7fffffff
-        || group < 0 || group > 0xffffffffLL || name <= 0 || name > 0xffffffffLL)
-        return luaL_error(state, "actor action requires an exact member and valid native identities");
-    std::array<std::byte, action::kBytes> body{};
-    if (!action::encode({static_cast<std::uint32_t>(generation), static_cast<std::uint32_t>(revision),
-                        static_cast<std::uint32_t>(group), static_cast<std::uint32_t>(name)}, body))
+        || !valid_counter(generation) || !valid_counter(revision) || group < 0
+        || group > kMaximumHash || action <= 0 || action > kMaximumHash) {
+        return luaL_error(state,
+                          "actor action requires an exact member and valid native identities");
+    }
+    std::array<std::byte, combatant::kActionBytes> body{};
+    if (!combatant::encode_action({static_cast<std::uint32_t>(generation),
+                                   static_cast<std::uint32_t>(revision),
+                                   static_cast<std::uint32_t>(group),
+                                   static_cast<std::uint32_t>(action)},
+                                  body)) {
         return luaL_error(state, "actor action encoder failed");
-    return queue_slot_auth(state, actor, action::kSchema, action::kBits, body);
+    }
+    return queue_slot_auth(state, actor, combatant::kSchema, combatant::kActionBits, body);
+}
+
+/**
+ * Encodes and queues one delivery manifest for the named actor.
+ * @param squads Reserved squads in the actor's registry.
+ */
+[[nodiscard]] int queue_delivery(
+    lua_State* state,
+    const SlotDefinition& actor,
+    lua_Integer generation,
+    lua_Integer revision,
+    std::span<const middleware::bap::activity_message::combatant_auth::SquadReference> squads) {
+    namespace combatant = middleware::bap::activity_message::combatant_auth;
+    std::array<std::byte, combatant::kDeliveryMaximumBytes> body{};
+    std::size_t written = 0;
+    std::size_t bits = 0;
+    if (!combatant::encode_delivery(static_cast<std::uint32_t>(generation),
+                                    static_cast<std::uint32_t>(revision),
+                                    squads,
+                                    body,
+                                    written,
+                                    bits)) {
+        return luaL_error(state, "invalid delivery manifest");
+    }
+    return queue_slot_auth(state, actor, combatant::kSchema, bits, std::span(body).first(written));
 }
 
-/** Gives a reserved squad to the named actor's native passenger-delivery component. */
+/** Gives one reserved squad to the named actor's native passenger-delivery component. */
 [[nodiscard]] int slot_deliver_squad(lua_State* state) {
-    namespace delivery = middleware::bap::activity_message::combatant_delivery;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 3> declared{"generation", "revision", "squad"};
-    refuse_unknown_arguments(state, declared);
-    const auto generation = checked_integer_argument(state, "generation");
-    const auto revision = checked_integer_argument(state, "revision");
+    namespace combatant = middleware::bap::activity_message::combatant_auth;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 3> kDeclared{"generation", "revision", "squad"};
+    refuse_unknown_arguments(state, kDeclared);
+    const lua_Integer generation = checked_integer_argument(state, "generation");
+    const lua_Integer revision = checked_integer_argument(state, "revision");
     const auto reference = checked_argument<SlotHandle>(state, "squad", kSlotMetatable);
-    SlotDefinition actor{}, squad{};
+    SlotDefinition actor{};
+    SlotDefinition squad{};
     if (!current_slot(state, *handle, actor) || !exact_combatant_slot(actor)
-        || !current_slot(state, reference, squad) || squad.slotType != 1
-        || squad.componentClass != format::kSquadComponentClass
-        || !(squad.flags & format::kSlotSchemaJoinExact)
-        || actor.objectTag != squad.objectTag || actor.registryKey != squad.registryKey)
+        || !current_slot(state, reference, squad) || !exact_squad_of(squad, actor)) {
         return luaL_error(state, "delivery requires an exact member and same-registry squad");
-    if (generation <= 0 || generation > 0x7fffffff || revision <= 0 || revision > 0x7fffffff
-        || squad.slotIndex > 32767)
-        return luaL_error(state, "delivery generation, revision or index is outside its native range");
-    std::array<std::byte, delivery::kBytes> body{};
-    if (!delivery::encode({static_cast<std::uint32_t>(generation), static_cast<std::uint32_t>(revision),
-                          squad.registryKey, static_cast<std::uint16_t>(squad.slotIndex)}, body))
-        return luaL_error(state, "delivery encoder failed");
-    return queue_slot_auth(state, actor, delivery::kSchema, delivery::kBits, body);
+    }
+    if (!valid_counter(generation) || !valid_counter(revision)) {
+        return luaL_error(state, "delivery generation or revision is outside its native range");
+    }
+    const std::array<combatant::SquadReference, 1> squads{
+        {{squad.registryKey, static_cast<std::uint16_t>(squad.slotIndex)}}};
+    return queue_delivery(state, actor, generation, revision, squads);
 }
 
-/** Sends one native manifest so multiple reserved squads share the same ship and unload. */
+/** Sends one manifest so several reserved squads share the same ship and unload together. */
 [[nodiscard]] int slot_deliver_squads(lua_State* state) {
-    namespace delivery = middleware::bap::activity_message::combatant_delivery;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view,3> declared{"generation","revision","squads"};
-    refuse_unknown_arguments(state, declared);
-    const auto generation = checked_integer_argument(state, "generation");
-    const auto revision = checked_integer_argument(state, "revision");
+    namespace combatant = middleware::bap::activity_message::combatant_auth;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 3> kDeclared{"generation", "revision", "squads"};
+    refuse_unknown_arguments(state, kDeclared);
+    const lua_Integer generation = checked_integer_argument(state, "generation");
+    const lua_Integer revision = checked_integer_argument(state, "revision");
     SlotDefinition actor{};
-    if (!current_slot(state,*handle,actor) || !exact_combatant_slot(actor)
-        || generation <= 0 || generation > 0x7fffffff || revision <= 0 || revision > 0x7fffffff)
-        return luaL_error(state,"delivery requires an exact actor and positive generation/revision");
-    lua_getfield(state,2,"squads"); luaL_checktype(state,-1,LUA_TTABLE);
-    const int list=lua_gettop(state); const auto count=lua_rawlen(state,list);
-    if (count==0 || count>delivery::kMaxSquads) return luaL_error(state,"delivery needs 1..8 reserved squads");
-    std::array<delivery::SquadReference,delivery::kMaxSquads> refs{};
-    for (std::size_t i=0;i<count;++i) {
-        lua_rawgeti(state,list,static_cast<lua_Integer>(i+1));
-        const auto* squadHandle=static_cast<const SlotHandle*>(luaL_checkudata(state,-1,kSlotMetatable));
+    if (!current_slot(state, *handle, actor) || !exact_combatant_slot(actor)
+        || !valid_counter(generation) || !valid_counter(revision)) {
+        return luaL_error(state, "delivery requires an exact actor and positive generation/revision");
+    }
+    lua_getfield(state, 2, "squads");
+    luaL_checktype(state, -1, LUA_TTABLE);
+    const int list = lua_gettop(state);
+    const std::size_t count = lua_rawlen(state, list);
+    if (count == 0 || count > combatant::kMaximumManifestSquads) {
+        return luaL_error(state, "delivery manifest squad count is outside its native range");
+    }
+    std::array<combatant::SquadReference, combatant::kMaximumManifestSquads> squads{};
+    for (std::size_t index = 0; index < count; ++index) {
+        lua_rawgeti(state, list, static_cast<lua_Integer>(index + 1));
+        const auto* const squadHandle =
+            static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
         SlotDefinition squad{};
-        if (!current_slot(state,*squadHandle,squad) || squad.slotType!=1
-            || squad.componentClass!=format::kSquadComponentClass || !(squad.flags & format::kSlotSchemaJoinExact)
-            || squad.registryKey!=actor.registryKey || squad.objectTag!=actor.objectTag || squad.slotIndex>32767)
-            return luaL_error(state,"delivery squads must be exact and in the actor's registry");
-        refs[i]={squad.registryKey,static_cast<std::uint16_t>(squad.slotIndex)};
-        lua_pop(state,1);
-    }
-    lua_pop(state,1);
-    std::array<std::byte,delivery::kMaxBytes> body{}; std::size_t written{},bits{};
-    if (!delivery::encode_many(static_cast<std::uint32_t>(generation),static_cast<std::uint32_t>(revision),
-        std::span(refs).first(count),body,written,bits)) return luaL_error(state,"invalid delivery manifest");
-    return queue_slot_auth(state,actor,delivery::kSchema,bits,std::span(body).first(written));
+        if (!current_slot(state, *squadHandle, squad) || !exact_squad_of(squad, actor)) {
+            return luaL_error(state, "delivery squads must be exact and in the actor's registry");
+        }
+        squads[index] = {squad.registryKey, static_cast<std::uint16_t>(squad.slotIndex)};
+        lua_pop(state, 1);
+    }
+    lua_pop(state, 1);
+    return queue_delivery(state, actor, generation, revision, std::span(squads).first(count));
 }
 
-/** Disables the named actor after completion, including retained Auth on region reload. */
+/** Retires the named actor on a new generation, which also clears retained Auth on reload. */
 [[nodiscard]] int slot_retire_actor(lua_State* state) {
-    namespace retire = middleware::bap::activity_message::combatant_retire;
-    const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
-    static constexpr std::array<std::string_view, 1> declared{"generation"};
-    refuse_unknown_arguments(state, declared);
-    const auto generation = checked_integer_argument(state, "generation");
+    namespace combatant = middleware::bap::activity_message::combatant_auth;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 1> kDeclared{"generation"};
+    refuse_unknown_arguments(state, kDeclared);
+    const lua_Integer generation = checked_integer_argument(state, "generation");
     SlotDefinition actor{};
     if (!current_slot(state, *handle, actor) || !exact_combatant_slot(actor)
-        || generation <= 0 || generation > 0x7fffffff)
+        || !valid_counter(generation)) {
         return luaL_error(state, "actor retirement requires an exact member and positive generation");
-    std::array<std::byte, retire::kBytes> body{};
-    if (!retire::encode(static_cast<std::uint32_t>(generation), body))
+    }
+    std::array<std::byte, combatant::kRetireBytes> body{};
+    if (!combatant::encode_retire(static_cast<std::uint32_t>(generation), body)) {
         return luaL_error(state, "actor retirement encoder failed");
-    return queue_slot_auth(state, actor, retire::kSchema, retire::kBits, body);
+    }
+    return queue_slot_auth(state, actor, combatant::kSchema, combatant::kRetireBits, body);
 }
 
 /** Binds one exact combatant to its package-authored squad member. */
@@ -907,30 +1077,49 @@ constexpr std::size_t kOccupancyAuthByteCount = 11;
 
 /** Publishes one scene generation and its cumulative authored event keys. */
 [[nodiscard]] int slot_set_scene_events(lua_State* state) {
-    namespace scene=middleware::bap::activity_message::scene_events;
-    const auto* handle=static_cast<const SlotHandle*>(luaL_checkudata(state,1,kSlotMetatable));
+    namespace scene = middleware::bap::activity_message::scene_events;
+    const auto* const handle =
+        static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
+    static constexpr std::array<std::string_view, 2> kDeclared{"generation", "events"};
+    refuse_unknown_arguments(state, kDeclared);
     SlotDefinition slot{};
-    if (!current_slot(state,*handle,slot) || slot.slotType!=43 || slot.componentClass!=0x80806382U
-        || slot.authSchema!=scene::kSchema || (slot.flags&format::kSlotSchemaJoinExact)==0)
-        return luaL_error(state,"scene events require an exact type-43 scene");
-    static constexpr std::array<std::string_view,2> declared{"generation","events"};
-    refuse_unknown_arguments(state,declared);
-    lua_getfield(state,2,"generation"); const auto generation=luaL_checkinteger(state,-1);lua_pop(state,1);
-    if (generation<=0 || generation>0x7FFFFFFF) return luaL_error(state,"scene generation must be positive int32");
-    lua_getfield(state,2,"events");luaL_checktype(state,-1,LUA_TTABLE);
-    const auto count=lua_rawlen(state,-1);
-    if (count>scene::kMaximumEvents) return luaL_error(state,"scene event list exceeds 32 keys");
-    std::array<std::uint32_t,scene::kMaximumEvents> events{};
-    for (std::size_t i=0;i<count;++i) {
-        lua_rawgeti(state,-1,static_cast<lua_Integer>(i+1));const auto event=luaL_checkinteger(state,-1);lua_pop(state,1);
-        if (event<=0 || event>=0xFFFFFFFFLL) return luaL_error(state,"invalid scene event key");
-        events[i]=static_cast<std::uint32_t>(event);
-    }
-    lua_pop(state,1);
-    std::array<std::byte,scene::kMaximumBytes> body{};std::size_t bytes{},bits{};
-    if (!scene::encode(static_cast<std::int32_t>(generation),std::span(events).first(count),body,bytes,bits))
-        return luaL_error(state,"scene event keys must be unique");
-    return queue_slot_auth(state,slot,scene::kSchema,bits,std::span(body).first(bytes));
+    if (!current_slot(state, *handle, slot) || slot.slotType != scene::kSlotType
+        || slot.componentClass != scene::kComponentClass || slot.authSchema != scene::kSchema
+        || (slot.flags & format::kSlotSchemaJoinExact) == 0) {
+        return luaL_error(state, "scene events require an exact type-43 scene");
+    }
+    const lua_Integer generation = checked_integer_argument(state, "generation");
+    if (!valid_counter(generation)) {
+        return luaL_error(state, "scene generation must be a positive int32");
+    }
+    lua_getfield(state, 2, "events");
+    luaL_checktype(state, -1, LUA_TTABLE);
+    const std::size_t count = lua_rawlen(state, -1);
+    if (count > scene::kMaximumEvents) {
+        return luaL_error(state, "scene event list exceeds the manifest capacity");
+    }
+    std::array<std::uint32_t, scene::kMaximumEvents> events{};
+    for (std::size_t index = 0; index < count; ++index) {
+        lua_rawgeti(state, -1, static_cast<lua_Integer>(index + 1));
+        const lua_Integer event = luaL_checkinteger(state, -1);
+        lua_pop(state, 1);
+        if (event <= 0 || event >= static_cast<lua_Integer>(scene::kInvalidEventKey)) {
+            return luaL_error(state, "invalid scene event key");
+        }
+        events[index] = static_cast<std::uint32_t>(event);
+    }
+    lua_pop(state, 1);
+    std::array<std::byte, scene::kMaximumBytes> body{};
+    std::size_t bytes = 0;
+    std::size_t bits = 0;
+    if (!scene::encode(static_cast<std::int32_t>(generation),
+                       std::span(events).first(count),
+                       body,
+                       bytes,
+                       bits)) {
+        return luaL_error(state, "scene event keys must be unique");
+    }
+    return queue_slot_auth(state, slot, scene::kSchema, bits, std::span(body).first(bytes));
 }
 
 /** Lua `play_sequence` on a slot. Errors unless the slot is an exact type-5 sequence. */

+ 77 - 27
Sunrise/src/server/activity/mission/mission_script_object_sense.h

@@ -1,43 +1,93 @@
 #pragma once
+
 #include <cstdint>
 #include <span>
+
+#include "../../../middleware/bap/activity_message/interactable_object_auth.h"
 #include "../../../middleware/bap/activity_message/sense_update.h"
+
 namespace sunrise::server::activity::mission {
+
+/** Type-4 object Sense root ordinals. */
+inline constexpr std::uint16_t kObjectGenerationOrdinal = 0;
+inline constexpr std::uint16_t kObjectAliveOrdinal = 1;
+inline constexpr std::uint16_t kObjectPresentOrdinal = 2;
+/** Ownership reply: field 0 held, field 1 the owner key. */
+inline constexpr std::uint16_t kOwnerHeldOrdinal = 0;
+inline constexpr std::uint16_t kOwnerKeyOrdinal = 1;
+/** Interaction reply: field 0 is the one-shot used latch. */
+inline constexpr std::uint16_t kInteractedOrdinal = 0;
+
+/** Last object level seen for one slot. */
 struct ObjectInteractionLevel final {
     std::int32_t generation{};
-    bool generationKnown{}, interacted{}, interactionKnown{};
-    bool present{}, alive{}, stateKnown{}, ownerKnown{}, hasOwner{};
+    bool generationKnown{};
+    bool interacted{};
+    bool interactionKnown{};
+    bool present{};
+    bool alive{};
+    bool stateKnown{};
+    bool ownerKnown{};
+    bool hasOwner{};
     std::uint64_t ownerKey{};
 };
-// Do not combine a new object generation with a stale interaction latch. Replay of
-// an accepted true level is silent; the Lua encounter also retains its one-shot receipt.
-[[nodiscard]] inline bool update_object_interaction(ObjectInteractionLevel& level,
+
+/**
+ * Merges one decoded type-4 body into the retained level. A new generation clears the level, so
+ * a stale interaction latch never joins a new object. A body is a full snapshot, so an absent
+ * ownership reply clears the owner.
+ * @return True when this body is the first report of an accepted interaction for its generation.
+ */
+[[nodiscard]] inline bool update_object_interaction(
+    ObjectInteractionLevel& level,
     std::span<const middleware::bap::activity_message::sense_update::DecodedValue> values,
     std::uint32_t root) noexcept {
-    const auto before = level;
-    for (const auto& v : values) {
-        if (!v.present || v.schemaRow != root || v.fieldOrdinal != 0) continue;
-        const auto generation = static_cast<std::int32_t>(v.signedValue);
-        if (level.generationKnown && generation < level.generation) return false;
-        if (!level.generationKnown || generation != level.generation) level = {};
-        level.generation = generation; level.generationKnown = true;
-    }
-    // Complete native object bodies clear an absent owner subscription; they are not deltas.
-    level.ownerKnown = false; level.hasOwner = false; level.ownerKey = 0;
-    for (const auto& v : values) {
-        if (v.present && v.schemaRow == root && v.fieldOrdinal == 1) level.alive = v.unsignedValue != 0;
-        if (v.present && v.schemaRow == root && v.fieldOrdinal == 2) {
-            level.present = v.unsignedValue != 0; level.stateKnown = true;
+    namespace object = middleware::bap::activity_message::interactable_object;
+    const ObjectInteractionLevel before = level;
+    for (const auto& value : values) {
+        if (!value.present || value.schemaRow != root
+            || value.fieldOrdinal != kObjectGenerationOrdinal) {
+            continue;
         }
-        if (v.present && v.schemaRow == 0x80809ACCU && v.fieldOrdinal == 0) {
-            level.ownerKnown = true; level.hasOwner = v.unsignedValue != 0;
+        const auto generation = static_cast<std::int32_t>(value.signedValue);
+        if (level.generationKnown && generation < level.generation) {
+            return false;
         }
-        if (v.present && v.schemaRow == 0x80809ACCU && v.fieldOrdinal == 1) level.ownerKey = v.unsignedValue;
-        if (v.present && v.schemaRow == 0x80804FB7U && v.fieldOrdinal == 0) {
-            level.interacted = v.unsignedValue != 0; level.interactionKnown = true;
+        if (!level.generationKnown || generation != level.generation) {
+            level = {};
         }
+        level.generation = generation;
+        level.generationKnown = true;
     }
-    return level.generationKnown && level.generation > 0 && level.interactionKnown && level.interacted
-        && (!before.interactionKnown || !before.interacted || before.generation != level.generation);
-}
+    level.ownerKnown = false;
+    level.hasOwner = false;
+    level.ownerKey = 0;
+    for (const auto& value : values) {
+        if (!value.present) {
+            continue;
+        }
+        if (value.schemaRow == root && value.fieldOrdinal == kObjectAliveOrdinal) {
+            level.alive = value.unsignedValue != 0;
+        } else if (value.schemaRow == root && value.fieldOrdinal == kObjectPresentOrdinal) {
+            level.present = value.unsignedValue != 0;
+            level.stateKnown = true;
+        } else if (value.schemaRow == object::kOwnershipReply
+                   && value.fieldOrdinal == kOwnerHeldOrdinal) {
+            level.ownerKnown = true;
+            level.hasOwner = value.unsignedValue != 0;
+        } else if (value.schemaRow == object::kOwnershipReply
+                   && value.fieldOrdinal == kOwnerKeyOrdinal) {
+            level.ownerKey = value.unsignedValue;
+        } else if (value.schemaRow == object::kInteractionReply
+                   && value.fieldOrdinal == kInteractedOrdinal) {
+            level.interacted = value.unsignedValue != 0;
+            level.interactionKnown = true;
+        }
+    }
+    return level.generationKnown && level.generation > 0 && level.interactionKnown
+           && level.interacted
+           && (!before.interactionKnown || !before.interacted
+               || before.generation != level.generation);
 }
+
+} // namespace sunrise::server::activity::mission

Некоторые файлы не были показаны из-за большого количества измененных файлов