2 Commits fbfcb1ca3d ... f1e7c66baf

Autor SHA1 Nachricht Datum
  stan f1e7c66baf Merge remote-tracking branch 'origin/master' vor 1 Woche
  stan 9260d10c6c studio and 0.6 vor 1 Woche
100 geänderte Dateien mit 2259 neuen und 1947 gelöschten Zeilen
  1. 2 0
      README.md
  2. 163 20
      Sunrise/Sunrise.vcxproj
  3. 5 2
      Sunrise/resources/default_settings.json
  4. 3 3
      Sunrise/resources/resource.h
  5. 1 4
      Sunrise/src/client/activity/mission_launch.cpp
  6. 2 2
      Sunrise/src/client/content/activity/activity_sdk_activity_enrichment_inventory.cpp
  7. 10 17
      Sunrise/src/client/content/activity/activity_sdk_activity_inventory.cpp
  8. 74 73
      Sunrise/src/client/content/activity/activity_sdk_actor_rsat_inventory.cpp
  9. 4 11
      Sunrise/src/client/content/activity/activity_sdk_actor_rsat_inventory.h
  10. 1 1
      Sunrise/src/client/content/activity/activity_sdk_actor_rsat_inventory_internal.h
  11. 1 1
      Sunrise/src/client/content/activity/activity_sdk_actor_sequences.h
  12. 89 2
      Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.cpp
  13. 2 2
      Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.h
  14. 9 2
      Sunrise/src/client/content/activity/activity_sdk_authored_scene_validation.cpp
  15. 11 12
      Sunrise/src/client/content/activity/activity_sdk_behavior_inventory.cpp
  16. 8 7
      Sunrise/src/client/content/activity/activity_sdk_decode_plans.cpp
  17. 1 10
      Sunrise/src/client/content/activity/activity_sdk_decode_plans_compiler.cpp
  18. 64 12
      Sunrise/src/client/content/activity/activity_sdk_decode_plans_image.cpp
  19. 3 38
      Sunrise/src/client/content/activity/activity_sdk_decode_plans_index.cpp
  20. 51 10
      Sunrise/src/client/content/activity/activity_sdk_decode_plans_internal.h
  21. 2 48
      Sunrise/src/client/content/activity/activity_sdk_decode_plans_metadata.cpp
  22. 0 17
      Sunrise/src/client/content/activity/activity_sdk_estate_validation.cpp
  23. 0 3
      Sunrise/src/client/content/activity/activity_sdk_estate_validation.h
  24. 104 23
      Sunrise/src/client/content/activity/activity_sdk_generation_inventory.cpp
  25. 8 9
      Sunrise/src/client/content/activity/activity_sdk_generation_publish.cpp
  26. 13 7
      Sunrise/src/client/content/activity/activity_sdk_generation_scenario_shards.cpp
  27. 1 1
      Sunrise/src/client/content/activity/activity_sdk_generation_shard.cpp
  28. 58 44
      Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp
  29. 21 4
      Sunrise/src/client/content/activity/activity_sdk_generation_worker_internal.h
  30. 2 25
      Sunrise/src/client/content/activity/activity_sdk_generation_world_source.cpp
  31. 4 1
      Sunrise/src/client/content/activity/activity_sdk_ir_family_coverage.cpp
  32. 2 0
      Sunrise/src/client/content/activity/activity_sdk_ir_family_coverage.h
  33. 49 21
      Sunrise/src/client/content/activity/activity_sdk_live_publication.cpp
  34. 1 4
      Sunrise/src/client/content/activity/activity_sdk_live_publication.h
  35. 5 19
      Sunrise/src/client/content/activity/activity_sdk_lua_actor_sequences.cpp
  36. 2 4
      Sunrise/src/client/content/activity/activity_sdk_lua_artifacts.h
  37. 24 1
      Sunrise/src/client/content/activity/activity_sdk_lua_artifacts_internal.h
  38. 0 6
      Sunrise/src/client/content/activity/activity_sdk_lua_catalog_contract.cpp
  39. 19 257
      Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp
  40. 369 0
      Sunrise/src/client/content/activity/activity_sdk_lua_mission_api.cpp
  41. 338 0
      Sunrise/src/client/content/activity/activity_sdk_lua_mission_luals.cpp
  42. 20 17
      Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp
  43. 14 0
      Sunrise/src/client/content/activity/activity_sdk_lua_missions_internal.h
  44. 19 34
      Sunrise/src/client/content/activity/activity_sdk_lua_publish.cpp
  45. 69 0
      Sunrise/src/client/content/activity/activity_sdk_lua_scene_events.cpp
  46. 75 2
      Sunrise/src/client/content/activity/activity_sdk_lua_value.cpp
  47. 0 18
      Sunrise/src/client/content/activity/activity_sdk_lua_world_contract.cpp
  48. 0 67
      Sunrise/src/client/content/activity/activity_sdk_lua_world_core_views.cpp
  49. 0 67
      Sunrise/src/client/content/activity/activity_sdk_lua_world_spatial_views.cpp
  50. 0 291
      Sunrise/src/client/content/activity/activity_sdk_mission_abi_data.inc
  51. 2 50
      Sunrise/src/client/content/activity/activity_sdk_native_pack_pipeline.cpp
  52. 0 20
      Sunrise/src/client/content/activity/activity_sdk_native_pack_pipeline.h
  53. 5 14
      Sunrise/src/client/content/activity/activity_sdk_one_shot.cpp
  54. 53 110
      Sunrise/src/client/content/activity/activity_sdk_pack_composer.cpp
  55. 1 3
      Sunrise/src/client/content/activity/activity_sdk_pack_composer_internal.h
  56. 2 35
      Sunrise/src/client/content/activity/activity_sdk_pack_composer_rows.cpp
  57. 1 3
      Sunrise/src/client/content/activity/activity_sdk_pack_composer_validate.cpp
  58. 2 18
      Sunrise/src/client/content/activity/activity_sdk_policy_input_validation.cpp
  59. 11 10
      Sunrise/src/client/content/activity/activity_sdk_policy_inventory.cpp
  60. 4 4
      Sunrise/src/client/content/activity/activity_sdk_policy_inventory.h
  61. 1 6
      Sunrise/src/client/content/activity/activity_sdk_policy_inventory_internal.h
  62. 4 3
      Sunrise/src/client/content/activity/activity_sdk_semantic_surface_data.inc
  63. 8 28
      Sunrise/src/client/content/activity/activity_sdk_squad_config_parse.cpp
  64. 1 11
      Sunrise/src/client/content/activity/activity_sdk_squad_graph.cpp
  65. 2 55
      Sunrise/src/client/content/activity/activity_sdk_squad_graph_edges.cpp
  66. 1 0
      Sunrise/src/client/content/activity/activity_sdk_squad_graph_shard.h
  67. 3 64
      Sunrise/src/client/content/activity/activity_sdk_squad_inventory.cpp
  68. 0 9
      Sunrise/src/client/content/activity/activity_sdk_squad_inventory.h
  69. 68 0
      Sunrise/src/client/content/activity/activity_sdk_squad_inventory_internal.h
  70. 5 48
      Sunrise/src/client/content/activity/activity_sdk_squad_inventory_link.cpp
  71. 11 27
      Sunrise/src/client/content/activity/activity_sdk_squad_inventory_link_support.cpp
  72. 1 9
      Sunrise/src/client/content/activity/activity_sdk_topology_inventory.cpp
  73. 1 1
      Sunrise/src/client/content/activity/activity_sdk_topology_inventory.h
  74. 1 9
      Sunrise/src/client/content/activity/activity_sdk_topology_inventory_finish.cpp
  75. 9 0
      Sunrise/src/client/content/activity/activity_sdk_topology_inventory_internal.h
  76. 3 2
      Sunrise/src/client/content/activity/activity_sdk_tree_publication.cpp
  77. 1 1
      Sunrise/src/client/content/activity/entity_object_type_build.cpp
  78. 3 9
      Sunrise/src/client/content/activity/entity_position_profile_build.cpp
  79. 21 1
      Sunrise/src/client/content/activity/scriptable_catalog_builder.h
  80. 8 1
      Sunrise/src/client/content/activity/scriptable_catalog_container_index.cpp
  81. 7 1
      Sunrise/src/client/content/activity/scriptable_catalog_container_index.h
  82. 0 41
      Sunrise/src/client/content/activity/scriptable_catalog_container_placement_cache.cpp
  83. 9 3
      Sunrise/src/client/content/activity/scriptable_catalog_container_placements.cpp
  84. 14 2
      Sunrise/src/client/content/activity/scriptable_catalog_container_placements.h
  85. 0 1
      Sunrise/src/client/content/activity/scriptable_catalog_container_placements_internal.h
  86. 49 21
      Sunrise/src/client/content/activity/scriptable_catalog_names.cpp
  87. 10 1
      Sunrise/src/client/content/activity/scriptable_catalog_names.h
  88. 8 3
      Sunrise/src/client/content/activity/scriptable_catalog_spatial_graph.cpp
  89. 14 2
      Sunrise/src/client/content/activity/scriptable_catalog_spatial_graph.h
  90. 2 2
      Sunrise/src/client/content/activity/scriptable_catalog_type23_placement_links.cpp
  91. 23 25
      Sunrise/src/client/content/activity/scriptable_catalog_worker.cpp
  92. 4 1
      Sunrise/src/client/content/activity/scriptable_catalog_worker_lifecycle.cpp
  93. 92 16
      Sunrise/src/client/content/hash_names/hash_name_build.cpp
  94. 19 0
      Sunrise/src/client/content/hash_names/hash_name_build.h
  95. 40 40
      Sunrise/src/client/content/hash_names/hash_name_matcher.cpp
  96. 8 4
      Sunrise/src/client/content/hash_names/hash_name_matcher.h
  97. 1 4
      Sunrise/src/client/content/investment/investment_refresh.cpp
  98. 0 9
      Sunrise/src/client/content/items/layout.h
  99. 3 0
      Sunrise/src/client/content/items/packages/build.h
  100. 0 1
      Sunrise/src/client/content/items/packages/internal.h

+ 2 - 0
README.md

@@ -159,6 +159,8 @@ Pull Requests are welcome. Please follow these rules:
 - https://github.com/cohaereo/destinydocs
 - https://github.com/MontagueM/DestinyUnpacker
 - https://github.com/nblockbuster/bungie-lua-decompiler
+- https://github.com/Blam-Network/tiger
+- https://github.com/Blam-Network/api-tiger
 
 ### Other:
 

+ 163 - 20
Sunrise/Sunrise.vcxproj

@@ -99,6 +99,22 @@
     <None Include="vendor\lua\UPSTREAM_README" />
   </ItemGroup>
   <ItemGroup>
+    <ClCompile Include="src\client\diagnostics\studio\studio_link.cpp" />
+    <ClCompile Include="src\client\diagnostics\studio\studio_world_reader.cpp" />
+    <ClCompile Include="src\client\diagnostics\studio\studio_world_link.cpp" />
+    <ClCompile Include="src\client\diagnostics\studio\studio_pose_capture.cpp" />
+    <!-- Uncounted observer boundaries must not call compiler debug helpers. -->
+    <ClCompile Include="src\client\diagnostics\studio\studio_pose_link.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
+    <ClCompile Include="src\client\diagnostics\studio\studio_object_link.cpp" />
+    <ClCompile Include="src\client\diagnostics\studio\studio_cui_reader.cpp" />
+    <ClCompile Include="src\client\diagnostics\studio\studio_cui_link.cpp" />
+    <ClCompile Include="src\client\diagnostics\studio\studio_cui_observer.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\state\gameplay\external\entity_object_types.cpp" />
     <ClCompile Include="src\state\investment\investment_account_store.cpp" />
     <ClCompile Include="src\state\investment\investment_database.cpp" />
@@ -119,9 +135,43 @@
     <ClCompile Include="src\core\filesystem\path.cpp" />
     <ClCompile Include="src\core\filesystem\temporary_sibling.cpp" />
     <ClCompile Include="src\core\settings\settings_parser.cpp" />
-    <ClCompile Include="src\core\settings\json_number.cpp" />
-    <ClCompile Include="src\core\settings\json_scalar.cpp" />
-    <ClCompile Include="src\core\settings\json_structure.cpp" />
+    <ClCompile Include="src\middleware\crypto\sha1.cpp" />
+    <ClCompile Include="src\middleware\content\packages\merge\package_merge_read.cpp" />
+    <ClCompile Include="src\middleware\content\packages\config_bake\package_config_bake.cpp" />
+    <ClCompile Include="src\middleware\content\packages\config_bake\package_config_bake_read.cpp" />
+    <ClCompile Include="src\middleware\content\packages\config_bake\package_config_bake_plan.cpp" />
+    <ClCompile Include="src\middleware\content\packages\merge\package_merge_write.cpp" />
+    <ClCompile Include="src\middleware\content\packages\merge\dependency_records.cpp" />
+    <ClCompile Include="src\middleware\content\packages\table_merge\sidecar.cpp" />
+    <ClCompile Include="src\middleware\content\packages\table_merge\entry_merge.cpp" />
+    <ClCompile Include="src\middleware\content\packages\table_merge\manifest_set.cpp" />
+    <ClCompile Include="src\middleware\content\packages\table_merge\rows.cpp" />
+    <ClCompile Include="src\middleware\content\packages\table_merge\plan.cpp" />
+    <ClCompile Include="src\state\mods\mods_runtime.cpp" />
+    <ClCompile Include="src\state\mods\config\mod_config.cpp" />
+    <ClCompile Include="src\state\mods\problem\mod_problem.cpp" />
+    <ClCompile Include="src\state\mods\manifest\mod_version.cpp" />
+    <ClCompile Include="src\state\mods\manifest\mod_manifest_parser.cpp" />
+    <ClCompile Include="src\state\mods\manifest\mod_manifest_bindings.cpp" />
+    <ClCompile Include="src\middleware\content\cui\cui_config_scalar.cpp" />
+    <ClCompile Include="src\state\mods\resolution\mod_resolution.cpp" />
+    <ClCompile Include="src\state\mods\discovery\mod_discovery.cpp" />
+    <ClCompile Include="src\state\mods\packages\mod_packages.cpp" />
+    <ClCompile Include="src\state\mods\packages\mod_package_stage.cpp" />
+    <ClCompile Include="src\state\mods\packages\mod_package_serve.cpp" />
+    <ClCompile Include="src\state\mods\packages\mod_package_config.cpp" />
+    <ClCompile Include="src\client\patterns\game\package_path\package_path_signature_bytes.cpp" />
+    <ClCompile Include="src\client\targets\game\package_path\game_package_path_targets.cpp" />
+    <!-- Uncounted protected boundaries must not call compiler debug helpers. -->
+    <ClCompile Include="src\client\hooks\package_path\package_path_redirect.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
+    <ClCompile Include="src\core\ui\modules\mods\mods_module_runtime.cpp" />
+    <ClCompile Include="src\core\ui\modules\mods\mods_panel_render.cpp" />
+    <ClCompile Include="src\core\json\json_reader_number.cpp" />
+    <ClCompile Include="src\core\json\json_reader_scalar.cpp" />
+    <ClCompile Include="src\core\json\json_reader_structure.cpp" />
     <ClCompile Include="src\core\settings\settings_runtime.cpp" />
     <ClCompile Include="src\core\settings\client\client_settings_parser.cpp" />
     <ClCompile Include="src\core\settings\client\client_ui_settings_parser.cpp" />
@@ -249,6 +299,7 @@
     <ClCompile Include="src\client\hooks\teleport\teleport_move.cpp" />
     <ClCompile Include="src\client\hooks\teleport\teleport_action_key.cpp" />
     <ClCompile Include="src\client\hooks\world_objects\world_object_registry.cpp" />
+    <ClCompile Include="src\client\hooks\world_objects\dynamic_object_registry.cpp" />
     <ClCompile Include="src\client\hooks\world_objects\world_object_identity_report.cpp" />
     <ClCompile Include="src\client\hooks\world_objects\world_object_purge_policy.cpp" />
     <ClCompile Include="src\client\hooking\detour.cpp" />
@@ -261,7 +312,10 @@
     <ClCompile Include="src\client\hooks\cine_probe\cine_probe.cpp" />
     <ClCompile Include="src\client\hooks\net_tick_probe\net_tick_probe.cpp" />
     <ClCompile Include="src\client\hooks\external_server\external_server_route.cpp" />
-    <ClCompile Include="src\client\hooks\external_server\external_server_setopt_guard.cpp" />
+    <ClCompile Include="src\client\hooks\external_server\external_server_setopt_guard.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\hooks\bootflow\bootflow_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\bootflow_texture_override.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\lifetime_gate_probe.cpp" />
@@ -271,10 +325,16 @@
     <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" />
     <ClCompile Include="src\client\content\diagnostics\content_readiness_report.cpp" />
-    <ClCompile Include="src\client\hooks\network\bubble_authority\bubble_authority_replacements.cpp" />
+    <ClCompile Include="src\client\hooks\network\bubble_authority\bubble_authority_replacements.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\hooks\network\bubble_authority\scope\bubble_authority_scope.cpp" />
     <ClCompile Include="src\client\hooks\network\http\http_descriptor_route.cpp" />
-    <ClCompile Include="src\client\hooks\network\http\http_executor_replacement.cpp" />
+    <ClCompile Include="src\client\hooks\network\http\http_executor_replacement.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\hooks\network\http\http_worker_completion.cpp" />
     <ClCompile Include="src\client\patterns\game\signon\signon_readiness_signature_bytes.cpp" />
     <ClCompile Include="src\client\graphics\wine_compat.cpp" />
@@ -293,21 +353,36 @@
     <ClCompile Include="src\client\hooks\config_getter\config_getter_answers.cpp" />
     <ClCompile Include="src\client\patterns\game\packages\package_key_signature_bytes.cpp" />
     <ClCompile Include="src\client\patterns\game\assert_handler\assert_signature_bytes.cpp" />
-    <ClCompile Include="src\client\hooks\network\coordinator\network_call_coordinator.cpp" />
+    <ClCompile Include="src\client\hooks\network\coordinator\network_call_coordinator.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\hooks\network\network_call_quiescence.cpp" />
-    <ClCompile Include="src\client\hooks\network\content_config\coordinator\content_config_call_coordinator.cpp" />
+    <ClCompile Include="src\client\hooks\network\content_config\coordinator\content_config_call_coordinator.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\hooks\network\content_config\gate\content_config_signature_gate.cpp" />
     <ClCompile Include="src\client\hooks\network\content_config\lifecycle\content_config_hook_install.cpp" />
     <ClCompile Include="src\client\hooks\network\content_config\lifecycle\content_config_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\network\content_config\lifecycle\content_config_hook_storage.cpp" />
     <ClCompile Include="src\client\hooks\network\content_config\lifecycle\content_config_hook_uninstall.cpp" />
-    <ClCompile Include="src\client\hooks\network\content_config\request\content_config_get_replacement.cpp" />
-    <ClCompile Include="src\client\hooks\network\content_config\tick\content_config_tick_replacement.cpp" />
+    <ClCompile Include="src\client\hooks\network\content_config\request\content_config_get_replacement.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
+    <ClCompile Include="src\client\hooks\network\content_config\tick\content_config_tick_replacement.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\hooks\network\lifecycle\network_hook_group.cpp" />
     <ClCompile Include="src\client\hooks\network\lifecycle\network_hook_entries.cpp" />
     <ClCompile Include="src\client\hooks\network\lifecycle\network_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\network\network_consumer_registration.cpp" />
-    <ClCompile Include="src\client\hooks\network\platform.cpp" />
+    <ClCompile Include="src\client\hooks\network\platform.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
+    </ClCompile>
     <ClCompile Include="src\client\patterns\registry.cpp" />
     <ClCompile Include="src\client\patterns\image_scan.cpp" />
     <ClCompile Include="src\client\patterns\game_signatures.cpp" />
@@ -318,6 +393,8 @@
     <ClCompile Include="src\client\targets\game\network\network_content_derivation.cpp" />
     <ClCompile Include="src\client\targets\game\resolution\game_target_resolution.cpp" />
     <ClCompile Include="vendor\detours\detours.cpp">
+      <SupportJustMyCode>false</SupportJustMyCode>
+      <BasicRuntimeChecks>Default</BasicRuntimeChecks>
       <WarningLevel>Level3</WarningLevel>
       <TreatWarningAsError>false</TreatWarningAsError>
       <ExcludedFromBuild Condition="'$(SunriseRunClangTidy)'=='true'">true</ExcludedFromBuild>
@@ -566,6 +643,7 @@
     <ClCompile Include="src\state\gameplay\external\replication_world_state.cpp" />
     <ClCompile Include="src\state\activity\activity_session_lookup.cpp" />
     <ClCompile Include="src\state\activity\mission\activity_mission_state.cpp" />
+    <ClCompile Include="src\state\activity\mission\activity_mission_reload.cpp" />
     <ClCompile Include="src\state\activity\mission\activity_mission_publication.cpp" />
     <ClCompile Include="src\state\activity\mission\activity_mission_intents.cpp" />
     <ClCompile Include="src\state\activity\mission\activity_mission_population.cpp" />
@@ -614,6 +692,7 @@
     <ClCompile Include="src\state\content_manifest\scanner\package\content_manifest_package_name.cpp" />
     <ClCompile Include="src\state\content_manifest\content_manifest_row_validation.cpp" />
     <ClCompile Include="src\state\activity_sdk\activity_sdk_catalog.cpp" />
+    <ClCompile Include="src\state\activity_sdk\activity_sdk_identity.cpp" />
     <ClCompile Include="src\state\activity_sdk\activity_sdk_loader.cpp" />
     <ClCompile Include="src\state\activity_sdk\activity_sdk_relation_validation.cpp" />
     <ClCompile Include="src\state\activity_sdk\activity_sdk_resolver.cpp" />
@@ -715,6 +794,8 @@
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_activities.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_artifacts.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_contract.cpp" />
+    <ClCompile Include="src\client\content\activity\activity_sdk_lua_mission_api.cpp" />
+    <ClCompile Include="src\client\content\activity\activity_sdk_lua_mission_luals.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_actor_sequences.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_catalog_contract.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_manifest_contract.cpp" />
@@ -805,9 +886,7 @@
     <ClCompile Include="src\server\activity\mission\mission_script_lua_scene_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_lifetime_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_event_api.cpp" />
-    <ClCompile Include="src\server\activity\mission\mission_script_lua_event_input.cpp" />
-    <ClCompile Include="src\server\activity\mission\mission_script_lua_event_delivery.cpp" />
-    <ClCompile Include="src\server\activity\mission\mission_script_lua_event_derived.cpp" />
+    <ClCompile Include="src\server\activity\mission\mission_script_lua_event_fields.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_definition_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_state_api.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_message_api.cpp" />
@@ -852,11 +931,16 @@
     <ClCompile Include="src\server\activity\mission\mission_script_world_sdk_fields.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_world_sdk_values.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_vm.cpp" />
+    <ClCompile Include="src\server\activity\mission\mission_script_vm_exchange.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_slot_atoms.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_lua_combat_objective.cpp" />
     <ClCompile Include="src\server\activity\activity_sdk_combat_objective.cpp" />
     <ClCompile Include="src\server\activity\activity_sdk_actor_program.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_runtime_attach.cpp" />
+    <ClCompile Include="src\server\activity\mission\mission_script_runtime_reload.cpp" />
+    <ClCompile Include="src\server\studio\studio_command_link.cpp" />
+    <ClCompile Include="src\server\activity\mission\mission_script_sources.cpp" />
+    <ClCompile Include="src\server\activity\mission\mission_script_source_owner.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_runtime_diagnostics.cpp" />
     <ClCompile Include="src\server\activity\mission\mission_script_runtime_feed.cpp" />
     <ClCompile Include="src\server\activity\host_runtime.cpp" />
@@ -876,6 +960,7 @@
     <ClCompile Include="src\server\ui\activity_host\activity_host_panel.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_view.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_mission_view.cpp" />
+    <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_scene_inputs.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_state_pages.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_actor_sequences.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_squad_view.cpp" />
@@ -890,8 +975,6 @@
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_details.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_labels.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_browser.cpp" />
-    <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_view_text.cpp" />
-    <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_symbol_pages.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_world_links.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.cpp" />
@@ -1083,6 +1166,7 @@
     <ClCompile Include="src\client\content\items\packages\package_season_pass_build.cpp" />
     <ClCompile Include="src\client\content\items\packages\package_bounty_build.cpp" />
     <ClCompile Include="src\middleware\content\packages\reader\package_file_locator.cpp" />
+    <ClCompile Include="src\middleware\content\packages\reader\package_overlay.cpp" />
     <ClCompile Include="src\middleware\crypto\aes_gcm_decrypt.cpp" />
     <ClCompile Include="src\middleware\content\manifest\content_manifest_encoder.cpp" />
     <ClCompile Include="src\middleware\content\manifest\manifest_entitlement_encoder.cpp" />
@@ -1195,8 +1279,8 @@
     <ClCompile Include="src\middleware\bap\activity_message\activity_peer_ledger_parser.cpp" />
     <ClCompile Include="src\middleware\bap\activity_message\activity_telemetry_parser.cpp" />
     <ClCompile Include="src\middleware\bap\activity_message\activity_start_activity_parser.cpp" />
-    <ClCompile Include="src\middleware\bap\activity_message\activity_sense_update_parser.cpp" />
     <ClCompile Include="src\middleware\bap\activity_message\activity_sense_update_decoder.cpp" />
+    <ClCompile Include="src\middleware\bap\activity_message\activity_sense_roster_delta.cpp" />
     <ClCompile Include="src\server\bap\encrypted\activity_message\receipts\activity_message_receipts.cpp" />
     <ClCompile Include="src\server\bap\encrypted\activity_message\receipts\activity_incident_grants.cpp" />
     <ClCompile Include="src\state\activity\receipts\activity_receipts.cpp" />
@@ -1266,6 +1350,32 @@
     <ClInclude Include="src\core\threading\sendable.h" />
     <ClInclude Include="src\core\filesystem\temporary_sibling.h" />
     <ClInclude Include="src\core\settings\settings.h" />
+    <ClInclude Include="src\middleware\crypto\sha1.h" />
+    <ClInclude Include="src\middleware\content\packages\merge\merge.h" />
+    <ClInclude Include="src\middleware\content\packages\merge\internal.h" />
+    <ClInclude Include="src\middleware\content\packages\merge\dependency_records.h" />
+    <ClInclude Include="src\middleware\content\packages\table_merge\table_merge.h" />
+    <ClInclude Include="src\middleware\content\packages\table_merge\sidecar.h" />
+    <ClInclude Include="src\middleware\content\packages\table_merge\internal.h" />
+    <ClInclude Include="src\state\mods\definition.h" />
+    <ClInclude Include="src\state\mods\mods_runtime.h" />
+    <ClInclude Include="src\state\mods\config\mod_config.h" />
+    <ClInclude Include="src\state\mods\problem\mod_problem.h" />
+    <ClInclude Include="src\state\mods\manifest\mod_version.h" />
+    <ClInclude Include="src\state\mods\manifest\mod_manifest_parser.h" />
+    <ClInclude Include="src\state\mods\manifest\mod_manifest_bindings.h" />
+    <ClInclude Include="src\state\mods\resolution\mod_resolution.h" />
+    <ClInclude Include="src\state\mods\discovery\mod_discovery.h" />
+    <ClInclude Include="src\state\mods\discovery\mod_directory_listing.h" />
+    <ClInclude Include="src\state\mods\packages\mod_packages.h" />
+    <ClInclude Include="src\state\mods\packages\internal.h" />
+    <ClInclude Include="src\client\patterns\game\package_path\package_path_signature_bytes.h" />
+    <ClInclude Include="src\client\targets\game\package_path.h" />
+    <ClInclude Include="src\client\targets\game\package_path\game_package_path_targets.h" />
+    <ClInclude Include="src\client\hooks\package_path\package_path_redirect.h" />
+    <ClInclude Include="src\core\ui\modules\mods\mods.h" />
+    <ClInclude Include="src\core\ui\modules\mods\internal.h" />
+    <ClInclude Include="src\core\json\reader.h" />
     <ClInclude Include="src\core\settings\parser.h" />
     <ClInclude Include="src\core\settings\client\definition.h" />
     <ClInclude Include="src\core\settings\server\definition.h" />
@@ -1342,6 +1452,7 @@
     <ClInclude Include="src\client\hooks\teleport\internal.h" />
     <ClInclude Include="src\client\hooks\teleport\runtime.h" />
     <ClInclude Include="src\client\hooks\world_objects\world_object_registry.h" />
+    <ClInclude Include="src\client\hooks\world_objects\dynamic_object_registry.h" />
     <ClInclude Include="src\client\hooks\world_objects\internal.h" />
     <ClInclude Include="src\client\ui\movement\movement_panel.h" />
     <ClInclude Include="src\client\runtime\internal.h" />
@@ -1375,6 +1486,8 @@
     <ClInclude Include="src\client\network\consumer.h" />
     <ClInclude Include="src\client\hooking\detour.h" />
     <ClInclude Include="src\client\hooking\detour\transaction\detour_thread_transaction.h" />
+    <ClInclude Include="src\client\hooking\detour\transaction\detour_transaction_limits.h" />
+    <ClInclude Include="src\client\hooking\detour\transaction\detour_transaction_storage.h" />
     <ClInclude Include="src\client\executable\image.h" />
     <ClInclude Include="src\client\hooks\network\investment\investment_patches.h" />
     <ClInclude Include="src\client\hooks\network\runtime.h" />
@@ -1485,6 +1598,7 @@
     <ClInclude Include="src\state\content_manifest\content_manifest_row_validation.h" />
     <ClInclude Include="src\state\activity_sdk\format.h" />
     <ClInclude Include="src\state\activity_sdk\squad_profiles.h" />
+    <ClInclude Include="src\state\activity_sdk\scene_triggers.h" />
     <ClInclude Include="src\state\activity_sdk\identity.h" />
     <ClInclude Include="src\state\activity_sdk\generation\definition.h" />
     <ClInclude Include="src\state\activity_sdk\generation\internal.h" />
@@ -1550,6 +1664,8 @@
     <ClInclude Include="src\state\build_data\runtime\domain_markers.h" />
     <ClInclude Include="src\server\transport\bap_listener.h" />
     <ClInclude Include="src\server\runtime\server_runtime.h" />
+    <ClInclude Include="src\server\studio\studio_command_link.h" />
+    <ClInclude Include="src\server\studio\studio_command_protocol.h" />
     <ClInclude Include="src\server\activity\activity_sdk_scriptable_route.h" />
     <ClInclude Include="src\server\activity\activity_sdk_device_runtime.h" />
     <ClInclude Include="src\server\activity\activity_sdk_lifetime_runtime.h" />
@@ -1572,6 +1688,14 @@
     <ClInclude Include="src\server\activity\mission\mission_script_lua_enum_internal.h" />
     <ClInclude Include="src\server\activity\mission\mission_script_lua_key_internal.h" />
     <ClInclude Include="src\server\activity\mission\mission_script_lua_value_internal.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api_context.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api_events.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api_handles.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api_slot.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api_types.h" />
+    <ClInclude Include="src\server\activity\mission\mission_api_values.h" />
+    <ClInclude Include="src\server\activity\mission\mission_script_lua_dispatch.h" />
     <ClInclude Include="src\server\activity\mission\mission_script_lua_args.h" />
     <ClInclude Include="src\server\activity\mission\mission_script_lua_peer_internal.h" />
     <ClInclude Include="src\server\activity\mission\mission_script_lua_event_internal.h" />
@@ -1613,12 +1737,11 @@
     <ClInclude Include="src\server\ui\activity_host\activity_host_scriptable_browser.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_table_layout.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_tool_window.h" />
-    <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_view_text.h" />
-    <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_symbol_pages.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_mission_text.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_scriptable_world_links.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.h" />
+    <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_scene_inputs.h" />
     <ClInclude Include="src\server\bap\runtime.h" />
     <ClInclude Include="src\server\bap\activity_mission_seed_lease.h" />
     <ClInclude Include="src\server\bap\internal.h" />
@@ -1660,7 +1783,6 @@
     <ClInclude Include="src\server\bap\encrypted\push\snapshot\internal.h" />
     <ClInclude Include="src\server\bap\encrypted\push\snapshot\snapshot.h" />
     <ClInclude Include="src\server\bap\encrypted\push\snapshot\snapshot_storage.h" />
-    <ClInclude Include="src\server\web_service\internal.h" />
     <ClInclude Include="src\server\web_service\internal_actions.h" />
     <ClInclude Include="src\server\web_service\opcode_routes.h" />
     <ClInclude Include="src\server\web_service\web_service_runtime.h" />
@@ -1753,6 +1875,9 @@
     <ClInclude Include="src\middleware\content\packages\named_tags.h" />
     <ClInclude Include="src\middleware\content\packages\named_tags\internal.h" />
     <ClInclude Include="src\middleware\content\packages\package_catalog.h" />
+    <ClInclude Include="src\middleware\content\packages\config_bake.h" />
+    <ClInclude Include="src\middleware\content\packages\config_bake\internal.h" />
+    <ClInclude Include="src\middleware\content\cui\config_scalar.h" />
     <ClInclude Include="src\middleware\content\manifest\encoder.h" />
     <ClInclude Include="src\middleware\encoding\bit_writer.h" />
     <ClInclude Include="src\middleware\encoding\bit_reader.h" />
@@ -1856,6 +1981,7 @@
     <ClInclude Include="src\client\targets\game\packages\game_package_targets.h" />
     <ClInclude Include="src\client\targets\game\packages.h" />
     <ClInclude Include="src\middleware\content\packages\reader\internal.h" />
+    <ClInclude Include="src\middleware\content\packages\reader\overlay.h" />
     <ClInclude Include="src\middleware\content\packages\reader\layout.h" />
     <ClInclude Include="src\middleware\content\packages\reader\reader.h" />
     <ClInclude Include="src\middleware\content\packages\reader\parallel.h" />
@@ -1970,6 +2096,20 @@
     <ClInclude Include="src\middleware\content\packages\reader\locator_cache.h" />
     <ClInclude Include="src\server\bap\encrypted\push\queuez\queuez_push_reporting.h" />
     <ClInclude Include="src\client\diagnostics\module_range.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_link.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_protocol.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_object_link.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_object_protocol.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_cui_link.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_cui_protocol.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_cui_reader.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_pose_capture.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_pose_link.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_pose_protocol.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_pose_scale.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_world_link.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_world_protocol.h" />
+    <ClInclude Include="src\client\diagnostics\studio\studio_world_reader.h" />
     <ClInclude Include="src\client\process\freeze\client_process_freeze.h" />
     <ClInclude Include="src\core\settings\address_text.h" />
     <ClInclude Include="src\core\settings\server\gameplay\definition.h" />
@@ -2053,6 +2193,7 @@
     <ClInclude Include="src\middleware\bap\activity_message\peer_ledger.h" />
     <ClInclude Include="src\middleware\bap\activity_message\telemetry.h" />
     <ClInclude Include="src\middleware\bap\activity_message\start_activity.h" />
+    <ClInclude Include="src\middleware\bap\activity_message\sense_roster_delta.h" />
     <ClInclude Include="src\middleware\bap\activity_message\sense_update.h" />
     <ClInclude Include="src\server\bap\encrypted\activity_message\receipts\activity_message_receipts.h" />
     <ClInclude Include="src\server\bap\encrypted\activity_message\receipts\activity_incident_grants.h" />
@@ -2132,6 +2273,8 @@
     <ClCompile Include="src\client\content\activity\activity_sdk_authored_scene_validation.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_missions.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_lua_missions_catalog_constants.cpp" />
+    <ClCompile Include="src\client\content\activity\activity_sdk_lua_scene_events.cpp" />
+    <ClCompile Include="src\state\activity_sdk\scene_triggers.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_placement_cache.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_native_pack_text.cpp" />
     <ClCompile Include="src\client\content\activity\activity_sdk_policy_inventory_capabilities.cpp" />

+ 5 - 2
Sunrise/resources/default_settings.json

@@ -14,7 +14,8 @@
     },
     "activity_sdk_generation": {
       "lua_declarations": true
-    }
+    },
+    "studio_attach": false
   },
   "steam": {
     "language": "english",
@@ -35,7 +36,9 @@
     },
     "custom_bootflow_textures": true,
     "socket_menu_routing": false,
-    "reveal_lore_books": true
+    "reveal_lore_books": true,
+    "cheats": false,
+    "dump_package_keys": false
   },
   "server": {
     "bap_port": 30974,

+ 3 - 3
Sunrise/resources/resource.h

@@ -33,11 +33,11 @@
 
 /** The four numeric fields of the version resource, in FILEVERSION order. */
 #define SUNRISE_VER_MAJOR 0
-#define SUNRISE_VER_MINOR 5
-#define SUNRISE_VER_PATCH 1
+#define SUNRISE_VER_MINOR 6
+#define SUNRISE_VER_PATCH 0
 #define SUNRISE_VER_BUILD 0
 /** The same version as display text. Windows shows this string, not the four fields. */
-#define SUNRISE_VER_STRING "0.5.1.0"
+#define SUNRISE_VER_STRING "0.6.0.0"
 
 /** SQLite schema, default rows, and upstream notice are embedded in the DLL. */
 #define IDR_INVESTMENT_SCHEMA 106

+ 1 - 4
Sunrise/src/client/activity/mission_launch.cpp

@@ -35,8 +35,6 @@ constexpr auto kMemberRecord = signature<signature_length(kMemberRecordText)>(kM
 constexpr std::size_t kManagerCallOffset = 8;
 /** A near call is its opcode and a 4-byte displacement. */
 constexpr std::size_t kNearCallBytes = 5;
-/** Intel's direct near-call opcode guards a decoded target against layout drift. */
-constexpr std::byte kNearCallOpcode{0xE8};
 
 // Matches the membership-revision test; the exact field offset is what makes it unique.
 constexpr std::string_view kSessionReadyText = "83 B9 6C 08 00 00 00 0F 95 C0 C3";
@@ -395,8 +393,7 @@ bool install() noexcept {
         scan_main_image_unique(kCommitSelection, "mission_launch_commit_selection");
     if (record == nullptr || sessionReady == nullptr || memberReady == nullptr
         || construct == nullptr || valid == nullptr || name == nullptr || clearSite == nullptr
-        || select == nullptr || commit == nullptr || record[kManagerCallOffset] != kNearCallOpcode
-        || clearSite[0] != kNearCallOpcode) {
+        || select == nullptr || commit == nullptr) {
         core::log::write(core::log::Channel::client,
                          core::log::Level::warn,
                          "ev=mission_launch stage=install result=fail reason=target");

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

@@ -47,7 +47,7 @@ struct UniqueTag final {
     UniqueTag match{};
     reader::ScanResult result{};
     if (source.directory.empty()
-        || !reader::scan_class(source.directory, classId, &collect_unique_tag, &match, result)
+        || !reader::scan_class(source, classId, &collect_unique_tag, &match, result)
         || match.count != 1 || result.matches != 1 || match.value == 0) {
         return false;
     }
@@ -234,7 +234,7 @@ bool build(const reader::Source& source,
             }
             pending.rows.push_back(row);
         }
-        if (!validate(pending) || !validate_source(activities, pending)) {
+        if (!validate_source(activities, pending)) {
             return false;
         }
         output = std::move(pending);

+ 10 - 17
Sunrise/src/client/content/activity/activity_sdk_activity_inventory.cpp

@@ -24,7 +24,6 @@ constexpr std::uint32_t kAbsentTag = 0xFFFFFFFFU;
 /** One effective class-scan identity. */
 struct LiveRoot final {
     std::uint32_t tag{};
-    std::uint32_t classId{};
     std::uint32_t patchIndex{};
 };
 
@@ -70,7 +69,7 @@ private:
     }
     auto& rows = *static_cast<std::vector<LiveRoot>*>(opaque);
     try {
-        rows.push_back({entry.tag, 0, entry.patchIndex});
+        rows.push_back({entry.tag, entry.patchIndex});
         return true;
     } catch (...) {
         return false;
@@ -116,12 +115,9 @@ read_u32(std::span<const std::byte> bytes, std::size_t offset, std::uint32_t& ou
     return true;
 }
 
-[[nodiscard]] bool same_name(const ActivityRoot& root, std::string_view name) noexcept {
-    return root.nameStatus == NameStatus::exact && root.nameLength == name.size()
-           && std::equal(root.name.begin(), root.name.begin() + root.nameLength, name.begin());
-}
-
-[[nodiscard]] bool same_name(const ScenarioRoot& root, std::string_view name) noexcept {
+/** @return True when the root's exact selected name equals `name`. */
+template <typename Root>
+[[nodiscard]] bool same_name(const Root& root, std::string_view name) noexcept {
     return root.nameStatus == NameStatus::exact && root.nameLength == name.size()
            && std::equal(root.name.begin(), root.name.begin() + root.nameLength, name.begin());
 }
@@ -186,18 +182,15 @@ template <typename Root>
     }
 }
 
-/** Scans one exact class and stamps the class omitted by the visitor shape. */
-[[nodiscard]] bool scan_live(std::wstring_view directory,
+/** Scans one exact class and requires every match to be copied. */
+[[nodiscard]] bool scan_live(const reader::Source& source,
                              std::uint32_t classId,
                              std::vector<LiveRoot>& rows) noexcept {
     const std::size_t first = rows.size();
     reader::ScanResult result{};
-    if (!reader::scan_class_entries(directory, classId, &collect_live_root, &rows, result)) {
+    if (!reader::scan_class_entries(source, classId, &collect_live_root, &rows, result)) {
         return false;
     }
-    for (std::size_t index = first; index < rows.size(); ++index) {
-        rows[index].classId = classId;
-    }
     return result.matches == rows.size() - first;
 }
 
@@ -484,16 +477,16 @@ bool build(const reader::Source& source,
         std::vector<LiveRoot> liveScenarios{};
         std::vector<NamedCandidate> names{};
         reader::release_caches();
-        if (!scan_live(source.directory, kActivityRootClass, liveActivityRoots)
+        if (!scan_live(source, kActivityRootClass, liveActivityRoots)
             || is_cancelled(cancel, cancelContext)
-            || !scan_live(source.directory, tables::kScenarioClass, liveScenarios)
+            || !scan_live(source, tables::kScenarioClass, liveScenarios)
             || !unique_sorted_live(liveActivityRoots) || !unique_sorted_live(liveScenarios)
             || is_cancelled(cancel, cancelContext)) {
             return false;
         }
         named_tags::DirectoryResult namedResult{};
         if (!named_tags::extract_directory(
-                source.directory, &collect_named_root, &names, namedResult)
+                source.directory, source.installedOnly, &collect_named_root, &names, namedResult)
             || is_cancelled(cancel, cancelContext)) {
             return false;
         }

+ 74 - 73
Sunrise/src/client/content/activity/activity_sdk_actor_rsat_inventory.cpp

@@ -35,17 +35,79 @@ private:
     std::unique_ptr<reader::Scratch> value_{};
 };
 
-/** Releases the process-wide parallel package readers after one inventory build. */
-class ParallelReadOwner final {
-public:
-    ~ParallelReadOwner() noexcept {
-        reader::parallel::release();
+/**
+ * Reads the RSAT blobs and their schema blobs in parallel and copies them into the context.
+ * Kept blobs die with the lease, so each one is copied before this returns.
+ * @param source Package directory and block keys.
+ * @param scratch Reader storage for the class checks.
+ * @param rsatTags RSAT tags to read, sorted.
+ * @param cancel Optional cancellation probe.
+ * @param cancelContext Probe context.
+ * @param context Receives one prefetched row per blob read.
+ * @return False when a read, a class check or a descriptor array fails, or on cancel.
+ */
+[[nodiscard]] bool prefetch_rsats(const reader::Source& source,
+                                  reader::Scratch& scratch,
+                                  std::span<const std::uint32_t> rsatTags,
+                                  CancelProbe cancel,
+                                  void* cancelContext,
+                                  PackageReadContext& context) {
+    const reader::parallel::ScopedRead parallelReads{};
+    std::vector<reader::parallel::Held> held{};
+    if (!reader::parallel::read_kept(source, rsatTags, held)) {
+        return false;
     }
-
-    ParallelReadOwner() = default;
-    ParallelReadOwner(const ParallelReadOwner&) = delete;
-    ParallelReadOwner& operator=(const ParallelReadOwner&) = delete;
-};
+    std::vector<std::uint32_t> schemaTags{};
+    context.prefetched.reserve(held.size());
+    for (const reader::parallel::Held& row : held) {
+        std::uint32_t classId = 0;
+        TypedArray descriptors{};
+        if (!reader::read_tag_class(source, scratch, row.tag, classId) || classId != kActorRsatClass
+            || !typed_array(row.blob,
+                            format::kActorRsatDescriptorArrayOffset,
+                            format::kActorRsatDescriptorClass,
+                            kDescriptorStride,
+                            descriptors)) {
+            return false;
+        }
+        for (std::uint32_t ordinal = 0; ordinal < descriptors.count; ++ordinal) {
+            const std::size_t offset = static_cast<std::size_t>(descriptors.dataOffset)
+                                       + static_cast<std::size_t>(ordinal) * kDescriptorStride;
+            std::uint32_t schemaTag = 0;
+            if (!read_value(row.blob, offset + 4U, schemaTag) || is_absent_tag(schemaTag)) {
+                return false;
+            }
+            schemaTags.push_back(schemaTag);
+        }
+        PrefetchedTag cached{};
+        cached.bytes.assign(row.blob.begin(), row.blob.end());
+        cached.classId = kActorRsatClass;
+        if (!context.prefetched.emplace(row.tag, std::move(cached)).second) {
+            return false;
+        }
+    }
+    std::sort(schemaTags.begin(), schemaTags.end());
+    schemaTags.erase(std::unique(schemaTags.begin(), schemaTags.end()), schemaTags.end());
+    if (is_cancelled(cancel, cancelContext)
+        || !reader::parallel::read_kept(source, schemaTags, held)) {
+        return false;
+    }
+    context.prefetched.reserve(context.prefetched.size() + held.size());
+    for (const reader::parallel::Held& row : held) {
+        std::uint32_t classId = 0;
+        if (!reader::read_tag_class(source, scratch, row.tag, classId)
+            || classId != format::kActorRsatSchemaClass) {
+            return false;
+        }
+        PrefetchedTag cached{};
+        cached.bytes.assign(row.blob.begin(), row.blob.end());
+        cached.classId = format::kActorRsatSchemaClass;
+        if (!context.prefetched.emplace(row.tag, std::move(cached)).second) {
+            return false;
+        }
+    }
+    return true;
+}
 
 } // namespace
 
@@ -340,16 +402,7 @@ bool build_from_tags(std::span<const std::uint32_t> actorTags,
         actorTags, {}, readTag, readContext, cancel, cancelContext, output);
 }
 
-/** Builds the complete current-build inventory for an upstream exact actor tag set. */
-bool build(const reader::Source& source,
-           std::span<const std::uint32_t> actorTags,
-           CancelProbe cancel,
-           void* cancelContext,
-           Snapshot& output) noexcept {
-    return build_with_rsats(source, actorTags, {}, cancel, cancelContext, output);
-}
-
-/** Builds the actor inventory from tags plus already-read RSAT rows. */
+/** Prefetches the sorted installed RSAT rows, then builds the actor inventory from tags. */
 bool build_with_rsats(const reader::Source& source,
                       std::span<const std::uint32_t> actorTags,
                       std::span<const std::uint32_t> rsatTags,
@@ -366,62 +419,10 @@ bool build_with_rsats(const reader::Source& source,
         if (scratch.get() == nullptr) {
             return false;
         }
-        ParallelReadOwner parallelReads{};
         PackageReadContext context{&source, scratch.get()};
-        std::vector<reader::parallel::Held> held{};
-        if (!reader::parallel::read_kept(source, rsatTags, held)) {
-            return false;
-        }
-        std::vector<std::uint32_t> schemaTags{};
-        context.prefetched.reserve(held.size());
-        for (const reader::parallel::Held& row : held) {
-            std::uint32_t classId = 0;
-            TypedArray descriptors{};
-            if (!reader::read_tag_class(source, *scratch.get(), row.tag, classId)
-                || classId != kActorRsatClass
-                || !typed_array(row.blob,
-                                format::kActorRsatDescriptorArrayOffset,
-                                format::kActorRsatDescriptorClass,
-                                kDescriptorStride,
-                                descriptors)) {
-                return false;
-            }
-            for (std::uint32_t ordinal = 0; ordinal < descriptors.count; ++ordinal) {
-                const std::size_t offset = static_cast<std::size_t>(descriptors.dataOffset)
-                                           + static_cast<std::size_t>(ordinal) * kDescriptorStride;
-                std::uint32_t schemaTag = 0;
-                if (!read_value(row.blob, offset + 4U, schemaTag) || is_absent_tag(schemaTag)) {
-                    return false;
-                }
-                schemaTags.push_back(schemaTag);
-            }
-            PrefetchedTag cached{};
-            cached.bytes.assign(row.blob.begin(), row.blob.end());
-            cached.classId = kActorRsatClass;
-            if (!context.prefetched.emplace(row.tag, std::move(cached)).second) {
-                return false;
-            }
-        }
-        std::sort(schemaTags.begin(), schemaTags.end());
-        schemaTags.erase(std::unique(schemaTags.begin(), schemaTags.end()), schemaTags.end());
-        if (is_cancelled(cancel, cancelContext)
-            || !reader::parallel::read_kept(source, schemaTags, held)) {
+        if (!prefetch_rsats(source, *scratch.get(), rsatTags, cancel, cancelContext, context)) {
             return false;
         }
-        context.prefetched.reserve(context.prefetched.size() + held.size());
-        for (const reader::parallel::Held& row : held) {
-            std::uint32_t classId = 0;
-            if (!reader::read_tag_class(source, *scratch.get(), row.tag, classId)
-                || classId != format::kActorRsatSchemaClass) {
-                return false;
-            }
-            PrefetchedTag cached{};
-            cached.bytes.assign(row.blob.begin(), row.blob.end());
-            cached.classId = format::kActorRsatSchemaClass;
-            if (!context.prefetched.emplace(row.tag, std::move(cached)).second) {
-                return false;
-            }
-        }
         std::vector<SequenceTableSource> sequenceTables;
         if (!sequence_inventory::scan_tables(source, sequenceTables)) {
             return false;

+ 4 - 11
Sunrise/src/client/content/activity/activity_sdk_actor_rsat_inventory.h

@@ -42,7 +42,7 @@ struct Text final {
     std::uint16_t length{};
 };
 
-/** One exact format-v12 actor-class row before its id is linked. */
+/** One exact actor-class row before its id is linked. */
 struct ActorClass final {
     Text id{};
     std::uint32_t definitionTag{};
@@ -60,7 +60,7 @@ struct ActorClass final {
     std::array<std::int8_t, 4> authoredSpawnProfile{};
 };
 
-/** One exact format-v12 RSAT descriptor row before its id is linked. */
+/** One exact RSAT descriptor row before its id is linked. */
 struct RsatDescriptor final {
     Text id{};
     std::uint32_t actorClassIndex{};
@@ -79,7 +79,7 @@ struct RsatDescriptor final {
     std::array<std::byte, state::activity_sdk::format::kRsatDescriptorRawRowSize> rawRow{};
 };
 
-/** One exact format-v12 RSAT schema row before its id is linked. */
+/** One exact RSAT schema row before its id is linked. */
 struct RsatSchema final {
     Text id{};
     std::uint32_t schemaTag{};
@@ -96,7 +96,7 @@ struct RsatSchema final {
     state::activity_sdk::format::Range fields{};
 };
 
-/** One exact format-v12 RSAT field row. */
+/** One exact RSAT field row. */
 struct RsatField final {
     std::array<std::byte, state::activity_sdk::format::kRsatSchemaFieldRawRowSize> rawRow{};
 };
@@ -269,13 +269,6 @@ build_from_tags_and_rsats(std::span<const std::uint32_t> actorTags,
                           Snapshot& output,
                           std::span<const SequenceTableSource> sequenceTables = {}) noexcept;
 
-/** Builds a structurally complete installed inventory for an upstream exact actor tag set. */
-[[nodiscard]] bool build(const middleware::content::packages::reader::Source& source,
-                         std::span<const std::uint32_t> actorTags,
-                         CancelProbe cancel,
-                         void* cancelContext,
-                         Snapshot& output) noexcept;
-
 /** Builds actor joins against one complete sorted installed RSAT tag set. */
 [[nodiscard]] bool build_with_rsats(const middleware::content::packages::reader::Source& source,
                                     std::span<const std::uint32_t> actorTags,

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_actor_rsat_inventory_internal.h

@@ -61,7 +61,7 @@ struct BuildState final {
     std::unordered_map<std::uint32_t, SequenceOwner> sequenceOwners{};
 };
 
-/** Actual reader callback context. */
+/** One package row already read into memory, with its physical class. */
 struct PrefetchedTag final {
     std::vector<std::byte> bytes{};
     std::uint32_t classId{};

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_actor_sequences.h

@@ -408,7 +408,7 @@ inline bool scan_tables(const reader::Source& source, std::vector<SequenceTableS
                            format::kActorSequenceLocalDefinitionClass}) {
         ScanContext context{&output, cls};
         reader::ScanResult result{};
-        if (!reader::scan_class(source.directory, cls, &visit_table, &context, result)) {
+        if (!reader::scan_class(source, cls, &visit_table, &context, result)) {
             return false;
         }
     }

+ 89 - 2
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.cpp

@@ -4,8 +4,10 @@
 #include <array>
 #include <cstdio>
 #include <cstring>
+#include <span>
 #include <unordered_map>
 #include <utility>
+#include <vector>
 
 #include "../../../core/logging/log.h"
 #include "../../../middleware/content/packages/tables/slot_descriptor_reader.h"
@@ -48,7 +50,7 @@ read_value(std::span<const std::byte> blob, std::size_t offset, Value& output) n
     return true;
 }
 
-/** Names the guard that refused one type-42 sensor, so one run can say why an idle has no state. */
+/** Logs the outcome of one type-42 sensor edge: the refusing guard, or `ready`. */
 void log_performance_edge(const squad::DescriptorFact& descriptor, const char* result) noexcept {
     std::array<char, 176> line{};
     const int written =
@@ -145,6 +147,71 @@ read_gate_table(std::span<const std::byte> blob, std::size_t& rows, std::uint64_
     return rows <= blob.size() && count <= (blob.size() - rows) / format::kAuthoredSceneGateRowSize;
 }
 
+/**
+ * Collects every port a graph block fires, ascending and unique.
+ * @param blob Graph bytes.
+ * @param graphTag Tag every connection list of the graph opens with.
+ * @param gateLists Offsets of the gates' own connection lists, which name waiting blocks instead.
+ * @param output Receives the fired ports.
+ * @return False when a connection list does not fit the graph.
+ */
+[[nodiscard]] bool fired_ports(std::span<const std::byte> blob,
+                               std::uint32_t graphTag,
+                               std::span<const std::size_t> gateLists,
+                               std::vector<std::uint32_t>& output) {
+    output.clear();
+    for (std::size_t offset = 0; contains(
+             blob, offset, format::kAuthoredSceneConnectionSingleOffset + sizeof(std::int32_t));
+         offset += sizeof(std::uint32_t)) {
+        std::uint32_t owner = 0;
+        std::uint32_t classId = 0;
+        if (!read_value(blob, offset, owner) || owner != graphTag
+            || !read_value(blob, offset + 4U, classId)
+            || classId != format::kAuthoredSceneConnectionClass
+            || std::find(gateLists.begin(), gateLists.end(), offset) != gateLists.end()) {
+            continue;
+        }
+        std::int32_t single = 0;
+        std::uint64_t count = 0;
+        std::int64_t relative = 0;
+        const std::size_t field = offset + format::kAuthoredSceneConnectionListOffset;
+        if (!read_value(blob, offset + format::kAuthoredSceneConnectionSingleOffset, single)
+            || !read_value(blob, offset + format::kAuthoredSceneConnectionCountOffset, count)
+            || !read_value(blob, field, relative)) {
+            return false;
+        }
+        // One target sits inline; -1 means the list carries them.
+        if (single != -1) {
+            output.push_back(static_cast<std::uint32_t>(single));
+            continue;
+        }
+        if (count == 0) {
+            continue;
+        }
+        const auto header = static_cast<std::int64_t>(field) + relative;
+        std::uint64_t repeated = 0;
+        std::uint32_t elementClass = 0;
+        if (header < 0 || !read_value(blob, static_cast<std::size_t>(header), repeated)
+            || repeated != count
+            || !read_value(blob, static_cast<std::size_t>(header) + 8U, elementClass)
+            || elementClass != format::kPackageU32ArrayClass
+            || count > blob.size() / sizeof(std::uint32_t)) {
+            return false;
+        }
+        const std::size_t values = static_cast<std::size_t>(header) + 16U;
+        for (std::uint64_t index = 0; index < count; ++index) {
+            std::uint32_t port = 0;
+            if (!read_value(blob, values + static_cast<std::size_t>(index) * 4U, port)) {
+                return false;
+            }
+            output.push_back(port);
+        }
+    }
+    std::sort(output.begin(), output.end());
+    output.erase(std::unique(output.begin(), output.end()), output.end());
+    return true;
+}
+
 /** Reads one tag once and retains the physical class beside its bytes. */
 [[nodiscard]] bool package_row(squad::TagReader reader,
                                void* readerContext,
@@ -278,6 +345,7 @@ read_gate_table(std::span<const std::byte> blob, std::size_t& rows, std::uint64_
         log_scene_graph(descriptor, graphTag, "capacity", core::log::Level::warn);
         return true;
     }
+    std::vector<std::size_t> gateLists;
     for (std::uint64_t gate = 0; gate < count; ++gate) {
         const std::size_t tableRow =
             rows + static_cast<std::size_t>(gate) * format::kAuthoredSceneGateRowSize;
@@ -299,10 +367,12 @@ read_gate_table(std::span<const std::byte> blob, std::size_t& rows, std::uint64_
         std::uint32_t bodyClass = 0;
         std::uint32_t key = 0;
         std::int32_t ordinal = 0;
+        std::uint64_t listeners = 0;
         if (!read_value(blob, element, owner)
             || !read_value(blob, element + format::kAuthoredSceneGateClassOffset, bodyClass)
             || !read_value(blob, element + format::kAuthoredSceneGateKeyOffset, key)
             || !read_value(blob, element + format::kAuthoredSceneGateOrdinalOffset, ordinal)
+            || !read_value(blob, element + format::kAuthoredSceneGateListenerCountOffset, listeners)
             || owner != graphTag || bodyClass != format::kAuthoredSceneGateRowClass || key == 0
             || key == format::kAbsentIndex) {
             output.resize(start);
@@ -320,8 +390,25 @@ read_gate_table(std::span<const std::byte> blob, std::size_t& rows, std::uint64_
         row.gateOffset = static_cast<std::uint32_t>(element);
         row.ordinal = ordinal;
         row.key = key;
-        row.flags = format::kAuthoredSceneEventKeyExact;
+        row.flags = format::kAuthoredSceneEventKeyExact
+                    | (listeners != 0 ? format::kAuthoredSceneEventKeyListened : 0U);
         output.push_back(row);
+        gateLists.push_back(element + format::kAuthoredSceneGateConnectionOffset);
+    }
+    std::vector<std::uint32_t> fired;
+    if (!fired_ports(blob, graphTag, gateLists, fired)) {
+        output.resize(start);
+        log_scene_graph(descriptor, graphTag, "connections", core::log::Level::warn);
+        return true;
+    }
+    // Port k is gate ordinal k, so a block firing port k fires that gate.
+    for (std::size_t index = start; index < output.size(); ++index) {
+        EventKey& row = output[index];
+        if (row.ordinal >= 0
+            && std::binary_search(
+                fired.begin(), fired.end(), static_cast<std::uint32_t>(row.ordinal))) {
+            row.flags |= format::kAuthoredSceneEventKeyFired;
+        }
     }
     return true;
 }

+ 2 - 2
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.h

@@ -24,7 +24,7 @@ struct Text final {
     std::uint16_t length{};
 };
 
-/** One exact format-v12 type-43 package resource before its ID is linked. */
+/** One exact type-43 package resource before its ID is linked. */
 struct Resource final {
     Text id{};
     std::uint32_t slotIndex{};
@@ -50,7 +50,7 @@ struct EventKey final {
     std::uint32_t reserved{};
 };
 
-/** One exact format-v12 same-object scene-to-squad edge before its ID is linked. */
+/** One exact same-object scene-to-squad edge before its ID is linked. */
 struct SquadEdge final {
     Text id{};
     std::uint32_t sceneSlotIndex{};

+ 9 - 2
Sunrise/src/client/content/activity/activity_sdk_authored_scene_validation.cpp

@@ -79,6 +79,12 @@ constexpr std::size_t kHashCombineConstant = 0x9E3779B9U;
                 && schema.authSchema == format::kTaskAuthSchema) {
                 projectedOwners[slot.objectIndex] = true;
             }
+            if (slot.slotType == format::kPerformanceSlotType && schema.exact
+                && schema.componentClass == format::kPerformanceComponentClass
+                && schema.senseSchema == format::kAbsentIndex
+                && schema.authSchema == format::kPerformanceAuthSchema) {
+                projectedOwners[slot.objectIndex] = true;
+            }
         }
         for (const squad::DescriptorFact& descriptor : facts.descriptors) {
             std::string expected{};
@@ -243,8 +249,9 @@ bool validate(const topology::Snapshot& topology,
         Text expectedId{};
         if (descriptor == nullptr || resource->resourceTag != row.resourceTag || row.graphTag == 0
             || row.graphTag == format::kAbsentIndex || row.key == 0
-            || row.key == format::kAbsentIndex || row.flags != format::kAuthoredSceneEventKeyExact
-            || row.reserved != 0
+            || row.key == format::kAbsentIndex
+            || (row.flags & format::kAuthoredSceneEventKeyExact) == 0
+            || (row.flags & ~format::kAuthoredSceneEventKeyFlagMask) != 0 || row.reserved != 0
             || !event_key_id(topology, *descriptor, row.graphTag, row.gateOffset, expectedId)
             || expectedId.value != row.id.value || expectedId.length != row.id.length
             || (index != 0

+ 11 - 12
Sunrise/src/client/content/activity/activity_sdk_behavior_inventory.cpp

@@ -414,11 +414,10 @@ struct BuildOwner final {
     if (count == 0 || relative == 0) {
         return false;
     }
-    const std::int64_t headerValue = static_cast<std::int64_t>(relativeField) + relative;
-    if (headerValue < 4) {
+    std::size_t header = 0;
+    if (!relative_target(bytes, relativeField, header) || header < sizeof(std::uint32_t)) {
         return false;
     }
-    const std::size_t header = static_cast<std::size_t>(headerValue);
     std::uint32_t marker = 0;
     std::uint64_t repeated = 0;
     std::uint32_t actualClass = 0;
@@ -521,9 +520,10 @@ struct BuildOwner final {
     if (actorTags.empty()) {
         return true;
     }
+    // Held to the end, since the kept blobs are read up to the owner join.
+    const parallel::ScopedRead read{};
     std::vector<parallel::Held> actors{};
     if (!parallel::read_kept(source, actorTags, actors)) {
-        parallel::release();
         return false;
     }
     output.unreadActors = static_cast<std::uint32_t>(actorTags.size() - actors.size());
@@ -531,7 +531,6 @@ struct BuildOwner final {
     std::vector<std::uint32_t> configTags{};
     for (const parallel::Held& actor : actors) {
         if (cancel != nullptr && cancel(cancelContext)) {
-            parallel::release();
             return false;
         }
         const auto actorTag = std::lower_bound(actorTags.begin(), actorTags.end(), actor.tag);
@@ -555,7 +554,6 @@ struct BuildOwner final {
     configTags.erase(std::unique(configTags.begin(), configTags.end()), configTags.end());
     std::vector<parallel::Held> configs{};
     if (!parallel::read_kept(source, configTags, configs)) {
-        parallel::release();
         return false;
     }
     output.unreadConfigs = static_cast<std::uint32_t>(configTags.size() - configs.size());
@@ -592,7 +590,6 @@ struct BuildOwner final {
             }
         }
     }
-    parallel::release();
     std::sort(
         output.owners.begin(), output.owners.end(), [](const Owner& first, const Owner& second) {
             return std::tie(first.programRow,
@@ -703,7 +700,7 @@ bool build(const reader::Source& source,
     try {
         std::vector<std::uint32_t> tags{};
         reader::ScanResult scan{};
-        if (!reader::scan_class(source.directory, kRootClass, &collect_tag, &tags, scan)
+        if (!reader::scan_class(source, kRootClass, &collect_tag, &tags, scan)
             || (cancel != nullptr && cancel(cancelContext))) {
             output = {};
             return false;
@@ -715,12 +712,14 @@ bool build(const reader::Source& source,
         }
         std::vector<std::vector<ParsedProgram>> workers(parallel::worker_count());
         BuildContext context{cancel, cancelContext, &workers};
-        if (!parallel::read_tags(source, tags, &collect, &context)
-            || context.failed.load(std::memory_order_relaxed)) {
-            parallel::release();
+        bool read = false;
+        {
+            const parallel::ScopedRead lease{};
+            read = parallel::read_tags(source, tags, &collect, &context);
+        }
+        if (!read || context.failed.load(std::memory_order_relaxed)) {
             return false;
         }
-        parallel::release();
         std::vector<ParsedProgram> parsed{};
         for (auto& worker : workers) {
             for (ParsedProgram& row : worker) {

+ 8 - 7
Sunrise/src/client/content/activity/activity_sdk_decode_plans.cpp

@@ -13,6 +13,7 @@
 
 #include "../../../core/filesystem/temporary_sibling.h"
 #include "../../../core/logging/log.h"
+#include "../../../state/gameplay/external/rsat_decode_plans.h"
 #include "activity_sdk_decode_plans_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::decode_plans {
@@ -25,9 +26,8 @@ constexpr std::string_view kMagic = "SRRSATP2";
 constexpr std::uint32_t kVersion = 2;
 constexpr std::uint32_t kClientBuild = 86657;
 constexpr std::size_t kHeaderSize = 136;
-/** Identity of this builder in the evidence; the Python reference hashes the same text. */
+/** Identity of this builder; its text is hashed into the evidence. */
 constexpr std::string_view kGeneratorId = "sunrise-rsat-decode-plans-builder-1";
-constexpr std::wstring_view kManifestSuffix = L".json";
 constexpr std::wstring_view kTemporarySuffix = L".tmp";
 /** Per-pair refusals logged before the rest are only counted. */
 constexpr std::uint32_t kLoggedErrorLimit = 16;
@@ -80,7 +80,7 @@ void put_field(std::vector<std::byte>& bytes, const Field& field) {
 }
 
 void append_hex(std::string& text, std::span<const std::byte> bytes) {
-    // Digests are lowercase hex in the evidence and the manifest, as the reference writes them.
+    // Digests are lowercase hex in the evidence and the manifest.
     constexpr std::string_view digits = "0123456789abcdef";
     for (const std::byte value : bytes) {
         text.push_back(digits[static_cast<std::size_t>(value) >> 4U]);
@@ -94,7 +94,7 @@ void append_handle(std::string& text, std::uint32_t handle) {
     text.append(buffer.data(), written > 0 ? static_cast<std::size_t>(written) : 0U);
 }
 
-/** Compact JSON of the evidence, in key order, exactly as the reference hashes it. */
+/** Compact JSON of the evidence in key order; its exact bytes are hashed into the fingerprint. */
 void append_evidence(std::string& text,
                      const Digest& executable,
                      const Digest& generator,
@@ -116,7 +116,7 @@ void append_evidence(std::string& text,
     text.append("}}");
 }
 
-/** Two-space indented manifest in key order, exactly as the reference writes it. */
+/** Two-space indented manifest in key order. */
 void append_manifest(std::string& text,
                      const Digest& sdk,
                      const Digest& fingerprint,
@@ -258,7 +258,7 @@ void log_pair_error(std::uint32_t component, std::uint32_t schema, const char* d
     return request.cancel != nullptr && request.cancel(request.cancelContext);
 }
 
-/** Compiles every pair; refused pairs are counted and left out, as the reference does. */
+/** Compiles every pair; refused pairs are counted and left out. */
 [[nodiscard]] Status
 compile_pairs(const Request& request, Store& store, std::vector<Plan>& plans, Result& output) {
     std::set<std::pair<std::uint32_t, std::uint32_t>> pairs;
@@ -427,7 +427,8 @@ struct ScratchOwner final {
     append_manifest(
         manifest, sdk, fingerprint, executable, generator, payloadDigest, store.package_hashes());
     const std::wstring manifestPath =
-        std::wstring(request.cachePath) + std::wstring(kManifestSuffix);
+        std::wstring(request.cachePath)
+        + std::wstring(state::gameplay::rsat_decode_plans::kManifestSuffix);
     if (!write_file(request.cachePath, cache)
         || !write_file(
             manifestPath,

+ 1 - 10
Sunrise/src/client/content/activity/activity_sdk_decode_plans_compiler.cpp

@@ -12,18 +12,9 @@ constexpr std::uint64_t kNodeSize = 40;
 /** A dynamic container places its payload 16 bytes after a 16-aligned header. */
 constexpr std::uint64_t kDynamicAlignment = 16;
 constexpr std::uint64_t kDynamicHeader = 16;
-/** Serialized rows align to the largest power of two below their size, capped at 16. */
+/** Serialized rows align to the next power of two at or above their size, capped at 16. */
 constexpr std::uint64_t kAlignmentCap = 8;
 
-template <typename T>
-[[nodiscard]] bool read_at(std::span<const std::byte> bytes, std::uint64_t at, T& value) noexcept {
-    if (at > bytes.size() || bytes.size() - at < sizeof(T)) {
-        return false;
-    }
-    std::memcpy(&value, bytes.data() + static_cast<std::size_t>(at), sizeof(T));
-    return true;
-}
-
 /** Rounds up to a power-of-two alignment; zero or a non power of two is refused. */
 [[nodiscard]] bool
 align(std::uint64_t value, std::uint64_t alignment, std::uint64_t& output) noexcept {

+ 64 - 12
Sunrise/src/client/content/activity/activity_sdk_decode_plans_image.cpp

@@ -1,13 +1,27 @@
 #include <algorithm>
+#include <array>
 #include <cstring>
 #include <limits>
 #include <string_view>
 
+#include "../../patterns/registry.h"
+#include "../../patterns/signature_text.h"
 #include "activity_sdk_decode_plans_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::decode_plans::internal {
 namespace {
 
+// Matches the codec walk's primitive size lookup: movsx rcx, r9b; lea rdx, [table];
+// mov edx, [rdx+rcx*4]. The table address comes from the wildcarded lea operand.
+constexpr std::string_view kPrimitiveSizeLoadText =
+    "49 0F BE C9 48 8D 15 ? ? ? ? 8B 14 8A 03 D3 3B D6";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kPrimitiveSizeLoad =
+    patterns::signature<patterns::signature_length(kPrimitiveSizeLoadText)>(kPrimitiveSizeLoadText);
+/** The lea operand and the end of the lea, from the start of the match. */
+constexpr std::size_t kPrimitiveSizeOperand = 7;
+constexpr std::size_t kPrimitiveSizeNext = 11;
+
 /** PE32+ header layout the slot index depends on. */
 constexpr std::size_t kNewHeaderOffsetAt = 0x3C;
 constexpr std::size_t kSectionCountAt = 6;
@@ -19,6 +33,7 @@ constexpr std::size_t kSizeOfImageAt = 56;
 /** The headers and section table of a mapped module fit in its first page. */
 constexpr std::size_t kPeHeaderSpan = 4096;
 constexpr std::size_t kSectionRowSize = 40;
+constexpr std::size_t kSectionCharacteristicsAt = 36;
 constexpr std::size_t kSectionLimit = 96;
 /** A tagdefs slot is 32 bytes: handle, zero pad, record pointer, 16 more bytes. */
 constexpr std::size_t kSlotSize = 32;
@@ -27,15 +42,6 @@ constexpr std::size_t kSlotStep = 8;
 constexpr std::uint32_t kHandleSpaceMask = 0xFFFF0000U;
 constexpr std::uint32_t kHandleSpace = 0x80800000U;
 
-template <typename T>
-[[nodiscard]] bool read_at(std::span<const std::byte> bytes, std::size_t at, T& value) noexcept {
-    if (at > bytes.size() || bytes.size() - at < sizeof(T)) {
-        return false;
-    }
-    std::memcpy(&value, bytes.data() + at, sizeof(T));
-    return true;
-}
-
 } // namespace
 
 /** Releases the view, the mapping and the file in that order. */
@@ -88,7 +94,7 @@ Status Image::open(const wchar_t* path, const void* module) noexcept {
     data_ = module == nullptr ? view_ : static_cast<const std::byte*>(module);
     size_ = module == nullptr ? viewSize_ : kPeHeaderSpan;
     try {
-        if (!parse_headers(module != nullptr) || !index_slots()) {
+        if (!parse_headers(module != nullptr) || !index_slots() || !resolve_primitive_sizes()) {
             return Status::executableInvalid;
         }
     } catch (...) {
@@ -145,9 +151,11 @@ bool Image::parse_headers(bool mapped) noexcept {
         std::uint32_t virtualAddress = 0;
         std::uint32_t rawSize = 0;
         std::uint32_t rawOffset = 0;
+        std::uint32_t characteristics = 0;
         if (row > image.size() || image.size() - row < kSectionRowSize
             || !read_at(image, row + 8, virtualSize) || !read_at(image, row + 12, virtualAddress)
-            || !read_at(image, row + 16, rawSize) || !read_at(image, row + 20, rawOffset)) {
+            || !read_at(image, row + 16, rawSize) || !read_at(image, row + 20, rawOffset)
+            || !read_at(image, row + kSectionCharacteristicsAt, characteristics)) {
             return false;
         }
         const std::string_view name(reinterpret_cast<const char*>(image.data() + row), 8);
@@ -157,7 +165,8 @@ bool Image::parse_headers(bool mapped) noexcept {
                         virtualSize,
                         mapped ? virtualAddress : rawOffset,
                         mapped ? virtualSize : rawSize,
-                        trimmed == ".data"};
+                        trimmed == ".data",
+                        (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0};
         if (trimmed == ".vmp0" && !vmp0) {
             vmp0 = true;
             vmp0Low_ = imageBase_ + virtualAddress;
@@ -200,6 +209,49 @@ bool Image::index_slots() noexcept {
     return true;
 }
 
+/**
+ * Finds the primitive size table through the one code site that indexes it.
+ * @return False when the site is missing or not unique in the executable sections.
+ */
+bool Image::resolve_primitive_sizes() noexcept {
+    const std::span<const std::byte> image = bytes();
+    std::array<patterns::ImageRange, kSectionLimit> ranges{};
+    std::array<const Section*, kSectionLimit> owners{};
+    std::size_t count = 0;
+    for (const Section& section : sections_) {
+        if (!section.code || section.rawSize == 0 || section.rawOffset >= image.size()) {
+            continue;
+        }
+        const std::size_t length =
+            static_cast<std::size_t>((std::min)(section.rawSize, image.size() - section.rawOffset));
+        // The scan only reads; the range type is mutable because hook targets share it.
+        auto* const start = const_cast<std::byte*>(image.data() + section.rawOffset);
+        ranges[count] = patterns::ImageRange{std::span(start, length)};
+        owners[count++] = &section;
+    }
+    const std::array definitions{patterns::Pattern{"primitive_size_load", kPrimitiveSizeLoad}};
+    std::array<patterns::Match, 1> matches{};
+    if (!patterns::resolve_all(std::span(ranges.data(), count), definitions, matches)
+        || matches[0].status != patterns::MatchStatus::unique) {
+        return false;
+    }
+    for (std::size_t index = 0; index < count; ++index) {
+        const std::span<std::byte> range = ranges[index].bytes;
+        if (matches[0].address < range.data()
+            || matches[0].address >= range.data() + range.size()) {
+            continue;
+        }
+        const auto inside = static_cast<std::uint64_t>(matches[0].address - range.data());
+        std::int32_t displacement = 0;
+        std::memcpy(&displacement, matches[0].address + kPrimitiveSizeOperand, sizeof displacement);
+        const std::uint64_t site = imageBase_ + owners[index]->virtualAddress + inside;
+        primitiveSizes_ = site + kPrimitiveSizeNext
+                          + static_cast<std::uint64_t>(static_cast<std::int64_t>(displacement));
+        return true;
+    }
+    return false;
+}
+
 /**
  * Maps one virtual address to a file offset through the section table.
  * @param address Virtual address relative to the image base in the header.

+ 3 - 38
Sunrise/src/client/content/activity/activity_sdk_decode_plans_index.cpp

@@ -1,12 +1,8 @@
-#include <array>
-
 #include "activity_sdk_decode_plans_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::decode_plans::internal {
 namespace {
 
-/** Candidate offsets of the block chain marker inside a reflection record. */
-constexpr std::array<std::uint32_t, 4> kMarkerOffsets{0x58, 0x60, 0x68, 0x70};
 /** Codec block: bound handle at +0x10, serialized size at +0x18, field rows from +0x28. */
 constexpr std::uint64_t kCodecHandleAt = 0x10;
 constexpr std::uint64_t kCodecSerializedAt = 0x18;
@@ -14,38 +10,6 @@ constexpr std::uint64_t kCodecFieldsAt = 0x28;
 /** Bit offset of one field inside its 40-byte row. */
 constexpr std::uint64_t kFieldBitAt = 8;
 
-/**
- * Sizes one reflection block from its class and row count.
- * @param size Receives header plus rows.
- * @return False for an unknown block class.
- */
-[[nodiscard]] bool
-block_size(std::uint32_t kind, std::uint32_t count, std::uint64_t& size) noexcept {
-    std::uint64_t header = 0;
-    std::uint64_t stride = 0;
-    switch (kind) {
-    case kCodecBlock:
-        header = 0x28;
-        stride = 0x28;
-        break;
-    case kPropertyBlock:
-        header = 0x20;
-        stride = 0x0C;
-        break;
-    case kMethodBlock:
-        header = 0x10;
-        stride = 0x08;
-        break;
-    case kTailBlock:
-        header = 0x18;
-        break;
-    default:
-        return false;
-    }
-    size = header + stride * count;
-    return true;
-}
-
 } // namespace
 
 /**
@@ -84,16 +48,17 @@ bool build_codec_index(const Image& image,
             while (cursor < length) {
                 std::uint32_t kind = 0;
                 std::uint32_t count = 0;
-                std::uint64_t size = 0;
+                BlockLayout layout{};
                 if (!image.u32(address + cursor + 4, kind)
                     || !image.u32(address + cursor + 8, count)) {
                     error = "record_read";
                     return false;
                 }
-                if (!block_size(kind, count, size)) {
+                if (!block_layout(kind, layout)) {
                     error = "reflection_block_unknown";
                     return false;
                 }
+                const std::uint64_t size = layout.header + std::uint64_t{layout.stride} * count;
                 if (cursor + size > length) {
                     error = "reflection_block_extent";
                     return false;

+ 51 - 10
Sunrise/src/client/content/activity/activity_sdk_decode_plans_internal.h

@@ -2,8 +2,10 @@
 
 #include <Windows.h>
 
+#include <array>
 #include <cstddef>
 #include <cstdint>
+#include <cstring>
 #include <map>
 #include <span>
 #include <vector>
@@ -45,17 +47,55 @@ inline constexpr std::uint8_t kKindContainer = 11;
 /** Field type code of a nested schema; codes above 63 have no primitive size. */
 inline constexpr std::uint8_t kTypeNested = 1;
 inline constexpr std::uint8_t kTypeLimit = 64;
-/** RVA of the executable's 64-entry primitive size table (VA 0x7FF7438B5780). */
-inline constexpr std::uint64_t kPrimitiveSizeTableRva = 0x1BF5780U;
 /** Capacities the runtime decoder shares with the compiler. */
 inline constexpr std::size_t kMaximumDepth = 32;
 inline constexpr std::size_t kMaximumSteps = 65536;
 inline constexpr std::uint32_t kMaximumRepeat = 16384;
 inline constexpr std::uint32_t kMaximumSectionCount = 65536;
 inline constexpr std::uint64_t kMaximumArrayCount = 0x100000U;
-/** Stride of one selection child row and one codec field descriptor. */
-inline constexpr std::size_t kChildRowSize = 40;
+/** Stride of one codec field descriptor. */
 inline constexpr std::size_t kFieldRowSize = 40;
+/** Candidate offsets of the block chain marker inside a reflection record. */
+inline constexpr std::array<std::uint32_t, 4> kMarkerOffsets{0x58, 0x60, 0x68, 0x70};
+
+/** Header size and row stride of each reflection block class. */
+struct BlockLayout final {
+    std::uint32_t header{};
+    std::uint32_t stride{};
+};
+
+/**
+ * Looks up the header size and row stride of one reflection block class.
+ * @return False for an unknown block class.
+ */
+[[nodiscard]] inline bool block_layout(std::uint32_t kind, BlockLayout& output) noexcept {
+    switch (kind) {
+    case kCodecBlock:
+        output = {0x28, 0x28};
+        return true;
+    case kPropertyBlock:
+        output = {0x20, 0x0C};
+        return true;
+    case kMethodBlock:
+        output = {0x10, 0x08};
+        return true;
+    case kTailBlock:
+        output = {0x18, 0x00};
+        return true;
+    default:
+        return false;
+    }
+}
+
+/** Copies one unaligned value. @return False when it does not fit inside `bytes`. */
+template <typename T>
+[[nodiscard]] bool read_at(std::span<const std::byte> bytes, std::size_t at, T& value) noexcept {
+    if (at > bytes.size() || bytes.size() - at < sizeof(T)) {
+        return false;
+    }
+    std::memcpy(&value, bytes.data() + at, sizeof(T));
+    return true;
+}
 
 /** One codec field descriptor in reflection or package layout. */
 struct Field final {
@@ -76,7 +116,6 @@ struct Field final {
 /** One field codec block. */
 struct Codec final {
     bool present{};
-    std::uint32_t handle{};
     std::uint32_t original{};
     std::uint32_t serialized{};
     std::uint32_t flags{};
@@ -94,7 +133,6 @@ struct Property final {
 /** One reflection or package metadata record. */
 struct Metadata final {
     bool package{};
-    std::uint32_t handle{};
     std::uint32_t size{};
     std::uint8_t alignment{};
     std::uint8_t kind{};
@@ -132,13 +170,14 @@ public:
     [[nodiscard]] std::span<const std::byte> file() const noexcept;
     /** @return The bytes the reflection reads go through: the file or the mapped module. */
     [[nodiscard]] std::span<const std::byte> bytes() const noexcept;
-    [[nodiscard]] std::uint64_t image_base() const noexcept {
-        return imageBase_;
-    }
     /** Bound handle to reflection record address, first slot wins. */
     [[nodiscard]] const std::map<std::uint32_t, std::uint64_t>& slots() const noexcept {
         return slots_;
     }
+    /** Address of the 64-entry primitive size table, one u32 per field type code. */
+    [[nodiscard]] std::uint64_t primitive_sizes() const noexcept {
+        return primitiveSizes_;
+    }
     [[nodiscard]] bool u8(std::uint64_t address, std::uint8_t& value) const noexcept;
     [[nodiscard]] bool u16(std::uint64_t address, std::uint16_t& value) const noexcept;
     [[nodiscard]] bool u32(std::uint64_t address, std::uint32_t& value) const noexcept;
@@ -152,12 +191,14 @@ private:
         std::uint64_t rawOffset{};
         std::uint64_t rawSize{};
         bool data{};
+        bool code{};
     };
 
     [[nodiscard]] bool
     offset(std::uint64_t address, std::size_t size, std::size_t& at) const noexcept;
     [[nodiscard]] bool parse_headers(bool mapped) noexcept;
     [[nodiscard]] bool index_slots() noexcept;
+    [[nodiscard]] bool resolve_primitive_sizes() noexcept;
 
     HANDLE file_{INVALID_HANDLE_VALUE};
     HANDLE mapping_{};
@@ -169,6 +210,7 @@ private:
     std::uint64_t imageBase_{};
     std::uint64_t vmp0Low_{};
     std::uint64_t vmp0High_{};
+    std::uint64_t primitiveSizes_{};
     std::vector<Section> sections_{};
     std::map<std::uint32_t, std::uint64_t> slots_{};
 };
@@ -222,7 +264,6 @@ struct Plan final {
 
 /** Parses one bound reflection record at `address` for the compiler. */
 [[nodiscard]] bool parse_native(const Image& image,
-                                std::uint32_t handle,
                                 std::uint64_t address,
                                 Metadata& output,
                                 const char*& error) noexcept;

+ 2 - 48
Sunrise/src/client/content/activity/activity_sdk_decode_plans_metadata.cpp

@@ -6,8 +6,6 @@
 namespace sunrise::client::content::activity::sdk_generation::decode_plans::internal {
 namespace {
 
-/** Candidate offsets of the block chain marker inside a reflection record. */
-constexpr std::array<std::uint32_t, 4> kMarkerOffsets{0x58, 0x60, 0x68, 0x70};
 /** Reflection record header: length at 0, struct size at 20, alignment, kind, generic flag. */
 constexpr std::uint64_t kSizeAt = 20;
 constexpr std::uint64_t kAlignmentAt = 24;
@@ -22,44 +20,6 @@ constexpr std::size_t kPackageCodecHeader = 32;
 constexpr std::size_t kPackagePropertyHeader = 24;
 constexpr std::size_t kPropertyRowSize = 12;
 
-/** Header size and row stride of each reflection block class. */
-struct BlockLayout final {
-    std::uint32_t header{};
-    std::uint32_t stride{};
-};
-
-/**
- * Looks up the header size and row stride of one reflection block class.
- * @return False for an unknown block class.
- */
-[[nodiscard]] bool block_layout(std::uint32_t kind, BlockLayout& output) noexcept {
-    switch (kind) {
-    case kCodecBlock:
-        output = {0x28, 0x28};
-        return true;
-    case kPropertyBlock:
-        output = {0x20, 0x0C};
-        return true;
-    case kMethodBlock:
-        output = {0x10, 0x08};
-        return true;
-    case kTailBlock:
-        output = {0x18, 0x00};
-        return true;
-    default:
-        return false;
-    }
-}
-
-template <typename T>
-[[nodiscard]] bool read_at(std::span<const std::byte> bytes, std::size_t at, T& value) noexcept {
-    if (at > bytes.size() || bytes.size() - at < sizeof(T)) {
-        return false;
-    }
-    std::memcpy(&value, bytes.data() + at, sizeof(T));
-    return true;
-}
-
 /** Reads one 40-byte field descriptor from the executable. */
 [[nodiscard]] bool read_field(const Image& image, std::uint64_t at, Field& field) noexcept {
     return image.u32(at, field.offset) && image.u32(at + 4, field.alternate)
@@ -136,20 +96,17 @@ read_property(std::span<const std::byte> blob, std::size_t at, Property& propert
 /**
  * Parses one bound reflection record: header, generic argument, codec and property blocks.
  * @param image Mapped executable.
- * @param handle Bound handle the record belongs to.
  * @param address Record address from the slot index.
  * @param output Receives the record on success.
  * @param error Receives the refusal name on failure.
  * @return False when the record is malformed or unreadable.
  */
 bool parse_native(const Image& image,
-                  std::uint32_t handle,
                   std::uint64_t address,
                   Metadata& output,
                   const char*& error) noexcept {
     try {
         Metadata record{};
-        record.handle = handle;
         std::uint32_t length = 0;
         std::uint8_t genericFlag = 0;
         if (!image.u32(address, length) || !image.u32(address + kSizeAt, record.size)
@@ -208,7 +165,6 @@ bool parse_native(const Image& image,
                 }
                 Codec& codec = record.codec;
                 codec.present = true;
-                codec.handle = handle;
                 if (!image.u32(address + cursor + 20, codec.original)
                     || !image.u32(address + cursor + 24, codec.serialized)
                     || !image.u32(address + cursor + 28, codec.flags)
@@ -278,7 +234,6 @@ bool parse_package(std::uint32_t handle,
     try {
         Metadata record{};
         record.package = true;
-        record.handle = handle;
         std::uint8_t genericFlag = 0;
         if (blob.size() < kPackageMinimumSize || !read_at(blob, kSizeAt, record.size)
             || !read_at(blob, kAlignmentAt, record.alignment)
@@ -320,7 +275,6 @@ bool parse_package(std::uint32_t handle,
                 return false;
             }
             codec.present = true;
-            codec.handle = handle;
             codec.fields.resize(static_cast<std::size_t>(count));
             for (std::size_t index = 0; index < codec.fields.size(); ++index) {
                 if (!read_field(blob,
@@ -386,7 +340,7 @@ const Metadata* Store::get(std::uint32_t handle, const char*& error) noexcept {
         Metadata record{};
         const auto slot = image_.slots().find(handle);
         if (slot != image_.slots().end()) {
-            if (!parse_native(image_, handle, slot->second, record, error)) {
+            if (!parse_native(image_, slot->second, record, error)) {
                 return nullptr;
             }
         } else {
@@ -445,7 +399,7 @@ namespace {
         }
         repeat = codec.array;
     }
-    const std::uint64_t table = store.image().image_base() + kPrimitiveSizeTableRva;
+    const std::uint64_t table = store.image().primitive_sizes();
     for (std::uint64_t index = 0; index < repeat; ++index) {
         for (std::size_t fieldIndex = 0; fieldIndex < codec.fields.size(); ++fieldIndex) {
             const Field& field = codec.fields[fieldIndex];

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

@@ -89,23 +89,6 @@ bool g_hasResult{};
 
 } // namespace
 
-/** @return The stable log name of one estate validation status. */
-const char* status_name(Status value) noexcept {
-    switch (value) {
-    case Status::ready:
-        return "ready";
-    case Status::cancelled:
-        return "cancelled";
-    case Status::invalidInput:
-        return "invalid_input";
-    case Status::notGenerated:
-        return "not_generated";
-    case Status::publication:
-        return "publication";
-    }
-    return "not_generated";
-}
-
 /** Records one validation result for an artifact tree. @return False when an input is bad. */
 bool remember(std::wstring_view artifactDirectory,
               const Digest& sourceFingerprint,

+ 0 - 3
Sunrise/src/client/content/activity/activity_sdk_estate_validation.h

@@ -38,9 +38,6 @@ struct Result final {
     std::uint64_t packBytes{};
 };
 
-/** @return Stable diagnostic name for one retained-result lookup. */
-[[nodiscard]] const char* status_name(Status value) noexcept;
-
 /** Retains one successful generation result without reopening its output tree. */
 [[nodiscard]] bool remember(std::wstring_view artifactDirectory,
                             const Digest& sourceFingerprint,

+ 104 - 23
Sunrise/src/client/content/activity/activity_sdk_generation_inventory.cpp

@@ -1,8 +1,16 @@
 #include <Windows.h>
 
+#include <algorithm>
+#include <memory>
 #include <string_view>
+#include <tuple>
 #include <utility>
+#include <vector>
 
+#include "../../../core/logging/log.h"
+#include "../../../middleware/content/packages/named_tags.h"
+#include "../../../state/build_data/runtime.h"
+#include "../hash_names/hash_name_build.h"
 #include "activity_sdk_generation_worker_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::worker_internal {
@@ -160,29 +168,6 @@ append_scenario(std::vector<Scenario>& output, std::uint32_t tag, std::string_vi
     return false;
 }
 
-/** Converts the independently measured native completeness partition. */
-[[nodiscard]] bool convert_binding_completeness(const inventory::BindingCompleteness& input,
-                                                manifest::BindingCompleteness& output) noexcept {
-    manifest::BindingCompletenessStatus status{};
-    switch (input.status) {
-    case inventory::BindingCompletenessStatus::ready:
-        status = manifest::BindingCompletenessStatus::ready;
-        break;
-    case inventory::BindingCompletenessStatus::blockedUnresolvedRunnable:
-        status = manifest::BindingCompletenessStatus::blockedUnresolvedRunnable;
-        break;
-    default:
-        return false;
-    }
-    output = {input.total,
-              input.fixedScenario,
-              input.namedDefinitionUnavailable,
-              input.noDirectFixedActivityName,
-              input.unresolvedRunnable,
-              status};
-    return true;
-}
-
 /** Converts the checked native inventory into the bounded persistent catalog rows. */
 [[nodiscard]] bool build_inventory(Work& work, const package_reader::Source& source) noexcept {
     inventory::Snapshot snapshot{};
@@ -301,4 +286,100 @@ append_scenario(std::vector<Scenario>& output, std::uint32_t tag, std::string_vi
            && completeness.status == manifest::BindingCompletenessStatus::ready;
 }
 
+namespace {
+
+namespace named_tags = middleware::content::packages::named_tags;
+namespace hash_name_pass = ::sunrise::client::content::hash_names;
+
+/** Keeps one named definition, refusing a name the State catalog would refuse. */
+[[nodiscard]] bool collect_definition(void* opaque, const named_tags::Entry& entry) noexcept {
+    auto& rows = *static_cast<std::vector<state::content::Definition>*>(opaque);
+    if (entry.nameLength == 0 || entry.nameLength >= state::content::kDefinitionNameCapacity) {
+        return false;
+    }
+    state::content::Definition row{};
+    std::copy_n(entry.name.data(), entry.nameLength, row.name.data());
+    row.nameLength = entry.nameLength;
+    row.tag = entry.tag;
+    row.classId = entry.classId;
+    try {
+        rows.push_back(row);
+        return true;
+    } catch (...) {
+        return false;
+    }
+}
+
+/**
+ * Collects the named definitions of one package source, one row per distinct mapping.
+ * @param source Package directory and read mode.
+ * @param rows Receives the rows; empty on failure.
+ * @return True when the whole directory walk worked.
+ */
+[[nodiscard]] bool collect_definitions(const package_reader::Source& source,
+                                       std::vector<state::content::Definition>& rows) noexcept {
+    rows.clear();
+    named_tags::DirectoryResult result{};
+    if (!named_tags::extract_directory(
+            source.directory, source.installedOnly, &collect_definition, &rows, result)) {
+        rows.clear();
+        return false;
+    }
+    const auto key = [](const state::content::Definition& row) noexcept {
+        return std::tuple(row.tag, row.classId, std::string_view(row.name.data(), row.nameLength));
+    };
+    std::sort(rows.begin(), rows.end(), [&key](const auto& first, const auto& second) noexcept {
+        return key(first) < key(second);
+    });
+    // One mapping appears once per patch that carries it; the State catalog keeps it once.
+    const auto end =
+        std::unique(rows.begin(), rows.end(), [&key](const auto& first, const auto& second) {
+            return key(first) == key(second);
+        });
+    rows.erase(end, rows.end());
+    return true;
+}
+
+} // namespace
+
+/** Builds the layouts, named definitions and identifier names from installed packages only. */
+bool build_installed_tables(Work& work, const package_reader::Source& source) noexcept {
+    work.tables = {};
+    bool layouts = false;
+    try {
+        auto scratch = std::make_unique<package_reader::Scratch>();
+        work.installedLayouts =
+            std::make_unique<::sunrise::client::content::scenarios::LayoutSet>();
+        layouts = work.installedLayouts->build(source, *scratch);
+        package_reader::close_files(*scratch);
+    } catch (...) {
+        layouts = false;
+    }
+    const bool names = collect_definitions(source, work.installedDefinitions);
+    const bool hashNames = layouts
+                           && hash_name_pass::build_private(
+                               source, work.installedLayouts->rows(), work.installedHashNames);
+    // Identifier names are only a last fallback; without them the pass still builds, but never
+    // falls back to the mod-inclusive State table.
+    if (!hashNames) {
+        work.installedHashNames.clear();
+    }
+    const bool ready = layouts && names;
+    if (ready) {
+        work.tables = {
+            work.installedLayouts.get(), &work.installedDefinitions, &work.installedHashNames};
+    }
+    core::log::writef(core::log::Channel::client,
+                      !ready ? core::log::Level::error
+                             : (hashNames ? core::log::Level::info : core::log::Level::warn),
+                      "ev=activity_sdk_generation stage=installed_tables layouts=%zu names=%zu "
+                      "hash_names=%zu hash_pass=%s result=%s",
+                      work.installedLayouts != nullptr ? work.installedLayouts->rows().size() : 0U,
+                      work.installedDefinitions.size(),
+                      work.installedHashNames.size(),
+                      hashNames ? "ok" : "fail",
+                      ready ? "ok" : "fail");
+    return ready;
+}
+
 } // namespace sunrise::client::content::activity::sdk_generation::worker_internal

+ 8 - 9
Sunrise/src/client/content/activity/activity_sdk_generation_publish.cpp

@@ -12,6 +12,7 @@
 #include "../../../core/ui/busy/busy.h"
 #include "../../../state/activity_sdk/internal.h"
 #include "../../../state/activity_sdk/runtime.h"
+#include "../../../state/gameplay/external/rsat_decode_plans.h"
 #include "activity_sdk_decode_plans.h"
 #include "activity_sdk_generation_worker_internal.h"
 #include "activity_sdk_live_publication.h"
@@ -264,12 +265,9 @@ void log_decoder_cache(decode_plans::Status status,
     }
 }
 
-/**
- * Compiles the decoder cache pair beside the final pack for the staged catalog's identity.
- * The entity transport loads nothing without it, so a refusal fails the pass.
- * @return False with `failureDetail` set when the cache was not written.
- */
+/** Keeps the decoder pair beside the pack whose identity it uses. */
 [[nodiscard]] bool build_decoder_cache(const Work& work,
+                                       std::wstring_view packPath,
                                        const package_reader::Source& source,
                                        const state::activity_sdk::Catalog& catalog,
                                        const char*& failureDetail) noexcept {
@@ -277,10 +275,11 @@ void log_decoder_cache(decode_plans::Status status,
     decode_plans::Result result{};
     decode_plans::Status status = decode_plans::Status::invalidInput;
     try {
-        const std::size_t separator = work.packPath.find_last_of(L'\\');
+        const std::size_t separator = packPath.find_last_of(L"\\/");
         const std::wstring cachePath =
-            (separator == std::wstring::npos ? std::wstring{} : work.packPath.substr(0, separator))
-            + L"\\rsat_decode_plans.cache";
+            (separator == std::wstring_view::npos ? std::wstring{}
+                                                  : std::wstring(packPath.substr(0, separator)))
+            + std::wstring(state::gameplay::rsat_decode_plans::kCacheSuffix);
         decode_plans::Request request{};
         request.executablePath = work.executablePath;
         request.executableModule = work.executableModule;
@@ -380,7 +379,7 @@ bool publish_estate(Work& work,
     }
     if (complete) {
         report_publication_progress(progress, "building decoder cache");
-        complete = build_decoder_cache(work, source, *stagedCatalog, failureDetail);
+        complete = build_decoder_cache(work, packPath, source, *stagedCatalog, failureDetail);
     }
     stagedCatalog.reset();
     if (complete) {

+ 13 - 7
Sunrise/src/client/content/activity/activity_sdk_generation_scenario_shards.cpp

@@ -117,11 +117,10 @@ void report_shard_diagnostics(const Scenario& scenario,
                                 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.
+    // Each failure arm reports its own detail token.
     const std::string_view name(scenario.name.data(), scenario.nameLength);
     const auto snapshot = builder::build_scenario_catalog(
-        source, containers, scratch, analyses, scenario.tag, name, &cancel_requested);
+        source, containers, scratch, analyses, work.tables, scenario.tag, name, &cancel_requested);
     if (cancel_requested()) {
         detail = "shard_cancelled";
         return false;
@@ -156,6 +155,11 @@ void report_shard_diagnostics(const Scenario& scenario,
         detail = detailScratch.data();
         return false;
     }
+    if (!snapshot->containerPlacementDiagnostics.identityOwnerInventoryComplete) {
+        detail = "shard_container_inventory_incomplete";
+        report_shard_diagnostics(scenario, *snapshot);
+        return false;
+    }
     generated::PreparedShard prepared{};
     if (!generated::prepare(work.sourceFingerprint, *snapshot, prepared)) {
         detail = "shard_prepare_failed";
@@ -253,12 +257,14 @@ 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,
+/** A cached shard of a laid-out scenario is reusable only when its placement contexts resolved. */
+[[nodiscard]] bool placement_context_ready(const Work& work,
+                                           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) {
+    if (!::sunrise::client::content::scenarios::find_layout(work.tables.layouts, name, layout)
+        || layout.spawnStemLength == 0) {
         return true;
     }
     return snapshot.containerPlacementDiagnostics.contextResolved
@@ -297,7 +303,7 @@ void run_scenario_worker(ScenarioBuildBatch& batch) noexcept {
                                                 scenario,
                                                 *existing,
                                                 result.snapshot);
-                if (kept && !placement_context_ready(scenario, *result.snapshot)) {
+                if (kept && !placement_context_ready(*batch.work, scenario, *result.snapshot)) {
                     kept = false;
                 }
                 if (kept

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_generation_shard.cpp

@@ -44,7 +44,7 @@ constexpr std::wstring_view kShardExtension = L".pack";
     return true;
 }
 
-/** Reopens one legacy cache row through the same authenticated full-only gate. */
+/** Copies one authenticated full shard into caller-owned storage. */
 [[nodiscard]] bool load_record(const std::wstring& scenarioDirectory,
                                const generated::Digest& sourceFingerprint,
                                const Scenario& scenario,

+ 58 - 44
Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp

@@ -19,6 +19,7 @@
 #include "../../../core/filesystem/path.h"
 #include "../../../core/logging/log.h"
 #include "../../../core/ui/busy/busy.h"
+#include "../../../middleware/content/packages/reader/overlay.h"
 #include "../../../state/activity_sdk/generated_world/catalog_manifest.h"
 #include "../../../state/activity_sdk/generated_world/codec.h"
 #include "../../../state/activity_sdk/generated_world/store.h"
@@ -27,6 +28,7 @@
 #include "../../../state/activity_sdk/runtime.h"
 #include "../../../state/build_data/runtime.h"
 #include "../../../state/content_manifest/content_manifest_state_runtime.h"
+#include "../../../state/gameplay/external/rsat_decode_plans.h"
 #include "../items/packages/internal.h"
 #include "../scenarios/scenario_build.h"
 #include "activity_sdk_activity_inventory.h"
@@ -56,27 +58,24 @@ constexpr std::wstring_view kSdkDirectorySuffix = L"\\sdk";
 constexpr std::wstring_view kScenarioDirectorySuffix = L"\\sdk\\scenarios";
 constexpr std::wstring_view kCatalogFileSuffix = L"\\sdk\\catalog.bin";
 constexpr std::wstring_view kPackFileSuffix = L"\\activity_sdk.pack";
-constexpr std::wstring_view kShardExtension = L".pack";
 // Wait this long before probing package data again.
 constexpr ULONGLONG kPackageWaitMs = 1'000;
 /** A small batch bounds complete snapshots retained while parallel work finishes. */
 constexpr std::size_t kScenarioBatchSize = 24;
 
 using worker::binding_ready;
+using worker::build_installed_tables;
 using worker::build_inventory;
 using worker::build_scenarios;
 using worker::build_world_source;
-using worker::convert_binding_completeness;
 using worker::find_record;
 using worker::kDetailCapacity;
-using worker::load_full_record;
 using worker::load_record;
 using worker::publish_estate;
 using worker::publish_progress;
 using worker::record_name_matches;
 using worker::Scenario;
 using worker::ScenarioBuildResult;
-using worker::shard_path;
 using worker::Work;
 
 SRWLOCK g_lock{SRWLOCK_INIT};
@@ -137,17 +136,6 @@ void log_stage_duration(std::string_view stage,
     }
 }
 
-/** Copies the selected installed-content identity while the manifest owns its view. */
-[[nodiscard]] bool copy_fingerprint(void* opaque,
-                                    const state::content_manifest::View& view) noexcept {
-    if (opaque == nullptr || view.buildFingerprint.size() != generated::Digest{}.size()) {
-        return false;
-    }
-    auto& output = *static_cast<generated::Digest*>(opaque);
-    std::copy(view.buildFingerprint.begin(), view.buildFingerprint.end(), output.begin());
-    return true;
-}
-
 /** Accepts an existing directory or creates one exact owned child. */
 [[nodiscard]] bool ensure_directory(const std::wstring& path) noexcept {
     if (path.empty()) {
@@ -201,7 +189,8 @@ void log_stage_duration(std::string_view stage,
 /** Copies the package-reader inputs into one work item. */
 [[nodiscard]] bool prepare_work(Work& work) noexcept {
     try {
-        return state::content_manifest::visit_snapshot(&copy_fingerprint, &work.sourceFingerprint)
+        // The installed identity; mods never change it.
+        return state::content_manifest::installed_fingerprint(work.sourceFingerprint)
                && items::packages::collect_keys(work.keys);
     } catch (...) {
         SecureZeroMemory(&work.keys, sizeof work.keys);
@@ -209,11 +198,6 @@ void log_stage_duration(std::string_view stage,
     }
 }
 
-/** Adapts the process-wide cancellation flag to the inventory builder callback. */
-[[nodiscard]] bool inventory_cancelled(void*) noexcept {
-    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) {
@@ -245,17 +229,21 @@ DWORD WINAPI thread_main(void* opaque) noexcept {
     // Outlives the shard build, because the reported detail can point into it.
     std::array<char, kDetailCapacity> detailScratch{};
     try {
-        const package_reader::Source source{work->packageDirectory, &work->keys};
+        // The SDK is built from installed packages only. Mods ship their own Lua bindings.
+        const package_reader::Source source{work->packageDirectory, &work->keys, true};
         publish_progress(state::activity_sdk::generation::Status::preparing,
                          0,
                          static_cast<std::uint32_t>(
                              middleware::content::packages::tables::kActivityDefinitionCount),
                          0,
                          "building installed activity inventory");
+        // The layouts and names State publishes include mod packages whenever a mod serves one.
+        const bool modded = !package_reader::overlay::files().empty();
+        bool layoutsReady = modded ? build_installed_tables(*work, source)
+                                   : state::build_data::scenario_layouts_ready();
         // The game normally publishes these layouts during boot. An isolated generation
         // process has no boot worker, but container and spatial extraction still need them.
-        bool layoutsReady = state::build_data::scenario_layouts_ready();
-        if (work->offline && !layoutsReady) {
+        if (!modded && work->offline && !layoutsReady) {
             auto scratch = std::make_unique<package_reader::Scratch>();
             // Offline layout extraction has a finite work limit.
             constexpr std::size_t kMaximumLayoutSteps = 10'000;
@@ -267,7 +255,9 @@ DWORD WINAPI thread_main(void* opaque) noexcept {
         }
         bool inventoried = layoutsReady && build_inventory(*work, source);
         if (!inventoried) {
-            failureDetail = layoutsReady ? "inventory_incomplete" : "scenario_layouts_unavailable";
+            failureDetail = layoutsReady ? "inventory_incomplete"
+                                         : (modded ? "installed_tables_failed"
+                                                   : "scenario_layouts_unavailable");
         }
 
         const std::uint32_t total = static_cast<std::uint32_t>(work->scenarios.size());
@@ -279,9 +269,9 @@ DWORD WINAPI thread_main(void* opaque) noexcept {
         // The container sweep does not depend on the scenario. Sweeping it inside the loop read
         // the whole install once per scenario and kept one stem's worth of it.
         builder::ContainerIndex containers{};
-        // Shards built against an empty index drop every container placement, so a failed
-        // sweep must fail the pass instead of silently degrading the output.
-        const bool containersReady = builder::build_container_index(source, containers, &cancelled);
+        // Missing containers must not become a complete authoritative SDK.
+        const bool containersReady =
+            builder::build_complete_container_index(source, containers, &cancelled);
         publish_progress(state::activity_sdk::generation::Status::building,
                          0,
                          total,
@@ -327,8 +317,9 @@ DWORD WINAPI thread_main(void* opaque) noexcept {
                 if (!result.attempted || !result.ready || result.snapshot == nullptr) {
                     complete = false;
                     failureScenario = scenario.tag;
+                    detailScratch = result.detail;
                     failureDetail =
-                        result.detail[0] != '\0' ? result.detail.data() : "shard_build_failed";
+                        detailScratch[0] != '\0' ? detailScratch.data() : "shard_build_failed";
                     break;
                 }
                 built += result.reused ? 0U : 1U;
@@ -625,10 +616,10 @@ void service() noexcept {
         const state::activity_sdk::Snapshot loaded = state::activity_sdk::snapshot();
         generated::Digest fingerprint{};
         manifest::SdkIdentity identity{};
-        const bool hasIdentity =
-            loaded && loaded->sdk_build_sha256().size() == identity.buildSha256.size()
-            && loaded->payload_sha256().size() == identity.payloadSha256.size()
-            && state::content_manifest::visit_snapshot(&copy_fingerprint, &fingerprint);
+        const bool hasIdentity = loaded
+                                 && loaded->sdk_build_sha256().size() == identity.buildSha256.size()
+                                 && loaded->payload_sha256().size() == identity.payloadSha256.size()
+                                 && state::content_manifest::installed_fingerprint(fingerprint);
         if (hasIdentity) {
             std::copy(loaded->sdk_build_sha256().begin(),
                       loaded->sdk_build_sha256().end(),
@@ -637,12 +628,27 @@ void service() noexcept {
                       loaded->payload_sha256().end(),
                       identity.payloadSha256.begin());
         }
-        const bool current =
-            hasIdentity
+        // compatible_catalog names its own refusal and clears the name when it accepts.
+        const char* rejected = "identity";
+        bool preserveCurrent = false;
+        const bool currentIdentity = hasIdentity && final_sdk_identity(fingerprint, identity)
+                                     && std::equal(fingerprint.begin(),
+                                                   fingerprint.end(),
+                                                   loaded->content_key_sha256().begin(),
+                                                   loaded->content_key_sha256().end());
+        if (currentIdentity
             && shard_store::compatible_catalog(
-                g_catalogPath.c_str(), g_scenarioDirectory, fingerprint, identity)
-            && (!g_luaDeclarations || lua::is_current(g_sdkDirectory.c_str(), *loaded));
-        if (current) {
+                g_catalogPath.c_str(), g_scenarioDirectory, fingerprint, identity, rejected)) {
+            rejected = g_luaDeclarations && !lua::is_current(g_sdkDirectory.c_str(), *loaded)
+                           ? "lua_stale"
+                           : nullptr;
+            if (rejected == nullptr
+                && !state::gameplay::rsat_decode_plans::valid_installed_pair(*loaded)) {
+                rejected = "decoder_stale";
+                preserveCurrent = true;
+            }
+        }
+        if (rejected == nullptr) {
             g_started = true;
             publish_progress(state::activity_sdk::generation::Status::ready,
                              0,
@@ -652,7 +658,13 @@ void service() noexcept {
             ReleaseSRWLockExclusive(&g_lock);
             return;
         }
-        state::activity_sdk::shutdown();
+        core::log::writef(core::log::Channel::client,
+                          core::log::Level::info,
+                          "ev=activity_sdk_generation stage=reuse result=rejected reason=%s",
+                          rejected);
+        if (!preserveCurrent) {
+            state::activity_sdk::shutdown();
+        }
     }
     auto* work = new (std::nothrow) Work();
     if (work == nullptr) {
@@ -689,10 +701,12 @@ void service() noexcept {
         return;
     }
     core::path::Buffer packageDirectory;
-    const bool ready = work->module != nullptr && !work->sdkDirectory.empty()
-                       && !work->packPath.empty() && state::build_data::scenario_layouts_ready()
-                       && items::packages::package_directory(packageDirectory)
-                       && prepare_work(*work);
+    // Shards keep identifier names, so wait for layouts, spawn sets and the hash-name table.
+    const bool ready =
+        work->module != nullptr && !work->sdkDirectory.empty() && !work->packPath.empty()
+        && state::build_data::scenario_layouts_ready() && state::build_data::spawn_sets_ready()
+        && state::build_data::hash_names_ready()
+        && items::packages::package_directory(packageDirectory) && prepare_work(*work);
     if (ready) {
         try {
             work->packageDirectory.assign(packageDirectory.chars.data(), packageDirectory.length);
@@ -782,7 +796,7 @@ bool load_cached_scenario(std::uint32_t scenarioTag,
         return false;
     }
     generated::Digest fingerprint{};
-    if (!state::content_manifest::visit_snapshot(&copy_fingerprint, &fingerprint)) {
+    if (!state::content_manifest::installed_fingerprint(fingerprint)) {
         return false;
     }
     try {

+ 21 - 4
Sunrise/src/client/content/activity/activity_sdk_generation_worker_internal.h

@@ -12,7 +12,10 @@
 #include "../../../state/activity_sdk/generated_world/catalog_manifest.h"
 #include "../../../state/activity_sdk/generated_world/format.h"
 #include "../../../state/activity_sdk/generation/definition.h"
+#include "../../../state/build_data/hash_names/definition.h"
 #include "../../../state/build_data/scriptables/definition.h"
+#include "../../../state/content/content_catalog.h"
+#include "../scenarios/scenario_build.h"
 #include "activity_sdk_activity_inventory.h"
 #include "activity_sdk_external_placements.h"
 #include "activity_sdk_generation_worker.h"
@@ -64,6 +67,14 @@ struct Work final {
     std::vector<lua_artifacts::ScenarioWorldSource> scenarioWorldSources{};
     /** Placements the scenarios load from container lists and type-4 descriptors. */
     external_placements::Index externalPlacements{};
+    /** Layouts read from installed packages only. Built only when a mod serves package files. */
+    std::unique_ptr<::sunrise::client::content::scenarios::LayoutSet> installedLayouts{};
+    /** Named definitions read from installed packages only, built with the layouts. */
+    std::vector<state::content::Definition> installedDefinitions{};
+    /** Identifier names read from installed packages only, sorted by hash. */
+    std::vector<state::build_data::hash_names::Name> installedHashNames{};
+    /** Tables the scenario builds read. Null members read the ones State publishes. */
+    builder::BuildTables tables{};
     generated::Digest payloadSha256{};
     std::uint64_t packBytes{};
     std::uint32_t luaFiles{};
@@ -108,6 +119,16 @@ void publish_progress(state::activity_sdk::generation::Status status,
 /** Converts and checks the package-backed activity inventory used by every later stage. */
 [[nodiscard]] bool build_inventory(Work& work, const package_reader::Source& source) noexcept;
 
+/**
+ * Builds the destination layouts, named definitions and identifier names from installed packages.
+ * Needed when a mod serves package files, because the tables State publishes then include them.
+ * @param work Pass whose scenario builds read the tables; its tables stay empty on failure.
+ * @param source Installed-only package source with block keys.
+ * @return True when all three tables are complete.
+ */
+[[nodiscard]] bool build_installed_tables(Work& work,
+                                          const package_reader::Source& source) noexcept;
+
 /** Emits transparent positional trigger declarations for one mission module. */
 [[nodiscard]] bool build_world_source(const catalog::Snapshot& snapshot,
                                       lua_artifacts::ScenarioWorldSource& result);
@@ -120,10 +141,6 @@ void publish_progress(state::activity_sdk::generation::Status status,
 [[nodiscard]] bool binding_ready(const manifest::BindingCompleteness& completeness,
                                  std::size_t activityCount) noexcept;
 
-/** Converts the checked native binding partition to its persistent form. */
-[[nodiscard]] bool convert_binding_completeness(const inventory::BindingCompleteness& input,
-                                                manifest::BindingCompleteness& output) noexcept;
-
 /** Formats the content-addressed path for one exact scenario payload. */
 [[nodiscard]] bool shard_path(const std::wstring& directory,
                               std::uint32_t scenarioTag,

+ 2 - 25
Sunrise/src/client/content/activity/activity_sdk_generation_world_source.cpp

@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "activity_sdk_generation_worker_internal.h"
+#include "activity_sdk_lua_artifacts_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::worker_internal {
 namespace {
@@ -73,30 +74,6 @@ void append_format(std::string& output, const char* format, auto... values) {
     }
 }
 
-/** Converts an extracted trigger name into a declaration key. */
-[[nodiscard]] std::string lua_constant(std::string_view value) {
-    std::string output;
-    bool separator = false;
-    for (const unsigned char byte : value) {
-        if (std::isalnum(byte) != 0) {
-            if (separator && !output.empty()) {
-                output.push_back('_');
-            }
-            output.push_back(static_cast<char>(std::toupper(byte)));
-            separator = false;
-        } else {
-            separator = true;
-        }
-    }
-    if (output.empty()) {
-        output = "UNNAMED";
-    }
-    if (std::isdigit(static_cast<unsigned char>(output.front())) != 0) {
-        output.insert(output.begin(), '_');
-    }
-    return output;
-}
-
 } // namespace
 
 /** Emits transparent positional trigger declarations for one mission module. */
@@ -206,7 +183,7 @@ bool build_world_source(const catalog::Snapshot& snapshot,
                           instance.shapeIndex,
                           static_cast<unsigned>(instance.active));
             for (const std::string_view name : instanceNames) {
-                std::string key = lua_constant(name);
+                std::string key = lua_artifacts::internal::identifier(name, true);
                 const std::string base = key;
                 std::uint32_t suffix = 1;
                 while (std::any_of(constants.begin(), constants.end(), [&key](const auto& entry) {

+ 4 - 1
Sunrise/src/client/content/activity/activity_sdk_ir_family_coverage.cpp

@@ -294,7 +294,10 @@ std::string_view stable_name(Status status) noexcept {
     return {};
 }
 
-/** Renders the ledger as the JSON coverage report. @return False when the buffer is short. */
+/**
+ * Renders the ledger as the JSON coverage report.
+ * @return False when the ledger does not close or an allocation fails.
+ */
 bool render_json(const Ledger& ledger,
                  std::string& output,
                  Failure& failure,

+ 2 - 0
Sunrise/src/client/content/activity/activity_sdk_ir_family_coverage.h

@@ -8,6 +8,8 @@
 
 namespace sunrise::client::content::activity::sdk_generation::ir_family_coverage {
 
+// TODO: the native pack pipeline must declare every family and gate publication on close().
+
 /** Canonical native order of every normalized SDK family. Keep this in lockstep with IR v5. */
 enum class Family : std::uint8_t {
     sources,

+ 49 - 21
Sunrise/src/client/content/activity/activity_sdk_live_publication.cpp

@@ -9,6 +9,7 @@
 #include <string>
 #include <string_view>
 
+#include "../../../state/gameplay/external/rsat_decode_plans.h"
 #include "activity_sdk_tree_publication.h"
 
 namespace sunrise::client::content::activity::sdk_generation::live_publication {
@@ -21,8 +22,11 @@ constexpr std::wstring_view kPendingMarkerSuffix = L"\\.activity-sdk-publication
 constexpr std::wstring_view kCommittedMarkerSuffix = L"\\.activity-sdk-publication.committed";
 /** Distinct stage directories tried before giving up on a name collision. */
 constexpr std::size_t kAllocationAttempts = 16;
-/** Marker file magic, bytes "ASP1"; a file without it is not ours. */
-constexpr std::uint32_t kMarkerMagic = 0x31505341U;
+/** ASP1 journals cover Lua, pack, and catalog only. */
+constexpr std::uint32_t kLegacyMarkerMagic = 0x31505341U;
+/** ASP2 journals include the decoder pair before the catalog. */
+constexpr std::uint32_t kMarkerMagic = 0x32505341U;
+namespace plans = state::gameplay::rsat_decode_plans;
 /** A drive path opens with the drive letter, a colon and a separator. */
 constexpr std::size_t kDrivePrefixLength = 3;
 
@@ -150,7 +154,8 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
         return false;
     }
     const DWORD attributes = GetFileAttributesW(path);
-    return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+    return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0
+           && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
 }
 
 /** Requires one existing file that is not a directory. */
@@ -159,7 +164,8 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
         return false;
     }
     const DWORD attributes = GetFileAttributesW(path);
-    return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
+    return attributes != INVALID_FILE_ATTRIBUTES
+           && (attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) == 0;
 }
 
 /** Accepts a missing final path or checks its exact expected ordinary kind. */
@@ -171,7 +177,8 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
         return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
     }
     exists = true;
-    return ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory;
+    return ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory
+           && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
 }
 
 /** Creates one empty ordinary directory or accepts an existing ordinary directory. */
@@ -236,8 +243,10 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
         ReadFile(file, &output, sizeof output, &read, nullptr) != FALSE && read == sizeof output
         && ReadFile(file, &trailing, 1, &trailingRead, nullptr) != FALSE && trailingRead == 0;
     const bool closed = CloseHandle(file) != FALSE;
-    return complete && closed && output.magic == kMarkerMagic
-           && output.reserved == std::array<std::uint8_t, 3>{} && (output.priorMask & 0xF8U) == 0;
+    const bool validMask = output.magic == kLegacyMarkerMagic ? (output.priorMask & 0xF8U) == 0
+                           : output.magic == kMarkerMagic     ? (output.priorMask & 0xE0U) == 0
+                                                              : false;
+    return complete && closed && validMask && output.reserved == std::array<std::uint8_t, 3>{};
 }
 
 /** Removes one transaction-created final value before restoring its backup. */
@@ -329,12 +338,21 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
     try {
         std::wstring finalLua(finalSdkDirectory);
         finalLua.append(L"\\lua");
-        std::array<Resource, 3> resources{
+        std::wstring_view parent;
+        std::wstring_view leaf;
+        if (!split(finalPackPath, parent, leaf)) {
+            return false;
+        }
+        const std::wstring finalDecoder = std::wstring(parent) + plans::kCacheSuffix.data();
+        const std::wstring finalDecoderManifest = finalDecoder + plans::kManifestSuffix.data();
+        std::array<Resource, 5> allResources{
             Resource{nullptr, finalLua.c_str(), {}, true},
             Resource{nullptr, finalPackPath, {}, false},
+            Resource{nullptr, finalDecoder.c_str(), {}, false},
+            Resource{nullptr, finalDecoderManifest.c_str(), {}, false},
             Resource{nullptr, finalCatalogPath, {}, false},
         };
-        for (Resource& resource : resources) {
+        for (Resource& resource : allResources) {
             if (!backup_path(resource.final, resource.backup)) {
                 return false;
             }
@@ -348,11 +366,19 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
             && committedAttributes != INVALID_FILE_ATTRIBUTES) {
             return false;
         }
+        Marker marker{};
+        const bool hasPending = pendingAttributes != INVALID_FILE_ATTRIBUTES;
+        const bool hasCommitted = committedAttributes != INVALID_FILE_ATTRIBUTES;
+        if ((hasPending || hasCommitted)
+            && !read_marker(hasPending ? pending : committed, marker)) {
+            return false;
+        }
+        std::span<Resource> resources(allResources);
+        if ((hasPending || hasCommitted) && marker.magic == kLegacyMarkerMagic) {
+            std::swap(allResources[2], allResources[4]);
+            resources = resources.first(3);
+        }
         if (committedAttributes != INVALID_FILE_ATTRIBUTES) {
-            Marker committedMarker{};
-            if (!read_marker(committed, committedMarker)) {
-                return false;
-            }
             for (Resource& resource : resources) {
                 bool exists = false;
                 if (!final_state(resource.final, resource.directory, exists) || !exists) {
@@ -374,7 +400,6 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
             return DeleteFileW(committed.c_str()) != FALSE;
         }
 
-        Marker marker{};
         std::uint8_t priorMask = 0;
         bool hasAnyBackup = false;
         for (const Resource& resource : resources) {
@@ -383,11 +408,7 @@ split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf
         if (pendingAttributes == INVALID_FILE_ATTRIBUTES && !hasAnyBackup) {
             return true;
         }
-        const bool hasPending = pendingAttributes != INVALID_FILE_ATTRIBUTES;
         if (hasPending) {
-            if (!read_marker(pending, marker)) {
-                return false;
-            }
             priorMask = marker.priorMask;
         } else {
             for (std::size_t index = 0; index < resources.size(); ++index) {
@@ -547,7 +568,7 @@ Status allocate(const wchar_t* finalPackPath, Stage& output) noexcept {
     return Status::stageAllocation;
 }
 
-/** Commits Lua, pack, and catalog in order while retaining rollback state through finalization. */
+/** Keeps Lua, pack, decoder pair, and catalog in one rollback transaction. */
 Status publish(const Stage& stage,
                const wchar_t* finalSdkDirectory,
                const wchar_t* finalPackPath,
@@ -584,6 +605,8 @@ Status publish(const Stage& stage,
     const std::wstring expectedStageLua = expectedStageSdk + L"\\lua";
     const std::wstring expectedStagePack = canonicalStageRoot + L"\\activity_sdk.pack";
     const std::wstring expectedStageCatalog = expectedStageSdk + L"\\catalog.bin";
+    const std::wstring stageDecoder = canonicalStageRoot + plans::kCacheSuffix.data();
+    const std::wstring stageDecoderManifest = stageDecoder + plans::kManifestSuffix.data();
     const std::wstring expectedFinalCatalog = canonicalFinalSdk + L"\\catalog.bin";
     std::wstring_view stageParent;
     std::wstring_view stageLeaf;
@@ -601,7 +624,8 @@ Status publish(const Stage& stage,
         || !same_text(canonicalStageCatalog, expectedStageCatalog)
         || !same_text(canonicalFinalSdk, expectedFinalSdk)
         || !same_text(canonicalFinalCatalog, expectedFinalCatalog)
-        || !same_text(finalLeaf, L"activity_sdk.pack")) {
+        || !same_text(finalLeaf, L"activity_sdk.pack") || !is_file(stageDecoder.c_str())
+        || !is_file(stageDecoderManifest.c_str())) {
         return Status::invalidInput;
     }
     std::wstring finalLua;
@@ -611,9 +635,13 @@ Status publish(const Stage& stage,
     } catch (...) {
         return Status::invalidInput;
     }
-    std::array<Resource, 3> resources{
+    const std::wstring finalDecoder = std::wstring(finalParent) + plans::kCacheSuffix.data();
+    const std::wstring finalDecoderManifest = finalDecoder + plans::kManifestSuffix.data();
+    std::array<Resource, 5> resources{
         Resource{canonicalStageLua.c_str(), finalLua.c_str(), {}, true},
         Resource{canonicalStagePack.c_str(), canonicalFinalPack.c_str(), {}, false},
+        Resource{stageDecoder.c_str(), finalDecoder.c_str(), {}, false},
+        Resource{stageDecoderManifest.c_str(), finalDecoderManifest.c_str(), {}, false},
         Resource{canonicalStageCatalog.c_str(), canonicalFinalCatalog.c_str(), {}, false},
     };
     std::wstring pendingMarker;

+ 1 - 4
Sunrise/src/client/content/activity/activity_sdk_live_publication.h

@@ -38,10 +38,7 @@ using FinalizeOperation = bool (*)(void* context) noexcept;
 /** Allocates one writer-owned isolated stage beside the live pack. */
 [[nodiscard]] Status allocate(const wchar_t* finalPackPath, Stage& output) noexcept;
 
-/**
- * Replaces only the owned Lua subtree, pack file, and catalog file, in that order. The catalog
- * rename is the commit point. Any failure before finalization restores all previous owned paths.
- */
+/** Publishes Lua, pack, decoder pair, then catalog, retaining all backups through finalization. */
 [[nodiscard]] Status publish(const Stage& stage,
                              const wchar_t* finalSdkDirectory,
                              const wchar_t* finalPackPath,

+ 5 - 19
Sunrise/src/client/content/activity/activity_sdk_lua_actor_sequences.cpp

@@ -6,26 +6,12 @@
 
 namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
 
-/** Every extracted row remains inspectable; only slot-owned runtime values can be played. */
+/**
+ * Every extracted row remains inspectable; only slot-owned runtime values can be played.
+ * The collection's count and at come from the mission API table; this adds its symbol keys.
+ */
 void append_actor_sequence_contract(const Source& source, std::string& output) {
-    output.append(R"lua(
----@class SunriseActorSequence
----@field id string
----@field name string|nil
----@field symbol string
----@field source_path string|nil
----@field key integer
----@field kind integer
----@field resource_tag integer
----@field table_index integer
----@field ordinal integer
----@field source_offset integer
----@field playable boolean
-
----@class SunriseActorSequences
----@field count integer
----@field at fun(self: SunriseActorSequences, ordinal: integer): SunriseActorSequence|nil
-)lua");
+    output.append("\n---@class SunriseActorSequences\n");
     std::set<std::string> symbols{};
     for (const auto& row : source.actorSequenceEntries) {
         const auto symbol = text(source, row.symbol);

+ 2 - 4
Sunrise/src/client/content/activity/activity_sdk_lua_artifacts.h

@@ -95,6 +95,8 @@ struct Bundle final {
     std::string activitySdkModule{};
     std::string behaviorModule{};
     std::string manifestJson{};
+    /** The mission Lua API schema, rendered from the table the VM dispatches from. */
+    std::string missionApiJson{};
     std::vector<SourceModule> activityModules{};
     std::vector<SourceModule> missionModules{};
 };
@@ -116,10 +118,6 @@ struct Result final {
 [[nodiscard]] Status
 publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result& output) noexcept;
 
-/** Builds then publishes one complete declaration transaction. */
-[[nodiscard]] Status
-publish(const wchar_t* sdkDirectory, const Source& source, Result& output) noexcept;
-
 /** @return True when the committed Lua manifest exactly matches the loaded SDK. */
 [[nodiscard]] bool is_current(const wchar_t* sdkDirectory,
                               const state::activity_sdk::Catalog& catalog) noexcept;

+ 24 - 1
Sunrise/src/client/content/activity/activity_sdk_lua_artifacts_internal.h

@@ -17,7 +17,8 @@ namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::int
 struct Value final {
     using Array = std::vector<Value>;
     using Object = std::vector<std::pair<std::string, Value>>;
-    using Data = std::variant<std::monostate, bool, std::uint64_t, std::string, Array, Object>;
+    using Data =
+        std::variant<std::monostate, bool, std::uint64_t, std::int64_t, std::string, Array, Object>;
 
     Data data{};
 };
@@ -25,11 +26,29 @@ struct Value final {
 [[nodiscard]] Value null_value();
 [[nodiscard]] Value boolean(bool value);
 [[nodiscard]] Value number(std::uint64_t value);
+[[nodiscard]] Value signed_number(std::int64_t value);
 [[nodiscard]] Value string(std::string_view value);
 [[nodiscard]] Value array(Value::Array values);
 [[nodiscard]] Value object(Value::Object values);
 [[nodiscard]] Value string_array(std::initializer_list<std::string_view> values);
 
+/** One declared field: its name and its contract type. */
+using Field = std::pair<std::string_view, std::string_view>;
+
+/** Sorts object members by key. */
+void sort_keys(Value::Object& value);
+
+/** @return One object mapping each field name to its type string. */
+[[nodiscard]] Value field_map(std::initializer_list<Field> source);
+
+/** Builds one view's declared fields, expanding each prefix into its seven name lanes. */
+[[nodiscard]] Value named_view(std::initializer_list<Field> source,
+                               std::initializer_list<std::string_view> prefixes,
+                               std::string_view rowScope);
+
+/** Builds one immutable view of plain fields that throws on a stale read. */
+[[nodiscard]] Value view(std::initializer_list<Field> fields, std::string_view rowScope);
+
 /** Renders one deterministic pretty JSON value without adding a final newline. */
 [[nodiscard]] bool
 render_json(const Value& value, std::uint32_t initialDepth, std::string& output) noexcept;
@@ -43,6 +62,10 @@ render_lua(const Value& value, std::uint32_t initialDepth, std::string& output)
 void append_world_core_views(Value::Object& output);
 void append_world_spatial_views(Value::Object& output);
 [[nodiscard]] bool build_world_sdk_contract_value(Value& output) noexcept;
+/** Builds the mission Lua API schema published as sunrise/mission_api.json. */
+[[nodiscard]] bool build_mission_api_value(Value& output) noexcept;
+/** Appends the LuaLS classes of every mission API receiver, event and program entry. */
+void append_mission_api_luals(std::string& output);
 [[nodiscard]] bool render_activity_files(const Source& source, Bundle& output) noexcept;
 [[nodiscard]] bool render_contract_files(const Source& source, Bundle& output) noexcept;
 void append_actor_sequence_contract(const Source& source, std::string& output);

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

@@ -290,12 +290,6 @@ constexpr std::array<RowSpec, 40> kRows{{
      "u64:writer_evidence_address;u64:reader_evidence_address"},
 }};
 
-void sort_keys(Value::Object& value) {
-    std::sort(value.begin(), value.end(), [](const auto& first, const auto& second) {
-        return first.first < second.first;
-    });
-}
-
 /** Splits the semicolon list into type:name pairs; false on a bad token or a refusing callback. */
 template <typename Callback>
 [[nodiscard]] bool for_each_field(std::string_view fields, Callback&& callback) {

+ 19 - 257
Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp

@@ -8,11 +8,11 @@
 
 namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
 
-/** Emits only generated activity identity and panel-data declarations. */
+/** Emits the manifest, the mission API schema, the LuaLS declarations and the behavior module. */
 bool render_contract_files(const Source& source, Bundle& output) noexcept {
     try {
         const Value manifest = object({
-            {"schema", string("sunrise-activity-sdk-v8")},
+            {"schema", string("sunrise-activity-sdk-v11")},
             {"format_version", number(format::kVersion)},
             {"sdk_build_id", string(digest_hex(source.sdkBuildSha256, true))},
             {"sdk_payload_sha256", string(digest_hex(source.sdkPayloadSha256, false))},
@@ -56,177 +56,17 @@ bool render_contract_files(const Source& source, Bundle& output) noexcept {
             return false;
         }
         output.manifestJson.push_back('\n');
+        Value missionApi{};
+        if (!build_mission_api_value(missionApi)
+            || !render_json(missionApi, 0, output.missionApiJson)) {
+            return false;
+        }
+        output.missionApiJson.push_back('\n');
         output.activitySdkModule =
-            R"lua(-- Generated LuaLS declarations and stable SDK enums. Do not edit.
----@enum SunriseEventKind
-local EventKind = {
-    SENSOR_SENSE_UPDATED = 0,
-    CLIENT_STATE_CHANGED = 1,
-    CLIENT_MESSAGE_RECEIVED = 2,
-    AUTH_STATE_COMMITTED = 3,
-    AUTH_STATE_TRANSPORT_STAGED = 4,
-    AUTH_STATE_CANCELED = 5,
-    INCIDENT_RECEIVED = 6,
-    INCIDENT_QUEUED = 7,
-    INCIDENT_TRANSPORT_STAGED = 8,
-    INCIDENT_CANCELED = 9,
-    INCIDENT_REFUSED = 10,
-    SCRIPTABLE_OVERRIDE_COMMITTED = 11,
-    SCRIPTABLE_OVERRIDE_TRANSPORT_STAGED = 12,
-    SCRIPTABLE_OVERRIDE_CANCELED = 13,
-    OPERATOR_REFUSED = 14,
-    TIMER_ELAPSED = 15,
-    EFFECT_RESULT = 16,
-    PHASE_ENTERED = 17,
-    TRIGGER_ENTERED = 18,
-    TRIGGER_EXITED = 19,
-    SQUAD_STATE = 20,
-    ENTITY_SPAWNED = 21,
-    ENTITY_DIED = 22,
-    SCENE_FINISHED = 23,
-    OBJECTIVE_PROGRESS = 24,
-    ENTITY_SLOTS_REQUESTED = 25,
-    SESSION_JOINED = 26,
-    SESSION_LEFT = 27,
-    PLAYER_TRIGGER = 28,
-    CINEMATIC_STARTED = 29,
-    CINEMATIC_TERMINATED = 30,
-    GHOST_LINK_STATE = 31,
-    ACTOR_PATH_STATE = 32,
-    OBJECT_INTERACTED = 33,
-    CINEMATIC_SKIP_REQUESTED = 34,
-    FIRETEAM_STATE = 35,
-    OBJECT_STATE = 36,
-    DAMAGE_STATE = 37,
-    SQUAD_PROVOKED = 38,
-    DEVICE_STATE = 39,
-    REGION_CHANGED = 40,
-}
-
----@class SunriseEvent
----@field kind SunriseEventKind
----@field sequence string
----@field source_generation string
----@field attempt_generation string
----@field mission_sequence string? Input order; absent on delivery lifecycle events.
----@field slot SunriseSlot?
-
----@alias SunriseEventHandler fun(context: any, state: SunriseState, event: SunriseEvent)
-
----@class SunrisePlayerTriggerEvent: SunriseEvent
----@field registry_key integer
----@field object_tag integer
----@field slot_type integer
----@field slot_index integer
----@field volume_registry_key integer
----@field volume_slot_type integer
----@field volume_slot_index integer
----@field resolved_object_id integer
-
----@class SunriseCinematicEvent: SunriseEvent
----@field registry_key integer
----@field object_tag integer
----@field slot_type integer
----@field slot_index integer
----@field runtime_object_id string
----@field event_value number
-
----@class SunriseEntitySlotsRequestedEvent: SunriseEvent
----@field requested_count integer
-
----@class SunriseObjectStateEvent: SunriseEvent
----@field generation integer
----@field present boolean
----@field alive boolean
----@field interaction_open boolean
----@field owner_known boolean
----@field has_owner boolean
----@field owner_key string?
-
----@class SunriseGhostLinkLevel
----@field generation integer
----@field progress number
----@field active boolean
-
----@class SunriseRegionChangedEvent: SunriseEvent
----@field region_index integer
----@field previous_region_index integer?
-
----@class SunriseSquadStateEvent: SunriseEvent
----@field task_cost fun(self: SunriseSquadStateEvent, )lua"
-            R"lua(args: {group: SunriseCombatTaskGroup}): number|nil, boolean
----@field task_group fun(self: SunriseSquadStateEvent, )lua"
-            R"lua(args: {objective: SunriseSlot}): SunriseCombatTaskGroup|nil, boolean
-
----@class SunriseDeviceStateEvent: SunriseEvent
----@field registry_key integer
----@field object_tag integer
----@field slot_type integer
----@field slot_index integer
----@field position number?
----@field power number?
----@field lock number?
----@field position_sequence integer?
----@field power_sequence integer?
----@field lock_sequence integer?
----@field first_report boolean
----@field reset boolean
----@field applied_request fun(self: SunriseDeviceStateEvent, )lua"
-            R"lua(args: {channel: any}): SunriseRequestKey|nil
-
----@class SunriseProgram
----@field on_start? fun(context: any, state: SunriseState)
----@field on_load? fun(context: any, state: SunriseState)
----@field on_event_sensor_sense_updated? SunriseEventHandler
----@field on_event_client_state_changed? SunriseEventHandler
----@field on_event_client_message_received? SunriseEventHandler
----@field on_event_auth_state_committed? SunriseEventHandler
----@field on_event_auth_state_transport_staged? SunriseEventHandler
----@field on_event_auth_state_canceled? SunriseEventHandler
----@field on_event_incident_received? SunriseEventHandler
----@field on_event_incident_queued? SunriseEventHandler
----@field on_event_incident_transport_staged? SunriseEventHandler
----@field on_event_incident_canceled? SunriseEventHandler
----@field on_event_incident_refused? SunriseEventHandler
----@field on_event_scriptable_override_committed? SunriseEventHandler
----@field on_event_scriptable_override_transport_staged? SunriseEventHandler
----@field on_event_scriptable_override_canceled? SunriseEventHandler
----@field on_event_operator_refused? SunriseEventHandler
----@field on_event_timer_elapsed? SunriseEventHandler
----@field on_event_effect_result? SunriseEventHandler
----@field on_event_phase_entered? SunriseEventHandler
----@field on_event_trigger_entered? SunriseEventHandler
----@field on_event_trigger_exited? SunriseEventHandler
----@field on_event_squad_state? SunriseEventHandler
----@field on_event_squad_provoked? SunriseEventHandler
----@field on_event_region_changed? fun(ctx: any, state: SunriseState, )lua"
-            R"lua(event: SunriseRegionChangedEvent)
----@field on_event_device_state? fun(ctx: any, state: SunriseState, event: SunriseDeviceStateEvent)
----@field on_event_entity_spawned? fun(context: any, state: SunriseState, event: SunriseEvent)
----@field on_event_entity_died? fun(context: any, state: SunriseState, event: SunriseEvent)
----@field on_event_scene_finished? fun(context: any, state: SunriseState, event: SunriseEvent)
----@field on_event_objective_progress? fun(context: any, state: SunriseState, event: SunriseEvent)
----@field on_event_entity_slots_requested? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunriseEntitySlotsRequestedEvent)
----@field on_event_session_joined? fun(context: any, state: SunriseState, event: SunriseEvent)
----@field on_event_session_left? fun(context: any, state: SunriseState, event: SunriseEvent)
----@field on_event_player_trigger? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunrisePlayerTriggerEvent)
----@field on_event_cinematic_started? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunriseCinematicEvent)
----@field on_event_cinematic_terminated? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunriseCinematicEvent)
----@field on_event_actor_path_state? SunriseEventHandler
----@field on_event_ghost_link_state? SunriseEventHandler
----@field on_event_object_interacted? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunriseObjectStateEvent)
----@field on_event_fireteam_state? SunriseEventHandler
----@field on_event_object_state? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunriseObjectStateEvent)
----@field on_event_damage_state? SunriseEventHandler
----@field on_event_cinematic_skip_requested? fun(context: any, state: SunriseState, )lua"
-            R"lua(event: SunriseCinematicEvent)
-
+            "-- Generated LuaLS declarations and stable SDK enums. Do not edit.\n";
+        append_mission_api_luals(output.activitySdkModule);
+        output.activitySdkModule +=
+            R"lua(
 ---@class SunriseVector3
 ---@field x number
 ---@field y number
@@ -247,77 +87,11 @@ local EventKind = {
 ---@field auth_dynamic boolean|nil
 ---@field auth_writable boolean|nil
 ---@field unresourced boolean|nil
----@field set_object_active fun(SunriseSlot, SunriseObjectArguments?): SunriseRequestKey
----@field applied fun(self: SunriseSlot, args: {channel: any}): boolean
----@field run_atoms fun(self: SunriseSlot, )lua"
-            R"lua(args: {spawn: boolean?, atoms: table[]}): SunriseRequestKey
----@field assign_combat_objective fun(self: SunriseSlot, )lua"
-            R"lua(args: {objective: SunriseSlot, )lua"
-            R"lua(task_group: SunriseCombatTaskGroup?, reconsider: boolean?, )lua"
-            R"lua(reserved: boolean?, )lua"
-            R"lua(refresh_player_awareness: boolean?}): SunriseRequestKey
----@field play_actor_path fun(self: SunriseSlot, )lua"
-            R"lua(args: {path: SunriseSlot, spawn: boolean?}): SunriseRequestKey
----@field play_actor_action fun(self: SunriseSlot, )lua"
-            R"lua(args: {ability: SunriseActorAbility, )lua"
-            R"lua(target: SunriseSlot?, spawn: boolean?}): SunriseRequestKey
----@field retire_actor fun(self: SunriseSlot, args: table?): SunriseRequestKey
----@field set_darkness_zone fun(self: SunriseSlot, args: {enabled: boolean, )lua"
-            R"lua(wipe_seconds: integer?}): SunriseRequestKey
----@field set_interactable_object fun(self: SunriseSlot, args: {track_owner: boolean?, )lua"
-            R"lua(active: boolean?, used: boolean?}): SunriseRequestKey
----@field set_ghost_link fun(self: SunriseSlot, args: {active: boolean?}): SunriseRequestKey
----@field ghost_link fun(self: SunriseSlot, args: table?): SunriseGhostLinkLevel|nil
----@field set_music_section fun(self: SunriseSlot, args: {section: integer, )lua"
-            R"lua(enabled: boolean?}): SunriseRequestKey
----@field watch_damage fun(self: SunriseSlot, args: {target: SunriseSlot}): SunriseRequestKey
----@field set_occupancy_condition fun(self: SunriseSlot, )lua"
-            R"lua(args: {value: integer, filter: SunriseSlot?}): SunriseRequestKey
----@field generate_map fun(self: SunriseSlot, args: {record: integer?, seed: integer?, )lua"
-            R"lua(mode: integer?, enabled: boolean?, anchors: table[]?, values: integer[]?, )lua"
-            R"lua(reals: number[]?, state_key: integer?}): SunriseRequestKey
----@field set_object_filter fun(self: SunriseSlot, args: {players: boolean?, )lua"
-            R"lua(target: SunriseSlot?, inside: SunriseSlot?, )lua"
-            R"lua(inside_any: SunriseSlot[]?}): SunriseRequestKey
----@field bind_combatant_to_squad fun(self: SunriseSlot): SunriseRequestKey
----@field transition fun(SunriseSlot, SunriseDeviceTransitionArguments): SunriseRequestKey
----@field set_channel fun(self: SunriseSlot, )lua"
-            R"lua(args: {channel: any, value: any, snap: boolean?}): SunriseRequestKey
----@field fire_trigger fun(self: SunriseSlot): SunriseRequestKey
----@field disarm_trigger fun(self: SunriseSlot): SunriseRequestKey
----@field sequences fun(self: SunriseSlot): SunriseActorSequences
----@field play_sequence fun(self: SunriseSlot, args: {sequence: SunriseActorSequence}?): )lua"
-            R"lua(SunriseRequestKey
----@field set_cinematic_active fun(SunriseSlot, SunriseCinematicArguments?): SunriseRequestKey
----@field reset_objectives fun(self: SunriseSlot): SunriseRequestKey
----@field advance_task fun(self: SunriseSlot): SunriseRequestKey
----@field play_performance fun(self: SunriseSlot, args: {state: table?}?): SunriseRequestKey
----@field play_dialogue_cue fun(SunriseSlot, SunriseDialogueCueArguments): SunriseRequestKey
----@field set_directive fun(SunriseSlot, SunriseDirectiveArguments): SunriseRequestKey
----@field clear_directives fun(self: SunriseSlot): SunriseRequestKey
----@field set_public_event_state fun(SunriseSlot, SunrisePublicEventArguments): SunriseRequestKey
-
----@class SunriseRequestKey
-
----@class SunriseCinematicArguments
----@field active? boolean
-
----@class SunriseObjectArguments
----@field active? boolean
----@field with? string[] Up to 63 more type-4 slots sent on the same request.
-
-
----@class SunriseDialogueCueArguments
----@field cue integer
----@field filter? SunriseSlot
-
----@class SunriseDeviceTransitionArguments
----@field transition any Generated device transition value.
----@field snap? boolean Jump to the end value instead of moving.
 
 ---@class SunriseDirective
 ---@field id string
 ---@field slot_row integer
+---@field sensor string The directive sensor's mission.Slot value.
 ---@field name_hash integer
 ---@field element integer
 ---@field title string
@@ -325,21 +99,6 @@ local EventKind = {
 ---@field progress string|nil Label shown before the counter, when authored.
 ---@field counter boolean|nil True when the HUD shows the lane's progress values.
 
----@class SunriseDirectiveArguments
----@field directive SunriseDirective Generated mission directive declaration.
----@field state? integer Defaults to 0, the native enter state.
----@field progress? integer[] One to four lane values, current then maximum; counter elements only.
----@field audience? SunriseSlot Authored type-70 engagement sensor for the mission banner.
----@field navpoint? SunriseSlot Authored type-47 navigation marker.
----@field waypoint? SunriseSlot Authored type-60 volume; inside it the HUD marker hides.
-
-
----@class SunrisePublicEventArguments
----@field state? integer Shown unchanged by the HUD directive that names this sensor. Defaults to 0.
----@field player? string Decimal player key of the watched player. Defaults to context.player_key.
----@field area SunriseSlot The object whose zone list bounds the event area.
----@field leave_seconds number Seconds outside that area before the client reports Sense 1.
-
 ---@class SunriseState
 ---@field id string
 ---@field ordinal integer
@@ -369,9 +128,6 @@ local EventKind = {
 ---@field slot integer
 ---@field config_tag integer
 ---@field resource_tag integer
----@field activate fun(self: SunriseScene, args: {spawn: boolean?}?): SunriseRequestKey
----@field stop fun(self: SunriseScene, args: table?): SunriseRequestKey
----@field send_event fun(self: SunriseScene, args: {key: integer}): SunriseRequestKey
 ---@field event_keys integer[]|nil
 
 ---@class SunriseTaskTarget
@@ -417,11 +173,16 @@ local EventKind = {
 ---@field Auth table<string, table<string, string>> Auth slots grouped by their native Auth type.
 ---@field Squad table<string, string>
 ---@field Scene table<string, string> Exact occurrence-bound authored scene identities.
+---@field SceneInput table<string, table<string, integer>> Keys a scene waits on, by trigger name.
+---@field SceneOutput table<string, table<string, integer>> Keys a scene reports, by trigger name.
 ---@field Task table<string, string>
 ---@field TaskGroup table<string, table<string, SunriseCombatTaskGroup>>
 ---@field ActorAbility table<string, table<string, table<string, SunriseActorAbility>>>
 ---@field TriggerVolume table<string, SunriseTriggerVolume>
 ---@field Directive table<string, SunriseDirective>
+---@field PerformanceState table<string, table<string, {slot_row: integer, )lua"
+            R"lua(name_hash: integer, ordinal: integer}>>
+---@field DialogueCue table<string, table<string, integer>>
 
 ---@class SunriseActivity
 ---@field client_teleport_reset integer
@@ -727,6 +488,7 @@ sdk.ObjectFilterPredicateSchema = {
         return true;
     } catch (...) {
         output.manifestJson.clear();
+        output.missionApiJson.clear();
         output.activitySdkModule.clear();
         output.behaviorModule.clear();
         return false;

+ 369 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_mission_api.cpp

@@ -0,0 +1,369 @@
+// The mission Lua API schema. It is rendered from the same table the mission VM dispatches from,
+// so the published file and the runtime cannot disagree.
+
+#include <cstddef>
+#include <string>
+#include <string_view>
+
+#include "../../../server/activity/mission/mission_api.h"
+#include "activity_sdk_lua_artifacts_internal.h"
+
+namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
+namespace {
+
+namespace api = ::sunrise::server::activity::mission::api;
+
+/** @return The schema word of one value kind. */
+[[nodiscard]] constexpr std::string_view value_kind_name(api::ValueKind kind) noexcept {
+    using Kind = api::ValueKind;
+    switch (kind) {
+    case Kind::boolean:
+        return "boolean";
+    case Kind::integer:
+        return "integer";
+    case Kind::number:
+        return "number";
+    case Kind::string:
+        return "string";
+    case Kind::decimalString:
+        return "decimal_string";
+    case Kind::name:
+        return "name";
+    case Kind::scalar:
+        return "scalar";
+    case Kind::selector:
+        return "selector";
+    case Kind::handle:
+        return "handle";
+    case Kind::enumValue:
+        return "enum";
+    case Kind::missionRow:
+        return "mission_row";
+    case Kind::list:
+        return "list";
+    case Kind::table:
+        return "table";
+    case Kind::atom:
+        return "atom";
+    case Kind::function:
+        return "function";
+    }
+    return "unknown";
+}
+
+/** @return The schema word of one member kind. */
+[[nodiscard]] constexpr std::string_view member_kind_name(api::MemberKind kind) noexcept {
+    using Kind = api::MemberKind;
+    switch (kind) {
+    case Kind::field:
+        return "field";
+    case Kind::request:
+        return "request";
+    case Kind::query:
+        return "query";
+    case Kind::lookup:
+        return "lookup";
+    case Kind::update:
+        return "update";
+    case Kind::maker:
+        return "maker";
+    }
+    return "unknown";
+}
+
+/** @return The schema word of one call form. */
+[[nodiscard]] constexpr std::string_view call_form_name(api::CallForm form) noexcept {
+    using Form = api::CallForm;
+    switch (form) {
+    case Form::none:
+        return "none";
+    case Form::named:
+        return "named";
+    case Form::positional:
+        return "positional";
+    }
+    return "unknown";
+}
+
+/** @return The schema word of one argument scope. */
+[[nodiscard]] constexpr std::string_view scope_name(api::Scope scope) noexcept {
+    using Scope = api::Scope;
+    switch (scope) {
+    case Scope::none:
+        return "none";
+    case Scope::receiver:
+        return "receiver";
+    case Scope::sameRegistry:
+        return "same_registry";
+    case Scope::sameObject:
+        return "same_object";
+    case Scope::argument:
+        return "argument";
+    }
+    return "unknown";
+}
+
+/** @return The slot properties a gate checks, in schema order. */
+[[nodiscard]] Value slot_checks(const api::SlotChecks& checks) {
+    Value::Array names{};
+    if (checks.componentClass) {
+        names.push_back(string("component_class"));
+    }
+    if (checks.senseSchema) {
+        names.push_back(string("sense_schema"));
+    }
+    if (checks.authSchema) {
+        names.push_back(string("auth_schema"));
+    }
+    if (checks.exactJoin) {
+        names.push_back(string("exact_join"));
+    }
+    if (checks.dialogueCues) {
+        names.push_back(string("dialogue_cues"));
+    }
+    return array(std::move(names));
+}
+
+/** @return One slot gate: the slot type and the properties checked against it. */
+[[nodiscard]] Value slot_gate(const api::SlotGate& gate) {
+    return object({{"slot_type", number(gate.slotType)}, {"checks", slot_checks(gate.checks)}});
+}
+
+/**
+ * Appends one bound under the names its kind gives it: a count for a list or a table's array
+ * part, a byte length for a string, and a value otherwise.
+ */
+void append_bound(const api::ValueSpec& value, Value::Object& output) {
+    const bool counted = value.kind == api::ValueKind::list || value.kind == api::ValueKind::table;
+    const bool length = value.kind == api::ValueKind::string || value.kind == api::ValueKind::name
+                        || value.kind == api::ValueKind::scalar;
+    const std::string_view low = counted ? "min_count" : length ? "min_length" : "minimum";
+    const std::string_view high = counted ? "max_count" : length ? "max_length" : "maximum";
+    if (value.bound.hasMinimum) {
+        output.emplace_back(std::string(low), signed_number(value.bound.minimum));
+    }
+    if (value.bound.hasMaximum) {
+        output.emplace_back(std::string(high), signed_number(value.bound.maximum));
+    }
+}
+
+/** @return One argument, field, return value or nested value of the table. */
+[[nodiscard]] Value value_object(const api::ValueSpec& value) {
+    Value::Object output{{"kind", string(value_kind_name(value.kind))},
+                         {"optional", boolean(value.optional)}};
+    if (!value.name.empty()) {
+        output.emplace_back("name", string(value.name));
+    }
+    if (!value.ref.empty()) {
+        output.emplace_back("ref", string(value.ref));
+    }
+    switch (value.fallback.kind) {
+    case api::DefaultKind::none:
+        break;
+    case api::DefaultKind::boolean:
+        output.emplace_back("default", boolean(value.fallback.boolean));
+        break;
+    case api::DefaultKind::integer:
+        output.emplace_back("default", signed_number(value.fallback.integer));
+        break;
+    case api::DefaultKind::text:
+        output.emplace_back("default_text", string(value.fallback.text));
+        break;
+    }
+    append_bound(value, output);
+    if (value.slot.slotType != api::kAnySlotType) {
+        output.emplace_back("slot_gate", slot_gate(value.slot));
+    }
+    if (value.scope != api::Scope::none) {
+        output.emplace_back("scope", string(scope_name(value.scope)));
+    }
+    if (value.handleAccepted) {
+        output.emplace_back("handle_accepted", boolean(true));
+    }
+    if (!value.element.empty()) {
+        output.emplace_back("element", value_object(value.element.front()));
+    }
+    if (!value.fields.empty()) {
+        Value::Array fields{};
+        for (const api::ValueSpec& field : value.fields) {
+            fields.push_back(value_object(field));
+        }
+        output.emplace_back("fields", array(std::move(fields)));
+    }
+    if (!value.note.empty()) {
+        output.emplace_back("note", string(value.note));
+    }
+    return object(std::move(output));
+}
+
+/** @return A list of values in table order. */
+[[nodiscard]] Value value_array(std::span<const api::ValueSpec> values) {
+    Value::Array output{};
+    for (const api::ValueSpec& value : values) {
+        output.push_back(value_object(value));
+    }
+    return array(std::move(output));
+}
+
+/**
+ * @return One member. A field carries its type; a callable carries its call form, gate,
+ * arguments, returns and effect name.
+ */
+[[nodiscard]] Value
+member_object(std::string_view owner, std::string_view ownerKey, const api::MemberSpec& member) {
+    Value::Object output{{std::string(ownerKey), string(owner)},
+                         {"name", string(member.name)},
+                         {"kind", string(member_kind_name(member.kind))}};
+    if (member.kind == api::MemberKind::field) {
+        output.emplace_back("type", value_object(member.returns.front()));
+    } else {
+        output.emplace_back("call", string(call_form_name(member.call)));
+        output.emplace_back("colon", boolean(member.colon));
+        output.emplace_back("arguments", value_array(member.arguments));
+        output.emplace_back("returns", value_array(member.returns));
+        if (member.call == api::CallForm::named) {
+            output.emplace_back("refuses_unknown_keys", boolean(true));
+        }
+    }
+    if (member.gate.slotType != api::kAnySlotType) {
+        output.emplace_back("gate", slot_gate(member.gate));
+    }
+    if (!member.effect.empty()) {
+        output.emplace_back("effect", string(member.effect));
+    }
+    if (!member.note.empty()) {
+        output.emplace_back("note", string(member.note));
+    }
+    return object(std::move(output));
+}
+
+/** Appends every receiver and, separately, every member in receiver then table order. */
+void append_receivers(Value::Object& output) {
+    Value::Array receivers{};
+    Value::Array members{};
+    for (const api::ReceiverSpec& receiver : api::kReceivers) {
+        Value::Object entry{{"name", string(receiver.name)},
+                            {"lua_class", string(receiver.luaClass)},
+                            {"complete", boolean(receiver.complete)},
+                            {"source", string(receiver.source)},
+                            {"member_count", number(receiver.members.size())}};
+        if (!receiver.entries.empty()) {
+            entry.emplace_back("entries", string(receiver.entries));
+        }
+        if (!receiver.dynamicKeys.empty()) {
+            entry.emplace_back("dynamic_keys", string(receiver.dynamicKeys));
+        }
+        receivers.push_back(object(std::move(entry)));
+        for (const api::MemberSpec& member : receiver.members) {
+            members.push_back(member_object(receiver.name, "receiver", member));
+        }
+    }
+    output.emplace_back("receivers", array(std::move(receivers)));
+    output.emplace_back("members", array(std::move(members)));
+}
+
+/** @return Every event kind with its callback and every field its view serves. */
+[[nodiscard]] Value events_value() {
+    Value::Array events{};
+    for (const api::EventSpec& event : api::kEvents) {
+        Value::Array groups{};
+        Value::Array fields{};
+        for (const api::FieldGroup* group : event.groups) {
+            groups.push_back(string(group->name));
+            for (const api::MemberSpec& member : group->members) {
+                fields.push_back(member_object(group->name, "group", member));
+            }
+        }
+        Value::Object entry{{"kind", number(event.kind)},
+                            {"name", string(event.screaming)},
+                            {"callback", string(event.callback)},
+                            {"lua_class", string(event.luaClass)},
+                            {"groups", array(std::move(groups))},
+                            {"fields", array(std::move(fields))}};
+        if (!event.note.empty()) {
+            entry.emplace_back("note", string(event.note));
+        }
+        events.push_back(object(std::move(entry)));
+    }
+    return array(std::move(events));
+}
+
+/** @return The program table keys, the refused keys and the callback parameters. */
+[[nodiscard]] Value program_value() {
+    Value::Array refused{};
+    for (const std::string_view key : api::kRefusedProgramKeys) {
+        refused.push_back(string(key));
+    }
+    return object({{"entries", value_array(api::kProgramEntries)},
+                   {"refused", array(std::move(refused))},
+                   {"start_parameters", string_array({"context", "state"})},
+                   {"event_parameters", string_array({"context", "state", "event"})}});
+}
+
+/** @return One closed collection: its entries, or its range and default. */
+[[nodiscard]] Value enum_value(const api::EnumSpec& collection) {
+    Value::Array entries{};
+    // Atom rows index kAtomKinds, so match the entries by identity, not by name.
+    const bool atoms = collection.entries.data() == api::kAtomKindEntries.data();
+    for (std::size_t index = 0; index < collection.entries.size(); ++index) {
+        const api::EnumEntry& entry = collection.entries[index];
+        Value::Object row{{"name", string(entry.name)},
+                          {std::string(collection.valueName), signed_number(entry.value)}};
+        if (!collection.detailName.empty()) {
+            row.emplace_back(std::string(collection.detailName), string(entry.detail));
+        }
+        if (atoms) {
+            const api::AtomKindSpec& kind = api::kAtomKinds[index];
+            row.emplace_back("fields", value_array(kind.fields));
+            row.emplace_back("closed", boolean(kind.closed));
+            row.emplace_back("note", string(kind.note));
+        }
+        entries.push_back(object(std::move(row)));
+    }
+    Value::Object output{{"name", string(collection.name)},
+                         {"lua_class", string(collection.luaClass)},
+                         {"entries", array(std::move(entries))},
+                         {"note", string(collection.note)}};
+    if (collection.range.hasMinimum) {
+        output.emplace_back("minimum", signed_number(collection.range.minimum));
+        output.emplace_back("maximum", signed_number(collection.range.maximum));
+    }
+    if (collection.hasDefault) {
+        output.emplace_back("default", signed_number(collection.defaultValue));
+    }
+    return object(std::move(output));
+}
+
+/** @return Every schema word a value kind can take, in vocabulary order. */
+[[nodiscard]] Value value_kinds_value() {
+    Value::Array output{};
+    for (std::size_t kind = 0; kind <= static_cast<std::size_t>(api::ValueKind::function); ++kind) {
+        output.push_back(string(value_kind_name(static_cast<api::ValueKind>(kind))));
+    }
+    return array(std::move(output));
+}
+
+} // namespace
+
+/** Builds the schema; on failure the output is left empty. */
+bool build_mission_api_value(Value& output) noexcept {
+    try {
+        Value::Object root{{"schema", string("sunrise-mission-lua-api-v1")},
+                           {"value_kinds", value_kinds_value()},
+                           {"events", events_value()},
+                           {"program", program_value()}};
+        append_receivers(root);
+        Value::Array enums{};
+        for (const api::EnumSpec& collection : api::kEnums) {
+            enums.push_back(enum_value(collection));
+        }
+        root.emplace_back("enums", array(std::move(enums)));
+        output = object(std::move(root));
+        return true;
+    } catch (...) {
+        output = null_value();
+        return false;
+    }
+}
+
+} // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal

+ 338 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_mission_luals.cpp

@@ -0,0 +1,338 @@
+// LuaLS declarations of the mission Lua API, written from the table the mission VM dispatches
+// from: one class per receiver, one per event view, the program table and the atom shape.
+
+#include <cstddef>
+#include <map>
+#include <set>
+#include <string>
+#include <string_view>
+
+#include "../../../server/activity/mission/mission_api.h"
+#include "activity_sdk_lua_artifacts_internal.h"
+
+namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
+namespace {
+
+namespace api = ::sunrise::server::activity::mission::api;
+
+/** One generated mission table and the LuaLS class of its rows. */
+struct RowClass final {
+    std::string_view table{};
+    std::string_view luaClass{};
+};
+
+// Generated mission tables whose rows already have a LuaLS class.
+constexpr RowClass kRowClasses[]{
+    {"mission.Directive", "SunriseDirective"},
+    {"mission.TaskGroup", "SunriseCombatTaskGroup"},
+    {"mission.ActorAbility", "SunriseActorAbility"},
+    {"mission.states", "SunriseState"},
+    {"mission.Slot", "SunriseSlotHandle"},
+    {"mission.Squad", "SunriseSquadHandle"},
+    {"mission.Scene", "SunriseSceneHandle"},
+};
+
+/** @return The LuaLS class of one generated mission table's rows, or empty when it has none. */
+[[nodiscard]] std::string_view row_class(std::string_view table) noexcept {
+    for (const RowClass& row : kRowClasses) {
+        if (row.table == table) {
+            return row.luaClass;
+        }
+    }
+    return {};
+}
+
+/** @return The LuaLS class of one receiver, or a plain table for a view this table omits. */
+[[nodiscard]] std::string_view receiver_class(std::string_view name) noexcept {
+    if (name == "event") {
+        return "SunriseEvent";
+    }
+    for (const api::ReceiverSpec& receiver : api::kReceivers) {
+        if (receiver.name == name) {
+            return receiver.luaClass;
+        }
+    }
+    return "table";
+}
+
+/** @return The LuaLS class of one context.sdk collection's entries. */
+[[nodiscard]] std::string_view enum_class(std::string_view name) noexcept {
+    for (const api::EnumSpec& collection : api::kEnums) {
+        if (collection.name == name) {
+            return collection.luaClass;
+        }
+    }
+    return "any";
+}
+
+[[nodiscard]] std::string lua_type(const api::ValueSpec& value);
+
+/** @return An inline LuaLS table type with the value's array part and named fields. */
+[[nodiscard]] std::string inline_table(const api::ValueSpec& value) {
+    std::string output = "{";
+    const char* separator = "";
+    if (!value.element.empty()) {
+        output.append("[integer]: ").append(lua_type(value.element.front()));
+        separator = ", ";
+    }
+    for (const api::ValueSpec& field : value.fields) {
+        output.append(separator).append(field.name).append(field.optional ? "?: " : ": ");
+        output.append(lua_type(field));
+        separator = ", ";
+    }
+    return output.append("}");
+}
+
+/** @return The LuaLS type of one value, without its optional marker. */
+[[nodiscard]] std::string lua_type(const api::ValueSpec& value) {
+    using Kind = api::ValueKind;
+    switch (value.kind) {
+    case Kind::boolean:
+        return "boolean";
+    case Kind::integer:
+        return value.ref == "EventKind" ? "SunriseEventKind" : "integer";
+    case Kind::number:
+        return "number";
+    case Kind::string:
+    case Kind::decimalString:
+    case Kind::name:
+        return "string";
+    case Kind::scalar:
+        return "boolean|integer|number|string";
+    case Kind::selector:
+        return value.handleAccepted ? "string|integer|" + std::string(row_class(value.ref))
+                                    : "string|integer";
+    case Kind::handle:
+        return std::string(receiver_class(value.ref));
+    case Kind::enumValue:
+        return std::string(enum_class(value.ref));
+    case Kind::missionRow: {
+        const std::string_view known = row_class(value.ref);
+        return !known.empty()         ? std::string(known)
+               : value.fields.empty() ? "table"
+                                      : inline_table(value);
+    }
+    case Kind::list: {
+        const std::string element = lua_type(value.element.front());
+        return element.find('|') == std::string::npos ? element + "[]" : "(" + element + ")[]";
+    }
+    case Kind::table: {
+        if (value.ref == "atom_kinds") {
+            return "table<string, string>";
+        }
+        const std::string_view known = row_class(value.ref);
+        return known.empty() ? inline_table(value) : std::string(known);
+    }
+    case Kind::atom:
+        return "SunriseAtom";
+    case Kind::function:
+        return "function";
+    }
+    return "any";
+}
+
+/** @return The LuaLS type of a read value, with nil when it may be absent. */
+[[nodiscard]] std::string read_type(const api::ValueSpec& value) {
+    std::string type = lua_type(value);
+    return value.optional ? type.append("|nil") : type;
+}
+
+/** @return The note a slot method's gate adds: its slot type and whether it is exact. */
+[[nodiscard]] std::string gate_note(const api::SlotGate& gate) {
+    if (gate.slotType == api::kAnySlotType) {
+        return {};
+    }
+    std::string note = "Slot type " + std::to_string(gate.slotType);
+    return note.append(gate.checks.exactJoin ? " (exact)." : ".");
+}
+
+/** @return The LuaLS function type of one callable member. */
+[[nodiscard]] std::string function_type(std::string_view self, const api::MemberSpec& member) {
+    std::string output = "fun(";
+    const char* separator = "";
+    if (member.colon) {
+        output.append("self: ").append(self);
+        separator = ", ";
+    }
+    if (member.call == api::CallForm::named) {
+        bool required = false;
+        api::ValueSpec table{.kind = api::ValueKind::table, .fields = member.arguments};
+        for (const api::ValueSpec& argument : member.arguments) {
+            required = required || !argument.optional;
+        }
+        output.append(separator)
+            .append(required ? "args: " : "args?: ")
+            .append(inline_table(table));
+    } else {
+        for (const api::ValueSpec& argument : member.arguments) {
+            output.append(separator).append(argument.name).append(argument.optional ? "?: " : ": ");
+            output.append(lua_type(argument));
+            separator = ", ";
+        }
+    }
+    output.append(")");
+    separator = ": ";
+    for (const api::ValueSpec& value : member.returns) {
+        output.append(separator).append(read_type(value));
+        separator = ", ";
+    }
+    return output;
+}
+
+/** Appends one ---@field line, with the note after the type when there is one. */
+void append_field(std::string& output,
+                  std::string_view name,
+                  std::string_view type,
+                  std::string_view note) {
+    output.append("---@field ").append(name).append(" ").append(type);
+    if (!note.empty()) {
+        output.append(" ").append(note);
+    }
+    output.push_back('\n');
+}
+
+/** Appends every member of one list; overloads of one name become one union field. */
+void append_members(std::string& output,
+                    std::string_view self,
+                    std::span<const api::MemberSpec> members) {
+    for (std::size_t index = 0; index < members.size(); ++index) {
+        const api::MemberSpec& member = members[index];
+        if (member.kind == api::MemberKind::field) {
+            const api::ValueSpec& type = member.returns.front();
+            append_field(output,
+                         member.name,
+                         read_type(type),
+                         member.note.empty() ? type.note : member.note);
+            continue;
+        }
+        bool repeated = false;
+        for (std::size_t earlier = 0; earlier < index; ++earlier) {
+            repeated = repeated || members[earlier].name == member.name;
+        }
+        if (repeated) {
+            continue;
+        }
+        std::string type{};
+        std::string note{};
+        std::size_t overloads = 0;
+        for (std::size_t later = index; later < members.size(); ++later) {
+            const api::MemberSpec& overload = members[later];
+            if (overload.name != member.name) {
+                continue;
+            }
+            // One signature is written bare; several become a union of parenthesized ones.
+            const std::string signature = function_type(self, overload);
+            ++overloads;
+            if (overloads == 1) {
+                type = signature;
+            } else {
+                if (overloads == 2) {
+                    type.insert(0, "(").append(")");
+                }
+                type.append("|(").append(signature).append(")");
+            }
+            std::string part = gate_note(overload.gate);
+            if (!overload.note.empty()) {
+                part.append(part.empty() ? "" : " ").append(overload.note);
+            }
+            note.append(note.empty() || part.empty() ? "" : " Or: ").append(part);
+        }
+        append_field(output, member.name, type, note);
+    }
+}
+
+/** @return A program callback type; an empty event class means no event parameter. */
+[[nodiscard]] std::string handler_type(std::string_view event) {
+    std::string output = "fun(context: SunriseContext, state: SunriseStateView";
+    if (!event.empty()) {
+        output.append(", event: ").append(event);
+    }
+    return output.append(")");
+}
+
+/** Appends the EventKind enum, the event classes and the program table. */
+void append_events(std::string& output) {
+    output.append("---@enum SunriseEventKind\nlocal EventKind = {\n");
+    for (const api::EventSpec& event : api::kEvents) {
+        output.append("    ").append(event.screaming).append(" = ");
+        output.append(std::to_string(event.kind)).append(",\n");
+    }
+    output.append("}\n\n---@class SunriseEvent\n");
+    const api::FieldGroup* const common = api::group(api::FieldGroupId::common);
+    append_members(output, "SunriseEvent", common->members);
+    output.append("\n---@alias SunriseEventHandler ")
+        .append(handler_type("SunriseEvent"))
+        .append("\n");
+    std::set<std::string_view> emitted{};
+    for (const api::EventSpec& event : api::kEvents) {
+        if (!emitted.insert(event.luaClass).second) {
+            continue;
+        }
+        output.append("\n---@class ").append(event.luaClass).append(": SunriseEvent\n");
+        for (const api::FieldGroup* group : event.groups) {
+            if (group != common) {
+                append_members(output, event.luaClass, group->members);
+            }
+        }
+    }
+    output.append("\n---@class SunriseProgram\n");
+    for (const api::ValueSpec& entry : api::kProgramEntries) {
+        const std::string type =
+            entry.kind == api::ValueKind::function ? handler_type({}) : lua_type(entry);
+        append_field(output, std::string(entry.name) + "?", type, entry.note);
+    }
+    for (const api::EventSpec& event : api::kEvents) {
+        append_field(output, std::string(event.callback) + "?", handler_type(event.luaClass), {});
+    }
+}
+
+/** Appends the atom table shape: every field any atom kind reads, with the kinds that read it. */
+void append_atom(std::string& output) {
+    std::map<std::string_view, std::string> kinds{};
+    std::map<std::string_view, std::string> types{};
+    for (const api::AtomKindSpec& kind : api::kAtomKinds) {
+        for (const api::ValueSpec& field : kind.fields) {
+            std::string& readers = kinds[field.name];
+            readers.append(readers.empty() ? "read by " : ", ").append(kind.name);
+            // One name may carry a different type per kind, so the field takes their union.
+            const std::string type = lua_type(field);
+            std::string& known = types[field.name];
+            if (("|" + known + "|").find("|" + type + "|") == std::string::npos) {
+                known.append(known.empty() ? "" : "|").append(type);
+            }
+        }
+    }
+    output.append("\n---@class SunriseAtom\n");
+    for (const auto& [name, type] : types) {
+        const bool always = name == api::kAtomKindField.name;
+        append_field(output,
+                     always ? std::string(name) : std::string(name) + "?",
+                     type,
+                     always ? std::string_view("an atom_kinds name") : kinds[name]);
+    }
+}
+
+} // namespace
+
+/** Writes the declarations; every receiver class lists exactly the members the VM serves. */
+void append_mission_api_luals(std::string& output) {
+    append_events(output);
+    for (const api::ReceiverSpec& receiver : api::kReceivers) {
+        output.append("\n---@class ").append(receiver.luaClass).append("\n");
+        append_members(output, receiver.luaClass, receiver.members);
+        for (const api::EnumSpec& collection : api::kEnums) {
+            if (collection.name != receiver.entries) {
+                continue;
+            }
+            for (const api::EnumEntry& row : collection.entries) {
+                append_field(output, row.name, collection.luaClass, {});
+            }
+        }
+        if (receiver.id == api::Receiver::modConfig) {
+            append_field(output, "[string]", "integer|number|boolean|nil", receiver.dynamicKeys);
+        }
+    }
+    append_atom(output);
+}
+
+} // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal

+ 20 - 17
Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp

@@ -5,6 +5,7 @@
 #include <cstdint>
 #include <cstdio>
 #include <limits>
+#include <span>
 #include <string>
 #include <string_view>
 #include <unordered_map>
@@ -12,6 +13,7 @@
 #include <vector>
 
 #include "../../../middleware/bap/activity_message/auth_schema_catalog.h"
+#include "../../../state/activity_sdk/scene_triggers.h"
 #include "activity_sdk_lua_missions_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
@@ -332,6 +334,8 @@ bool render_mission(const Source& source,
     output.append("    },\n    scenes = {\n");
     std::unordered_set<std::string> sceneKeys;
     std::string sceneConstants = "mission.Scene = {\n";
+    std::string sceneInputConstants = "mission.SceneInput = {\n";
+    std::string sceneOutputConstants = "mission.SceneOutput = {\n";
     if (!range_inside(scenario.occurrences, source.occurrences.size())) {
         return false;
     }
@@ -388,26 +392,19 @@ bool render_mission(const Source& source,
             append_hex(output, scene.configTag);
             output.append(", resource_tag = ");
             append_hex(output, scene.resourceTag);
-            // The graph's gates in order: what set_scene_events publishes to run the scene.
-            // The section is sorted by scene slot, so the slot's rows are one range.
-            const auto& gates = source.authoredSceneEventKeys;
-            const auto firstGate = std::lower_bound(
-                gates.begin(), gates.end(), slotRow, [](const auto& row, std::uint32_t index) {
-                    return row.sceneSlotIndex < index;
-                });
-            const auto lastGate = std::upper_bound(
-                firstGate, gates.end(), slotRow, [](std::uint32_t index, const auto& row) {
-                    return index < row.sceneSlotIndex;
-                });
-            if (firstGate != lastGate) {
+            // The graph's gates in order; a script fires one with scene:send_event.
+            const auto gates =
+                state::activity_sdk::scene_gates(source.authoredSceneEventKeys, slotRow);
+            if (!gates.empty()) {
                 output.append(", event_keys = { ");
-                for (auto gate = firstGate; gate != lastGate; ++gate) {
-                    if (gate != firstGate) {
+                for (std::size_t gate = 0; gate < gates.size(); ++gate) {
+                    if (gate != 0) {
                         output.append(", ");
                     }
-                    append_hex(output, gate->key);
+                    append_hex(output, gates[gate].key);
                 }
                 output.append(" }");
+                append_scene_ports(key, gates, sceneInputConstants, sceneOutputConstants);
             }
             output.append(" },\n");
             sceneConstants.append("    ");
@@ -455,6 +452,8 @@ bool render_mission(const Source& source,
     slotConstants.append("}\n");
     squadConstants.append("}\n");
     sceneConstants.append("}\n");
+    sceneInputConstants.append("}\n");
+    sceneOutputConstants.append("}\n");
     taskConstants.append("}\n");
     std::string taskGroupConstants = "mission.TaskGroup = {\n";
     for (const std::uint32_t slot : slots) {
@@ -599,8 +598,7 @@ bool render_mission(const Source& source,
         for (const std::uint32_t rowIndex : found->second) {
             const format::DirectiveElement& row = source.directiveElements[rowIndex];
             const std::string_view progress = text(source, row.progress);
-            // Only a row with no description is new; a described row keeps the title key that
-            // shipped scripts use.
+            // A row with no description is keyed by its progress label; others keep the title key.
             const bool labelKey = !progress.empty() && text(source, row.description).empty();
             (void)append_unique_key(directiveConstants,
                                     directiveKeys,
@@ -610,6 +608,9 @@ bool render_mission(const Source& source,
             append_string(directiveConstants, text(source, row.id));
             directiveConstants.append(", slot_row = ");
             append_uint(directiveConstants, row.slotIndex);
+            // The sensor's mission.Slot value, so a script compares it without a row lookup.
+            directiveConstants.append(", sensor = ");
+            append_string(directiveConstants, text(source, source.slots[slot].id));
             directiveConstants.append(", name_hash = ");
             append_hex(directiveConstants, row.nameHash);
             directiveConstants.append(", element = ");
@@ -707,6 +708,8 @@ bool render_mission(const Source& source,
     append_catalog_constants(source, output);
     output.append(squadConstants);
     output.append(sceneConstants);
+    output.append(sceneInputConstants);
+    output.append(sceneOutputConstants);
     output.append(taskConstants);
     output.append(taskGroupConstants);
     output.append(abilityConstants);

+ 14 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_missions_internal.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include <cstdint>
+#include <span>
 #include <string>
 #include <string_view>
 #include <unordered_map>
@@ -35,6 +36,19 @@ void append_int(std::string& output, std::int32_t value);
 /** Appends the catalog-wide constant tables every mission module shares. */
 void append_catalog_constants(const Source& source, std::string& output);
 
+/**
+ * Appends one scene's inputs and outputs, named where the trigger name is known.
+ * An input is waited on and fired by no graph block; an output is fired and waited on by none.
+ * @param scene The scene's Lua key.
+ * @param gates The scene's gate rows, in graph order.
+ * @param inputs Receives the scene's block of `mission.SceneInput`, when it has inputs.
+ * @param outputs Receives the scene's block of `mission.SceneOutput`, when it has outputs.
+ */
+void append_scene_ports(std::string_view scene,
+                        std::span<const format::AuthoredSceneEventKey> gates,
+                        std::string& inputs,
+                        std::string& outputs);
+
 /** Emits one concrete mission module with no pack or resolver access. */
 [[nodiscard]] bool render_mission(const Source& source,
                                   const RenderIndex& index,

+ 19 - 34
Sunrise/src/client/content/activity/activity_sdk_lua_publish.cpp

@@ -4,7 +4,6 @@
 #include <array>
 #include <atomic>
 #include <cctype>
-#include <cstdio>
 #include <limits>
 #include <span>
 #include <string>
@@ -25,12 +24,14 @@ constexpr std::wstring_view kLuaSuffix = L"\\lua";
 constexpr std::wstring_view kActivitiesSuffix = L"\\activities";
 constexpr std::wstring_view kMissionsSuffix = L"\\missions";
 constexpr std::wstring_view kSunriseSuffix = L"\\sunrise";
-constexpr std::wstring_view kTemporarySuffix = L".%08lX.%08lX.%08lX.tmp";
+// The mission API schema sits beside activity_sdk.lua.
+constexpr std::wstring_view kMissionApiName = L"\\mission_api.json";
+// Files every estate publishes beside its modules: two indexes, the manifest, the SDK module,
+// the behavior module and the mission API schema.
+constexpr std::size_t kFixedFileCount = 6;
 // Distinct temporary names tried before publication fails.
 constexpr std::size_t kTemporaryAttempts = 16;
 
-volatile LONG g_temporarySequence{};
-
 /** One small handle owner keeps failed publication paths closed. */
 class FileHandle final {
 public:
@@ -77,27 +78,11 @@ private:
     return core::path::assign(output, parent) && core::path::append(output, suffix);
 }
 
-/** Forms a process-, thread-, and sequence-scoped sibling name for publication. */
-[[nodiscard]] bool temporary_path(const wchar_t* finalPath, core::path::Buffer& output) noexcept {
-    std::array<wchar_t, 64> suffix{};
-    const DWORD sequence = static_cast<DWORD>(InterlockedIncrement(&g_temporarySequence));
-    const int length = std::swprintf(suffix.data(),
-                                     suffix.size(),
-                                     kTemporarySuffix.data(),
-                                     GetCurrentProcessId(),
-                                     GetCurrentThreadId(),
-                                     sequence);
-    return length > 0 && static_cast<std::size_t>(length) < suffix.size()
-           && core::path::assign(output, finalPath)
-           && core::path::append(output, std::wstring_view(suffix.data(), length));
-}
-
 /** Writes the complete source in DWORD-sized chunks and rejects short writes. */
 [[nodiscard]] bool write_all(HANDLE file, std::string_view source) noexcept {
     while (!source.empty()) {
-        const std::size_t count =
-            (std::min)(source.size(),
-                       static_cast<std::size_t>((std::numeric_limits<DWORD>::max)()));
+        const std::size_t count = (std::min)(
+            source.size(), static_cast<std::size_t>((std::numeric_limits<DWORD>::max)()));
         DWORD transferred = 0;
         if (WriteFile(file, source.data(), static_cast<DWORD>(count), &transferred, nullptr)
                 == FALSE
@@ -114,7 +99,7 @@ private:
     core::path::remove_stale_siblings(finalPath);
     for (std::size_t attempt = 0; attempt < kTemporaryAttempts; ++attempt) {
         core::path::Buffer temporary{};
-        if (!temporary_path(finalPath, temporary)) {
+        if (!core::path::temporary_sibling(finalPath, temporary)) {
             return false;
         }
         const HANDLE raw = CreateFileW(temporary.chars.data(),
@@ -326,7 +311,8 @@ Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result&
     if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0' || bundle.activityIndex.empty()
         || bundle.missionIndex.empty() || bundle.activitySdkModule.empty()
         || bundle.behaviorModule.empty() || bundle.manifestJson.empty()
-        || bundle.activityModules.empty() || bundle.missionModules.empty()) {
+        || bundle.missionApiJson.empty() || bundle.activityModules.empty()
+        || bundle.missionModules.empty()) {
         return Status::invalidInput;
     }
     try {
@@ -348,6 +334,7 @@ Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result&
         if (!publish_named(lua.chars.data(), L"\\manifest.json", bundle.manifestJson, bytes)
             || !publish_named(
                 sunrise.chars.data(), L"\\activity_sdk.lua", bundle.activitySdkModule, bytes)
+            || !publish_named(sunrise.chars.data(), kMissionApiName, bundle.missionApiJson, bytes)
             || !publish_named(
                 sunrise.chars.data(), L"\\behaviors.lua", bundle.behaviorModule, bytes)) {
             return Status::writeFailure;
@@ -363,11 +350,11 @@ Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result&
         }
         const std::size_t sourceFiles =
             bundle.activityModules.size() + bundle.missionModules.size();
-        if (sourceFiles > format::kAbsentIndex - 5U) {
+        if (sourceFiles > format::kAbsentIndex - kFixedFileCount) {
             return Status::writeFailure;
         }
         output.activityCount = static_cast<std::uint32_t>(bundle.activityModules.size());
-        output.fileCount = static_cast<std::uint32_t>(sourceFiles + 5U);
+        output.fileCount = static_cast<std::uint32_t>(sourceFiles + kFixedFileCount);
         output.byteCount = bytes;
         return Status::ready;
     } catch (...) {
@@ -376,13 +363,6 @@ Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result&
     }
 }
 
-Status publish(const wchar_t* sdkDirectory, const Source& source, Result& output) noexcept {
-    output = {};
-    Bundle bundle{};
-    const Status built = build(source, bundle);
-    return built == Status::ready ? publish_bundle(sdkDirectory, bundle, output) : built;
-}
-
 /** @return True when the published Lua modules already match this catalog. */
 bool is_current(const wchar_t* sdkDirectory, const state::activity_sdk::Catalog& catalog) noexcept {
     if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0') {
@@ -433,9 +413,14 @@ bool is_current(const wchar_t* sdkDirectory, const state::activity_sdk::Catalog&
         }
         core::path::Buffer lua{};
         core::path::Buffer manifest{};
+        core::path::Buffer sunrise{};
+        core::path::Buffer missionApi{};
         return child_path(sdkDirectory, kLuaSuffix, lua)
                && child_path(lua.chars.data(), L"\\manifest.json", manifest)
-               && file_equals(manifest.chars.data(), expected.manifestJson);
+               && file_equals(manifest.chars.data(), expected.manifestJson)
+               && child_path(lua.chars.data(), kSunriseSuffix, sunrise)
+               && child_path(sunrise.chars.data(), kMissionApiName, missionApi)
+               && file_equals(missionApi.chars.data(), expected.missionApiJson);
     } catch (...) {
         return false;
     }

+ 69 - 0
Sunrise/src/client/content/activity/activity_sdk_lua_scene_events.cpp

@@ -0,0 +1,69 @@
+#include <array>
+#include <cstdint>
+#include <cstdio>
+#include <span>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+
+#include "../../../state/activity_sdk/scene_triggers.h"
+#include "activity_sdk_lua_missions_internal.h"
+
+namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
+namespace {
+
+/**
+ * Appends the gates one predicate selects as constants, named where the trigger name is known.
+ * @return The number of gates appended.
+ */
+std::size_t append_gates(std::span<const format::AuthoredSceneEventKey> gates,
+                         bool (*selected)(const format::AuthoredSceneEventKey&) noexcept,
+                         std::string& output) {
+    std::unordered_set<std::string> names;
+    std::size_t appended = 0;
+    for (const format::AuthoredSceneEventKey& gate : gates) {
+        if (!selected(gate)) {
+            continue;
+        }
+        ++appended;
+        const std::string_view known = state::activity_sdk::scene_trigger_name(gate.key);
+        std::string name = known.empty() ? std::string{} : identifier(known, true);
+        // An unknown name, or one that folds onto an earlier name, takes the key itself.
+        if (name.empty() || !names.insert(name).second) {
+            std::array<char, 16> symbol{};
+            const int length = std::snprintf(symbol.data(), symbol.size(), "KEY_%08X", gate.key);
+            name.assign(symbol.data(), static_cast<std::size_t>(length));
+        }
+        output.append("        ");
+        output.append(name);
+        output.append(" = ");
+        append_hex(output, gate.key);
+        output.append(",\n");
+    }
+    return appended;
+}
+
+/** Appends one scene's block to a table when the predicate selects any of its gates. */
+void append_scene_table(std::string_view scene,
+                        std::span<const format::AuthoredSceneEventKey> gates,
+                        bool (*selected)(const format::AuthoredSceneEventKey&) noexcept,
+                        std::string& output) {
+    std::string block;
+    if (append_gates(gates, selected, block) == 0) {
+        return;
+    }
+    output.append("    ").append(scene).append(" = {\n").append(block).append("    },\n");
+}
+
+} // namespace
+
+/** Appends one scene's inputs and outputs to their constant tables. */
+void append_scene_ports(std::string_view scene,
+                        std::span<const format::AuthoredSceneEventKey> gates,
+                        std::string& inputs,
+                        std::string& outputs) {
+    append_scene_table(scene, gates, &state::activity_sdk::scene_trigger_is_input, inputs);
+    append_scene_table(scene, gates, &state::activity_sdk::scene_trigger_is_output, outputs);
+}
+
+} // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal

+ 75 - 2
Sunrise/src/client/content/activity/activity_sdk_lua_value.cpp

@@ -133,7 +133,7 @@ render_value(const Value& value, Dialect dialect, std::uint32_t depth, std::stri
     return append_indent(output, depth, width) && (output.push_back('}'), true);
 }
 
-/** Renders only the six Value alternatives supported by both output dialects. */
+/** Renders only the seven Value alternatives supported by both output dialects. */
 [[nodiscard]] bool
 render_value(const Value& value, Dialect dialect, std::uint32_t depth, std::string& output) {
     return std::visit(
@@ -145,7 +145,8 @@ render_value(const Value& value, Dialect dialect, std::uint32_t depth, std::stri
             } else if constexpr (std::is_same_v<Item, bool>) { // Both dialects share the literals.
                 output.append(item ? "true" : "false");
                 return true;
-            } else if constexpr (std::is_same_v<Item, std::uint64_t>) { // No float form is emitted.
+            } else if constexpr (std::is_same_v<Item, std::uint64_t>
+                                 || std::is_same_v<Item, std::int64_t>) { // No float form.
                 output.append(std::to_string(item));
                 return true;
             } else if constexpr (std::is_same_v<Item, std::string>) { // Non-ASCII is refused.
@@ -159,6 +160,22 @@ render_value(const Value& value, Dialect dialect, std::uint32_t depth, std::stri
         value.data);
 }
 
+/** Appends the seven declared name lanes for one field prefix. */
+void append_name_fields(Value::Object& output, std::string_view prefix) {
+    const auto add = [&output, prefix](std::string_view suffix, std::string_view type) {
+        std::string name(prefix);
+        name.append(suffix);
+        output.push_back({std::move(name), string(type)});
+    };
+    add("_name", "string_or_nil");
+    add("_name_provenance", "unresolved_package_inline_package_path_or_identifier_candidate");
+    add("_name_provenance_code", "u32");
+    add("_name_candidate_count", "u32");
+    add("_name_source_tag", "u32");
+    add("_name_source_class", "u32");
+    add("_name_strongest_tier_overflow", "boolean");
+}
+
 } // namespace
 
 Value null_value() {
@@ -173,6 +190,10 @@ Value number(std::uint64_t value) {
     return Value{value};
 }
 
+Value signed_number(std::int64_t value) {
+    return Value{value};
+}
+
 Value string(std::string_view value) {
     return Value{std::string(value)};
 }
@@ -197,6 +218,58 @@ Value string_array(std::initializer_list<std::string_view> values) {
     return array(std::move(output));
 }
 
+void sort_keys(Value::Object& value) {
+    std::sort(value.begin(), value.end(), [](const auto& first, const auto& second) {
+        return first.first < second.first;
+    });
+}
+
+Value field_map(std::initializer_list<Field> source) {
+    Value::Object output;
+    output.reserve(source.size());
+    for (const auto& [name, type] : source) {
+        output.push_back({std::string(name), string(type)});
+    }
+    sort_keys(output);
+    return object(std::move(output));
+}
+
+/**
+ * Builds one view's declared fields, expanding each prefix into its seven name lanes.
+ * @param source Plain fields.
+ * @param prefixes Field prefixes that each carry the name lanes.
+ * @param rowScope Row scope the view declares.
+ * @return The view object.
+ */
+Value named_view(std::initializer_list<Field> source,
+                 std::initializer_list<std::string_view> prefixes,
+                 std::string_view rowScope) {
+    Value::Object fields;
+    fields.reserve(source.size() + prefixes.size() * 7U);
+    for (const auto& [name, type] : source) {
+        fields.push_back({std::string(name), string(type)});
+    }
+    for (const std::string_view prefix : prefixes) {
+        append_name_fields(fields, prefix);
+    }
+    sort_keys(fields);
+    return object({
+        {"fields", object(std::move(fields))},
+        {"immutable", boolean(true)},
+        {"row_scope", string(rowScope)},
+        {"stale_read", string("throws")},
+    });
+}
+
+Value view(std::initializer_list<Field> fields, std::string_view rowScope) {
+    return object({
+        {"fields", field_map(fields)},
+        {"immutable", boolean(true)},
+        {"row_scope", string(rowScope)},
+        {"stale_read", string("throws")},
+    });
+}
+
 bool render_json(const Value& value, std::uint32_t initialDepth, std::string& output) noexcept {
     output.clear();
     try {

+ 0 - 18
Sunrise/src/client/content/activity/activity_sdk_lua_world_contract.cpp

@@ -9,24 +9,6 @@
 namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
 namespace {
 
-using Field = std::pair<std::string_view, std::string_view>;
-
-void sort_keys(Value::Object& value) {
-    std::sort(value.begin(), value.end(), [](const auto& first, const auto& second) {
-        return first.first < second.first;
-    });
-}
-
-[[nodiscard]] Value field_map(std::initializer_list<Field> source) {
-    Value::Object output;
-    output.reserve(source.size());
-    for (const auto& [name, type] : source) {
-        output.push_back({std::string(name), string(type)});
-    }
-    sort_keys(output);
-    return object(std::move(output));
-}
-
 /** Builds one projection row: its surface, view, artifact, sections and coverage. */
 [[nodiscard]] Value projection(std::string_view surface,
                                std::string_view viewName,

+ 0 - 67
Sunrise/src/client/content/activity/activity_sdk_lua_world_core_views.cpp

@@ -7,73 +7,6 @@
 #include "activity_sdk_lua_artifacts_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
-namespace {
-
-using Field = std::pair<std::string_view, std::string_view>;
-
-void sort_keys(Value::Object& value) {
-    std::sort(value.begin(), value.end(), [](const auto& first, const auto& second) {
-        return first.first < second.first;
-    });
-}
-
-[[nodiscard]] Value field_map(std::initializer_list<Field> source) {
-    Value::Object output;
-    output.reserve(source.size());
-    for (const auto& [name, type] : source) {
-        output.push_back({std::string(name), string(type)});
-    }
-    sort_keys(output);
-    return object(std::move(output));
-}
-
-/** Appends the seven declared name lanes for one field prefix. */
-void append_name_fields(Value::Object& output, std::string_view prefix) {
-    const auto add = [&output, prefix](std::string_view suffix, std::string_view type) {
-        std::string name(prefix);
-        name.append(suffix);
-        output.push_back({std::move(name), string(type)});
-    };
-    add("_name", "string_or_nil");
-    add("_name_provenance", "unresolved_package_inline_package_path_or_identifier_candidate");
-    add("_name_provenance_code", "u32");
-    add("_name_candidate_count", "u32");
-    add("_name_source_tag", "u32");
-    add("_name_source_class", "u32");
-    add("_name_strongest_tier_overflow", "boolean");
-}
-
-/** Builds one view's declared fields, expanding each prefix into its name lanes. */
-[[nodiscard]] Value named_view(std::initializer_list<Field> source,
-                               std::initializer_list<std::string_view> prefixes,
-                               std::string_view rowScope) {
-    Value::Object fields;
-    fields.reserve(source.size() + prefixes.size() * 7U);
-    for (const auto& [name, type] : source) {
-        fields.push_back({std::string(name), string(type)});
-    }
-    for (const std::string_view prefix : prefixes) {
-        append_name_fields(fields, prefix);
-    }
-    sort_keys(fields);
-    return object({
-        {"fields", object(std::move(fields))},
-        {"immutable", boolean(true)},
-        {"row_scope", string(rowScope)},
-        {"stale_read", string("throws")},
-    });
-}
-
-[[nodiscard]] Value view(std::initializer_list<Field> fields, std::string_view rowScope) {
-    return object({
-        {"fields", field_map(fields)},
-        {"immutable", boolean(true)},
-        {"row_scope", string(rowScope)},
-        {"stale_read", string("throws")},
-    });
-}
-
-} // namespace
 
 /** Appends the core world view declarations to the contract object. */
 void append_world_core_views(Value::Object& output) {

+ 0 - 67
Sunrise/src/client/content/activity/activity_sdk_lua_world_spatial_views.cpp

@@ -7,73 +7,6 @@
 #include "activity_sdk_lua_artifacts_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::lua_artifacts::internal {
-namespace {
-
-using Field = std::pair<std::string_view, std::string_view>;
-
-void sort_keys(Value::Object& value) {
-    std::sort(value.begin(), value.end(), [](const auto& first, const auto& second) {
-        return first.first < second.first;
-    });
-}
-
-[[nodiscard]] Value field_map(std::initializer_list<Field> source) {
-    Value::Object output;
-    output.reserve(source.size());
-    for (const auto& [name, type] : source) {
-        output.push_back({std::string(name), string(type)});
-    }
-    sort_keys(output);
-    return object(std::move(output));
-}
-
-/** Appends the seven declared name lanes for one field prefix. */
-void append_name_fields(Value::Object& output, std::string_view prefix) {
-    const auto add = [&output, prefix](std::string_view suffix, std::string_view type) {
-        std::string name(prefix);
-        name.append(suffix);
-        output.push_back({std::move(name), string(type)});
-    };
-    add("_name", "string_or_nil");
-    add("_name_provenance", "unresolved_package_inline_package_path_or_identifier_candidate");
-    add("_name_provenance_code", "u32");
-    add("_name_candidate_count", "u32");
-    add("_name_source_tag", "u32");
-    add("_name_source_class", "u32");
-    add("_name_strongest_tier_overflow", "boolean");
-}
-
-/** Builds one view's declared fields, expanding each prefix into its name lanes. */
-[[nodiscard]] Value named_view(std::initializer_list<Field> source,
-                               std::initializer_list<std::string_view> prefixes,
-                               std::string_view rowScope) {
-    Value::Object fields;
-    fields.reserve(source.size() + prefixes.size() * 7U);
-    for (const auto& [name, type] : source) {
-        fields.push_back({std::string(name), string(type)});
-    }
-    for (const std::string_view prefix : prefixes) {
-        append_name_fields(fields, prefix);
-    }
-    sort_keys(fields);
-    return object({
-        {"fields", object(std::move(fields))},
-        {"immutable", boolean(true)},
-        {"row_scope", string(rowScope)},
-        {"stale_read", string("throws")},
-    });
-}
-
-[[nodiscard]] Value view(std::initializer_list<Field> fields, std::string_view rowScope) {
-    return object({
-        {"fields", field_map(fields)},
-        {"immutable", boolean(true)},
-        {"row_scope", string(rowScope)},
-        {"stale_read", string("throws")},
-    });
-}
-
-} // namespace
 
 /** Appends the declared shape of every spatial world view. */
 void append_world_spatial_views(Value::Object& output) {

+ 0 - 291
Sunrise/src/client/content/activity/activity_sdk_mission_abi_data.inc

@@ -1,291 +0,0 @@
-// Generated by interact_mng/tools/export_sunrise_mission_abi.py.
-// Do not edit this file by hand.
-
-inline constexpr SourceAbi kSourceAbi{
-    "sunrise-mission-api-v1",
-    std::array<std::byte, 32>{std::byte{0xCA}, std::byte{0x95}, std::byte{0x05}, std::byte{0x35}, std::byte{0xD2}, std::byte{0xEC}, std::byte{0x59}, std::byte{0x2D}, std::byte{0x51}, std::byte{0x61}, std::byte{0xCA}, std::byte{0xBF}, std::byte{0x6B}, std::byte{0x13}, std::byte{0xDD}, std::byte{0xC4}, std::byte{0x26}, std::byte{0xAF}, std::byte{0xBE}, std::byte{0xD8}, std::byte{0x10}, std::byte{0x4B}, std::byte{0xC2}, std::byte{0xDC}, std::byte{0x58}, std::byte{0x77}, std::byte{0xDA}, std::byte{0xC2}, std::byte{0x66}, std::byte{0xBB}, std::byte{0x17}, std::byte{0x94}},
-    1U, 0U, 1U, 0U, 31U, 1U, 0U};
-
-inline constexpr std::array<SourceSurface, 31> kSourceSurfaces{{
-    SourceSurface{"mission.event.activity-incident-received.v1", "incidentReceived", "activity.msg19.incident-event.v1", "mission.value.activity-incident-event.v1", "Publishes the bounded outer fields of one owned message-19 incident. Numeric target identity has no inferred mission meaning.", 1U, 19U, 4294967295U, 3U, 1U, 1665U, 0U, 9U, 0U, 9U, 1U},
-    SourceSurface{"mission.event.client-state-changed.v1", "clientStateChanged", "activity.msg22.committed-state-event.v1", "mission.value.client-state-event.v1", "Publishes only changed numeric fields copied from the committed message-22 State after-image after connection publication.", 1U, 22U, 4294967295U, 3U, 1U, 6787U, 9U, 13U, 9U, 13U, 1U},
-    SourceSurface{"mission.event.effect-result.v1", "effectResult", "mission.effect.result-event.v1", "mission.value.effect-result.v1", "Reports the delivery outcome of one script-requested effect. Transport staging is the strongest outcome available; no outcome proves the stock client applied the effect.", 1U, 4294967295U, 4294967295U, 3U, 1U, 33411U, 22U, 4U, 22U, 4U, 1U},
-    SourceSurface{"mission.event.entity-died.v1", "entityDied", "activity.type1.entity-died.v1", "mission.value.entity-died.v1", "Reports the squad alive count falling. The client has no death event; it finds death by polling handle validity. So there is no member identity and no cause, only the squad.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 26U, 5U, 26U, 5U, 1U},
-    SourceSurface{"mission.event.entity-slots-requested.v1", "entitySlotsRequested", "activity.msg20.entity-slots-requested.v1", "mission.value.entity-slots-requested.v1", "Reports the requested simulation entity capacity after its native transaction commits. It does not report entity readiness.", 1U, 20U, 4294967295U, 3U, 1U, 33411U, 31U, 1U, 31U, 1U, 1U},
-    SourceSurface{"mission.event.entity-spawned.v1", "entitySpawned", "activity.type1.entity-spawned.v1", "mission.value.entity-spawned.v1", "Reports one per-slot member count rising. The client sends no spawn message, so the rising count is the only spawn signal and the event names no member.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 32U, 6U, 32U, 6U, 1U},
-    SourceSurface{"mission.event.objective-progress.v1", "objectiveProgress", "activity.type3.objective-progress.v1", "mission.value.objective-progress.v1", "Reports one objective task counter rising. The client owns the counter and raises it on an actor teardown, so no server field sets it and the event names no actor.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 38U, 7U, 38U, 7U, 1U},
-    SourceSurface{"mission.event.phase-entered.v1", "phaseEntered", "mission.phase.commit-event.v1", "mission.value.phase-entered.v1", "Reports one committed mission phase change. The phase is server state, so the event follows the commit and never precedes it.", 1U, 4294967295U, 4294967295U, 3U, 1U, 33411U, 45U, 3U, 45U, 3U, 1U},
-    SourceSurface{"mission.event.scene-finished.v1", "sceneFinished", "activity.type43.scene-finished.v1", "mission.value.scene-finished.v1", "Reports one authored scene latching complete. The field is a latch, so only the false to true edge is a finish and a repeat of true is not.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 48U, 4U, 48U, 4U, 1U},
-    SourceSurface{"mission.event.sensor-sense-updated.v1", "sensorSenseUpdated", "activity.msg6.sensor-sense-event.v1", "mission.value.sensor-sense-event.v1", "Publishes a complete package-selected message-6 decode through the bounded Sense observation view.", 1U, 6U, 4294967295U, 3U, 1U, 697U, 52U, 10U, 52U, 10U, 1U},
-    SourceSurface{"mission.event.session-joined.v1", "sessionJoined", "mission.session.joined-event.v1", "mission.value.session-joined.v1", "Reports one committed peer session appearing at this instance's destination. The first roster read after a program loads records every current peer and raises nothing, so a script that needs the current set must read state rather than wait for an edge.", 1U, 4294967295U, 4294967295U, 3U, 1U, 33411U, 62U, 4U, 62U, 4U, 1U},
-    SourceSurface{"mission.event.session-left.v1", "sessionLeft", "mission.session.left-event.v1", "mission.value.session-left.v1", "Reports one watched peer session leaving this instance's destination. A record replacement changes the generation, which counts as a leave followed by a join.", 1U, 4294967295U, 4294967295U, 3U, 1U, 33411U, 66U, 3U, 66U, 3U, 1U},
-    SourceSurface{"mission.event.squad-state.v1", "squadState", "activity.type1.squad-state.v1", "mission.value.squad-state.v1", "Reports one squad's published counters when they change: the alive count, one client flag, and the per-slot counts. The alive count is six bits, so 0 through 63. The flag is set on the client's death or removal path and its meaning is unproved.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 69U, 6U, 69U, 6U, 1U},
-    SourceSurface{"mission.event.timer-elapsed.v1", "timerElapsed", "mission.timer.scheduler-event.v1", "mission.value.timer-elapsed.v1", "Publishes one due authoritative timer without entering or advancing the Activity Host input feed.", 1U, 4294967295U, 4294967295U, 3U, 1U, 49795U, 75U, 3U, 75U, 3U, 1U},
-    SourceSurface{"mission.event.trigger-entered.v1", "triggerEntered", "activity.type30.occupancy-entered.v1", "mission.value.trigger-entered.v1", "Reports that a watched trigger volume became occupied. The client reports a level, so the edge is derived and a first observation never raises one.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 78U, 6U, 78U, 6U, 1U},
-    SourceSurface{"mission.event.trigger-exited.v1", "triggerExited", "activity.type30.occupancy-exited.v1", "mission.value.trigger-exited.v1", "Reports that a watched trigger volume became empty. The client reports a level, so the edge is derived and a first observation never raises one.", 1U, 6U, 4294967295U, 3U, 1U, 33467U, 84U, 6U, 84U, 6U, 1U},
-    SourceSurface{"mission.intent.authored-scene-activate.v1", "scene.activate", "activity.type43.authored-scene-activate.v1", "mission.value.authored-scene-activate.v1", "Activates one exact generated type-43 authored-scene row through the existing guarded message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 90U, 1U, 90U, 1U, 1U},
-    SourceSurface{"mission.intent.device-set-channel.v1", "slot.set_channel", "activity.type23.device-set-channel.v1", "mission.value.device-set-channel.v1", "Sets one verified type-23 position, power, or lock channel through the existing guarded message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 91U, 4U, 91U, 4U, 1U},
-    SourceSurface{"mission.intent.device-transition.v1", "device.transition", "activity.type23.device-transition.v1", "mission.value.device-transition.v1", "Applies one named device transition to the position, power, or lock lane through the existing guarded type-23 message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 95U, 2U, 95U, 2U, 1U},
-    SourceSurface{"mission.intent.lifetime-set.v1", "lifetime.set", "activity.type17.lifetime-set.v1", "mission.value.lifetime-set.v1", "Sets the activity lifetime state that the type-17 block reports through the existing guarded message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 97U, 1U, 97U, 1U, 1U},
-    SourceSurface{"mission.intent.occupancy-condition-set.v1", "slot.set_occupancy_condition", "activity.type30.occupancy-condition-set.v1", "mission.value.occupancy-condition-set.v1", "Sets the player set and threshold one type-30 occupancy condition reports on, through the guarded slot Auth message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 98U, 3U, 98U, 3U, 1U},
-    SourceSurface{"mission.intent.slot-apply-auth.v1", "slot.apply_auth", "activity.slot.sdk-auth-apply.v1", "mission.value.slot-auth-apply.v1", "Compiles one exact dense Auth draft against a generation-bound SDK Slot and delivers the value-owned body through the guarded message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 101U, 2U, 101U, 2U, 1U},
-    SourceSurface{"mission.intent.squad-place.v1", "squad.place", "activity.type1.squad-place.v1", "mission.value.squad-place.v1", "Places one exact runnable generated squad through the existing guarded type-1 message-5 adapter.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 103U, 3U, 103U, 3U, 1U},
-    SourceSurface{"mission.intent.trigger-fire.v1", "slot.fire_trigger", "activity.type31.trigger-fire.v1", "", "Fires one verified type-31 configured trigger through the existing guarded message-5 adapter. The pulse carries no caller value: enabled is fixed true, the generation is minted by the Host from that target's own guard, and the auxiliary value stays zero.", 2U, 4294967295U, 5U, 3U, 2U, 961U, 106U, 1U, 106U, 1U, 1U},
-    SourceSurface{"mission.mutation.script-state-clear.v1", "context.clear_variable", "mission.state.clear-variable.v1", "mission.value.state-clear-variable.v1", "Removes one bounded variable only when its enclosing callback commits atomically.", 4U, 4294967295U, 4294967295U, 3U, 2U, 41601U, 107U, 2U, 107U, 2U, 1U},
-    SourceSurface{"mission.mutation.script-state-set.v1", "context.set_variable", "mission.state.set-variable.v1", "mission.value.state-set-variable.v1", "Sets one boolean, signed integer, finite real, or bounded string only when its enclosing callback commits atomically.", 4U, 4294967295U, 4294967295U, 3U, 2U, 41601U, 109U, 2U, 109U, 2U, 1U},
-    SourceSurface{"mission.query.script-state-has-variable.v1", "state.has_variable", "mission.state.has-variable.v1", "mission.value.state-has-variable.v1", "Tests the current callback candidate before authoritative publication.", 3U, 4294967295U, 4294967295U, 3U, 1U, 33409U, 111U, 2U, 111U, 2U, 1U},
-    SourceSurface{"mission.query.script-state-timer.v1", "state.timer", "mission.state.timer-query.v1", "mission.value.state-timer-query.v1", "Reads one candidate timer deadline as a decimal u64, or nil when absent.", 3U, 4294967295U, 4294967295U, 3U, 1U, 33409U, 113U, 2U, 113U, 2U, 1U},
-    SourceSurface{"mission.query.script-state-variable.v1", "state.variable", "mission.state.variable-query.v1", "mission.value.state-variable-query.v1", "Reads one copied candidate scalar, or nil when the variable is absent.", 3U, 4294967295U, 4294967295U, 3U, 1U, 33409U, 115U, 2U, 115U, 2U, 1U},
-    SourceSurface{"mission.service.timer-cancel.v1", "context.cancel_timer", "mission.timer.cancel.v1", "mission.value.timer-cancel.v1", "Cancels one candidate timer only when the enclosing callback commits.", 5U, 4294967295U, 4294967295U, 3U, 2U, 41601U, 117U, 2U, 117U, 2U, 1U},
-    SourceSurface{"mission.service.timer-start.v1", "context.start_timer", "mission.timer.start.v1", "mission.value.timer-start.v1", "Arms or replaces one timer at an absolute authoritative service deadline when the callback commits.", 5U, 4294967295U, 4294967295U, 3U, 2U, 57985U, 119U, 3U, 119U, 3U, 1U},
-}};
-
-inline constexpr std::array<SourceMember, 122> kSourceMembers{{
-    SourceMember{"sequence", "u64.decimal", "Monotonic Activity Host event sequence encoded as a decimal string.", 0U, 0U, 4U, 3U, 1U},
-    SourceMember{"source_generation", "u64.decimal", "Exact ActivityClient generation encoded as a decimal string.", 0U, 1U, 4U, 3U, 1U},
-    SourceMember{"client_message_sequence", "u64.decimal", "Owned client-message history sequence encoded as a decimal string.", 0U, 2U, 4U, 3U, 1U},
-    SourceMember{"mission_sequence", "u64.decimal", "Per-binding ordered mission-input sequence encoded as a decimal string.", 0U, 3U, 4U, 3U, 1U},
-    SourceMember{"payload_bytes", "u32", "Size of the validated message-19 payload.", 0U, 4U, 3U, 3U, 1U},
-    SourceMember{"incident_target", "activity.incident-target-index", "Range-checked primary target index. Its mission meaning remains activity-specific.", 0U, 5U, 3U, 3U, 1U},
-    SourceMember{"incident_extra_targets", "u32", "Range-checked count of extra target indices.", 0U, 6U, 3U, 3U, 1U},
-    SourceMember{"incident_selector_bytes", "u32", "Validated selector byte count. Selector contents are not script-visible.", 0U, 7U, 3U, 3U, 1U},
-    SourceMember{"incident_payload_bytes", "u32", "Validated incident payload byte count. Payload contents are not script-visible.", 0U, 8U, 3U, 3U, 1U},
-    SourceMember{"sequence", "u64.decimal", "Monotonic Activity Host event sequence encoded as a decimal string.", 1U, 0U, 4U, 3U, 1U},
-    SourceMember{"source_generation", "u64.decimal", "Exact ActivityClient generation encoded as a decimal string.", 1U, 1U, 4U, 3U, 1U},
-    SourceMember{"client_message_sequence", "u64.decimal", "Owned client-message history sequence encoded as a decimal string.", 1U, 2U, 4U, 3U, 1U},
-    SourceMember{"mission_sequence", "u64.decimal", "Per-binding ordered mission-input sequence encoded as a decimal string.", 1U, 3U, 4U, 3U, 1U},
-    SourceMember{"payload_bytes", "u32", "Size of the validated message-22 payload.", 1U, 4U, 3U, 3U, 1U},
-    SourceMember{"activity_state_revision", "u64.decimal", "Committed activity State revision encoded as a decimal string.", 1U, 5U, 4U, 3U, 1U},
-    SourceMember{"membership_revision", "u32", "Committed membership revision from the message-22 after-image.", 1U, 6U, 3U, 3U, 1U},
-    SourceMember{"region_index", "activity.region-index", "Changed nonnegative region index copied from committed State.", 1U, 7U, 2U, 1U, 1U},
-    SourceMember{"region_slice_set_hash", "activity.slice-set-name-hash", "Slice-set name hash stored with the changed committed region.", 1U, 8U, 3U, 1U, 1U},
-    SourceMember{"spawn_state", "activity.spawn-state", "Changed typed spawn-state scalar copied from committed State.", 1U, 9U, 2U, 1U, 1U},
-    SourceMember{"teleport_state", "activity.teleport-state", "Changed typed teleport-state scalar copied from committed State.", 1U, 10U, 2U, 1U, 1U},
-    SourceMember{"teleport_slice_set_index", "activity.slice-set-index", "Changed teleport slice-set index copied from committed State.", 1U, 11U, 2U, 1U, 1U},
-    SourceMember{"teleport_slice_set_hash", "activity.slice-set-name-hash", "Changed teleport slice-set hash copied from committed State.", 1U, 12U, 3U, 1U, 1U},
-    SourceMember{"request_key", "u64.decimal", "Stable nonzero key the effect call returned to the script.", 2U, 0U, 4U, 3U, 1U},
-    SourceMember{"effect", "mission.effect-name", "Catalog-owned Lua name of the requested effect.", 2U, 1U, 6U, 3U, 1U},
-    SourceMember{"outcome", "mission.effect-outcome", "One of transport_staged, refused, expired or canceled.", 2U, 2U, 6U, 3U, 1U},
-    SourceMember{"outcome_code", "u32", "Numeric form of the same outcome.", 2U, 3U, 3U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the squad slot.", 3U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the squad slot.", 3U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the squad slot.", 3U, 2U, 3U, 3U, 1U},
-    SourceMember{"alive_count", "u32", "Alive members the client published after the fall.", 3U, 3U, 2U, 3U, 1U},
-    SourceMember{"previous_alive_count", "u32", "Alive count this observation replaced.", 3U, 4U, 2U, 3U, 1U},
-    SourceMember{"requested_count", "u32", "Capacity requested by the client and accepted by the native entity-slot transaction.", 4U, 0U, 3U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the squad slot.", 5U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the squad slot.", 5U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the squad slot.", 5U, 2U, 3U, 3U, 1U},
-    SourceMember{"member_slot", "u32", "Index of the per-slot count that rose.", 5U, 3U, 3U, 3U, 1U},
-    SourceMember{"count", "u32", "New value of that per-slot count.", 5U, 4U, 2U, 3U, 1U},
-    SourceMember{"previous_count", "u32", "Value that per-slot count held before it rose.", 5U, 5U, 2U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the objective slot.", 6U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the objective slot.", 6U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the objective slot.", 6U, 2U, 3U, 3U, 1U},
-    SourceMember{"objective", "activity.objective-ordinal", "Objective block the task belongs to. The block list is 24 long.", 6U, 3U, 3U, 3U, 1U},
-    SourceMember{"task", "activity.objective-task-ordinal", "Task inside that objective. The task list is 24 long.", 6U, 4U, 3U, 3U, 1U},
-    SourceMember{"task_count", "u32", "New counter value. The client clamps it to 80.", 6U, 5U, 2U, 3U, 1U},
-    SourceMember{"previous_task_count", "u32", "Counter value that task held before it rose.", 6U, 6U, 2U, 3U, 1U},
-    SourceMember{"phase", "u32", "Mission phase the commit entered.", 7U, 0U, 3U, 3U, 1U},
-    SourceMember{"previous_phase", "u32", "Mission phase the commit replaced.", 7U, 1U, 3U, 3U, 1U},
-    SourceMember{"state_revision", "u64.decimal", "Mission State revision that carries the new phase.", 7U, 2U, 4U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the scene slot.", 8U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the scene slot.", 8U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the scene slot.", 8U, 2U, 3U, 3U, 1U},
-    SourceMember{"activation_token", "activity.scene-activation-token", "Activation token the scene echoed, so a script can tell which activation completed.", 8U, 3U, 2U, 3U, 1U},
-    SourceMember{"sequence", "u64.decimal", "Monotonic Activity Host event sequence encoded as a decimal string.", 9U, 0U, 4U, 3U, 1U},
-    SourceMember{"source_generation", "u64.decimal", "Exact ActivityClient generation encoded as a decimal string.", 9U, 1U, 4U, 3U, 1U},
-    SourceMember{"client_message_sequence", "u64.decimal", "Owned client-message history sequence encoded as a decimal string.", 9U, 2U, 4U, 3U, 1U},
-    SourceMember{"mission_sequence", "u64.decimal", "Per-binding ordered mission-input sequence encoded as a decimal string.", 9U, 3U, 4U, 3U, 1U},
-    SourceMember{"payload_bytes", "u32", "Size of the validated message-6 payload.", 9U, 4U, 3U, 3U, 1U},
-    SourceMember{"peer_heard_mask", "activity.peer-heard-mask", "Peer-heard mask retained from the owned message envelope.", 9U, 5U, 3U, 3U, 1U},
-    SourceMember{"state_revision", "u64.decimal", "Activity Host state revision encoded as a decimal string.", 9U, 6U, 4U, 3U, 1U},
-    SourceMember{"objects_decoded", "u32", "Number of object blocks decoded completely.", 9U, 7U, 3U, 3U, 1U},
-    SourceMember{"groups_decoded", "u32", "Number of entity groups decoded completely.", 9U, 8U, 3U, 3U, 1U},
-    SourceMember{"observations", "activity.sense-observation-view", "Bounded immutable view of package-selected Sense observations; it contains no raw message body.", 9U, 9U, 9U, 3U, 128U},
-    SourceMember{"session_id", "u64.decimal", "Peer activity-session id.", 10U, 0U, 4U, 3U, 1U},
-    SourceMember{"session_generation", "u64.decimal", "Peer record creation revision, so a replacement reusing the id is told apart from the record it replaced.", 10U, 1U, 4U, 3U, 1U},
-    SourceMember{"member_key", "u64.decimal", "Client key the peer's join bound, or zero before one.", 10U, 2U, 4U, 3U, 1U},
-    SourceMember{"joined_revision", "u64.decimal", "State revision that committed the peer's join.", 10U, 3U, 4U, 3U, 1U},
-    SourceMember{"session_id", "u64.decimal", "Peer activity-session id.", 11U, 0U, 4U, 3U, 1U},
-    SourceMember{"session_generation", "u64.decimal", "Peer record creation revision the watch held.", 11U, 1U, 4U, 3U, 1U},
-    SourceMember{"member_key", "u64.decimal", "Client key the watch held, or zero when the peer never joined.", 11U, 2U, 4U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the squad slot.", 12U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the squad slot.", 12U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the squad slot.", 12U, 2U, 3U, 3U, 1U},
-    SourceMember{"alive_count", "u32", "Alive members the client published. Six bits on the wire, so 0 through 63.", 12U, 3U, 2U, 3U, 1U},
-    SourceMember{"removal_flag", "boolean", "Set by the client on its actor death or removal path. The meaning is unproved.", 12U, 4U, 1U, 3U, 1U},
-    SourceMember{"slot_counts", "activity.squad-slot-counts", "Per-slot member counts, cut to the list's four-bit length.", 12U, 5U, 8U, 3U, 8U},
-    SourceMember{"timer_name", "mission.state-key", "Canonical bounded name of the due timer.", 13U, 0U, 6U, 3U, 1U},
-    SourceMember{"timer_deadline_tick", "u64.decimal", "Absolute monotonic service tick committed when the timer was armed.", 13U, 1U, 4U, 3U, 1U},
-    SourceMember{"timer_sequence", "u64.decimal", "Stable nonzero timer identity used for retry and reattach correlation.", 13U, 2U, 4U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the occupancy slot.", 14U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the occupancy slot.", 14U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the occupancy slot.", 14U, 2U, 3U, 3U, 1U},
-    SourceMember{"member_count", "u32", "Members of the watched set inside the volume.", 14U, 3U, 2U, 3U, 1U},
-    SourceMember{"threshold", "u32", "Member count the server asked the client to compare against.", 14U, 4U, 2U, 3U, 1U},
-    SourceMember{"all_inside", "boolean", "True when the whole watched set is inside the volume.", 14U, 5U, 1U, 3U, 1U},
-    SourceMember{"registry_key", "u32", "Registry key of the object carrying the occupancy slot.", 15U, 0U, 3U, 3U, 1U},
-    SourceMember{"object_tag", "u32", "Owning object tag of the occupancy slot.", 15U, 1U, 3U, 3U, 1U},
-    SourceMember{"slot_index", "activity.slot", "Slot index of the occupancy slot.", 15U, 2U, 3U, 3U, 1U},
-    SourceMember{"member_count", "u32", "Members of the watched set inside the volume.", 15U, 3U, 2U, 3U, 1U},
-    SourceMember{"threshold", "u32", "Member count the server asked the client to compare against.", 15U, 4U, 2U, 3U, 1U},
-    SourceMember{"all_inside", "boolean", "True when the whole watched set is inside the volume.", 15U, 5U, 1U, 3U, 1U},
-    SourceMember{"scene", "activity.authored-scene", "Generation-bound authored-scene handle resolved from the pinned activity SDK view.", 16U, 0U, 7U, 3U, 1U},
-    SourceMember{"slot", "activity.slot", "Generation-bound slot handle with the exact type-23 component and schema tuple.", 17U, 0U, 7U, 3U, 1U},
-    SourceMember{"channel", "activity.type23.channel", "One of position, power, or lock.", 17U, 1U, 6U, 3U, 1U},
-    SourceMember{"value", "real32.unit-interval", "Finite real value in the closed interval zero through one.", 17U, 2U, 5U, 3U, 1U},
-    SourceMember{"snap", "boolean", "Optional snap flag; the Lua compatibility method defaults it to true.", 17U, 3U, 1U, 1U, 1U},
-    SourceMember{"slot", "activity.slot", "Generation-bound slot handle with the exact type-23 component and schema tuple.", 18U, 0U, 7U, 3U, 1U},
-    SourceMember{"transition", "activity.type23.transition", "One named transition over the position, power, or lock lane.", 18U, 1U, 6U, 3U, 1U},
-    SourceMember{"state", "activity.lifetime-state", "Lifetime state zero through ten. The client spawn gate has no jump-table entry above ten.", 19U, 0U, 3U, 3U, 1U},
-    SourceMember{"slot", "activity.slot", "Generation-bound slot handle with the exact type-30 component and schema tuple.", 20U, 0U, 7U, 3U, 1U},
-    SourceMember{"reference", "activity.slot", "Slot handle naming the player set the condition measures inside its authored volume.", 20U, 1U, 7U, 3U, 1U},
-    SourceMember{"threshold", "activity.type30.threshold", "Signed 32-bit threshold. The client echoes it back on Sense and never reads it.", 20U, 2U, 2U, 3U, 1U},
-    SourceMember{"slot", "activity.slot", "Generation-bound Slot handle carrying the exact object, registry key, slot type/index, and Auth schema identity.", 21U, 0U, 7U, 3U, 1U},
-    SourceMember{"draft", "activity.slot-auth-draft", "Dense explicit typed rows compiled by the generated Auth reflection schema; no raw body bytes are accepted from Lua.", 21U, 1U, 9U, 3U, 2048U},
-    SourceMember{"squad", "activity.squad", "Generation-bound runnable squad handle resolved from the pinned activity SDK view.", 22U, 0U, 7U, 3U, 1U},
-    SourceMember{"counts", "activity.squad-member-counts", "Optional ordered member counts bounded by the generated authored defaults.", 22U, 1U, 8U, 1U, 15U},
-    SourceMember{"mode", "activity.squad-place-mode", "Optional mode restricted to zero or two; the compatibility method defaults to zero.", 22U, 2U, 3U, 1U, 1U},
-    SourceMember{"slot", "activity.slot", "Generation-bound slot handle with the exact type-31 schema tuple.", 23U, 0U, 7U, 3U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded variable name.", 24U, 0U, 6U, 3U, 1U},
-    SourceMember{"removed", "boolean", "True when the callback candidate contained the selected variable.", 24U, 1U, 1U, 3U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded variable name.", 25U, 0U, 6U, 3U, 1U},
-    SourceMember{"value", "mission.variable-value", "One copied scalar from the closed durable value union.", 25U, 1U, 9U, 3U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded variable name.", 26U, 0U, 6U, 3U, 1U},
-    SourceMember{"exists", "boolean", "True when the candidate contains the selected variable.", 26U, 1U, 1U, 3U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded timer name.", 27U, 0U, 6U, 3U, 1U},
-    SourceMember{"deadline_tick", "u64.decimal", "Absolute monotonic service deadline encoded as a decimal string.", 27U, 1U, 4U, 1U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded variable name.", 28U, 0U, 6U, 3U, 1U},
-    SourceMember{"value", "mission.variable-value", "Copied scalar from the current callback candidate.", 28U, 1U, 9U, 1U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded timer name.", 29U, 0U, 6U, 3U, 1U},
-    SourceMember{"canceled", "boolean", "True when the candidate contained the selected timer.", 29U, 1U, 1U, 3U, 1U},
-    SourceMember{"name", "mission.state-key", "Canonical bounded timer name.", 30U, 0U, 6U, 3U, 1U},
-    SourceMember{"delay_ms", "duration.milliseconds.u32", "Nonnegative delay added to the callback service tick with saturation.", 30U, 1U, 3U, 3U, 1U},
-    SourceMember{"sequence", "u64.decimal", "New stable nonzero timer identity encoded as a decimal string.", 30U, 2U, 4U, 3U, 1U},
-}};
-
-inline constexpr std::array<SourceMapping, 122> kSourceMappings{{
-    SourceMapping{"mapping/activity-incident/sequence", "sequence", 0U, 0U, 4U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/activity-incident/source-generation", "source_generation", 0U, 1U, 4U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/activity-incident/client-message-sequence", "client_message_sequence", 0U, 2U, 4U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/activity-incident/mission-sequence", "mission_sequence", 0U, 3U, 4U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/activity-incident/payload-bytes", "payload_bytes", 0U, 4U, 4U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/activity-incident/target", "incident_target", 0U, 5U, 1U, 5U, 19U, "[0]", "primaryTarget", true, 1U},
-    SourceMapping{"mapping/activity-incident/extra-target-count", "incident_extra_targets", 0U, 6U, 1U, 6U, 19U, "[1][0]", "extraCount", true, 1U},
-    SourceMapping{"mapping/activity-incident/selector-byte-count", "incident_selector_bytes", 0U, 7U, 1U, 7U, 19U, "[2][1][0][0]", "selectorByteLength", true, 1U},
-    SourceMapping{"mapping/activity-incident/payload-byte-count", "incident_payload_bytes", 0U, 8U, 1U, 8U, 19U, "[4][0]", "payloadByteLength", true, 1U},
-    SourceMapping{"mapping/client-state-changed/sequence", "sequence", 1U, 9U, 4U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/source-generation", "source_generation", 1U, 10U, 4U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/client-message-sequence", "client_message_sequence", 1U, 11U, 4U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/mission-sequence", "mission_sequence", 1U, 12U, 4U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/payload-bytes", "payload_bytes", 1U, 13U, 4U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/activity-state-revision", "activity_state_revision", 1U, 14U, 5U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/membership-revision", "membership_revision", 1U, 15U, 5U, 6U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/client-state-changed/region-index", "region_index", 1U, 16U, 1U, 7U, 22U, "[1][1][2]", "region_index", true, 1U},
-    SourceMapping{"mapping/client-state-changed/region-slice-set-hash", "region_slice_set_hash", 1U, 17U, 1U, 8U, 22U, "[1][1][1]", "slice_set_name_hash", true, 1U},
-    SourceMapping{"mapping/client-state-changed/spawn-state", "spawn_state", 1U, 18U, 1U, 9U, 22U, "[2][0]", "spawn_state", true, 1U},
-    SourceMapping{"mapping/client-state-changed/teleport-state", "teleport_state", 1U, 19U, 1U, 10U, 22U, "[3][0]", "teleport_state", true, 1U},
-    SourceMapping{"mapping/client-state-changed/teleport-slice-set-index", "teleport_slice_set_index", 1U, 20U, 1U, 11U, 22U, "[3][1][1]", "teleport_slice_set_index", true, 1U},
-    SourceMapping{"mapping/client-state-changed/teleport-slice-set-hash", "teleport_slice_set_hash", 1U, 21U, 1U, 12U, 22U, "[3][1][2]", "the teleport slice-set hash", true, 1U},
-    SourceMapping{"mapping/effect-result/request-key", "request_key", 2U, 22U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/effect-result/effect", "effect", 2U, 23U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/effect-result/outcome", "outcome", 2U, 24U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/effect-result/outcome-code", "outcome_code", 2U, 25U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-died/registry-key", "registry_key", 3U, 26U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-died/object-tag", "object_tag", 3U, 27U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-died/slot-index", "slot_index", 3U, 28U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-died/alive-count", "alive_count", 3U, 29U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-died/previous-alive-count", "previous_alive_count", 3U, 30U, 5U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-slots-requested/requested-count", "requested_count", 4U, 31U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-spawned/registry-key", "registry_key", 5U, 32U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-spawned/object-tag", "object_tag", 5U, 33U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-spawned/slot-index", "slot_index", 5U, 34U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-spawned/member-slot", "member_slot", 5U, 35U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-spawned/count", "count", 5U, 36U, 5U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/entity-spawned/previous-count", "previous_count", 5U, 37U, 5U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/registry-key", "registry_key", 6U, 38U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/object-tag", "object_tag", 6U, 39U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/slot-index", "slot_index", 6U, 40U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/objective", "objective", 6U, 41U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/task", "task", 6U, 42U, 5U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/task-count", "task_count", 6U, 43U, 5U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/objective-progress/previous-task-count", "previous_task_count", 6U, 44U, 5U, 6U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/phase-entered/phase", "phase", 7U, 45U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/phase-entered/previous-phase", "previous_phase", 7U, 46U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/phase-entered/state-revision", "state_revision", 7U, 47U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/scene-finished/registry-key", "registry_key", 8U, 48U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/scene-finished/object-tag", "object_tag", 8U, 49U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/scene-finished/slot-index", "slot_index", 8U, 50U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/scene-finished/activation-token", "activation_token", 8U, 51U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/sequence", "sequence", 9U, 52U, 4U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/source-generation", "source_generation", 9U, 53U, 4U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/client-message-sequence", "client_message_sequence", 9U, 54U, 4U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/mission-sequence", "mission_sequence", 9U, 55U, 4U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/payload-bytes", "payload_bytes", 9U, 56U, 4U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/peer-heard-mask", "peer_heard_mask", 9U, 57U, 4U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/state-revision", "state_revision", 9U, 58U, 5U, 6U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/objects-decoded", "objects_decoded", 9U, 59U, 2U, 7U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/groups-decoded", "groups_decoded", 9U, 60U, 2U, 8U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/sensor-sense/observations", "observations", 9U, 61U, 2U, 9U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-joined/session-id", "session_id", 10U, 62U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-joined/session-generation", "session_generation", 10U, 63U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-joined/member-key", "member_key", 10U, 64U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-joined/joined-revision", "joined_revision", 10U, 65U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-left/session-id", "session_id", 11U, 66U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-left/session-generation", "session_generation", 11U, 67U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/session-left/member-key", "member_key", 11U, 68U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-state/registry-key", "registry_key", 12U, 69U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-state/object-tag", "object_tag", 12U, 70U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-state/slot-index", "slot_index", 12U, 71U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-state/alive-count", "alive_count", 12U, 72U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-state/removal-flag", "removal_flag", 12U, 73U, 5U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-state/slot-counts", "slot_counts", 12U, 74U, 5U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-elapsed/name", "timer_name", 13U, 75U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-elapsed/deadline-tick", "timer_deadline_tick", 13U, 76U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-elapsed/sequence", "timer_sequence", 13U, 77U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-entered/registry-key", "registry_key", 14U, 78U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-entered/object-tag", "object_tag", 14U, 79U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-entered/slot-index", "slot_index", 14U, 80U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-entered/member-count", "member_count", 14U, 81U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-entered/threshold", "threshold", 14U, 82U, 5U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-entered/all-inside", "all_inside", 14U, 83U, 5U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-exited/registry-key", "registry_key", 15U, 84U, 5U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-exited/object-tag", "object_tag", 15U, 85U, 5U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-exited/slot-index", "slot_index", 15U, 86U, 5U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-exited/member-count", "member_count", 15U, 87U, 5U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-exited/threshold", "threshold", 15U, 88U, 5U, 4U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-exited/all-inside", "all_inside", 15U, 89U, 5U, 5U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/authored-scene-activate/scene", "scene", 16U, 90U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/device-set-channel/slot", "slot", 17U, 91U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/device-set-channel/channel", "channel", 17U, 92U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/device-set-channel/value", "value", 17U, 93U, 2U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/device-set-channel/snap", "snap", 17U, 94U, 2U, 3U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/device-transition/slot", "slot", 18U, 95U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/device-transition/transition", "transition", 18U, 96U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/lifetime-set/state", "state", 19U, 97U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/occupancy-condition-set/slot", "slot", 20U, 98U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/occupancy-condition-set/reference", "reference", 20U, 99U, 3U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/occupancy-condition-set/threshold", "threshold", 20U, 100U, 2U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/slot-apply-auth/slot", "slot", 21U, 101U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/slot-apply-auth/draft", "draft", 21U, 102U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-place/squad", "squad", 22U, 103U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-place/counts", "counts", 22U, 104U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/squad-place/mode", "mode", 22U, 105U, 2U, 2U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/trigger-fire/slot", "slot", 23U, 106U, 3U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-clear/name", "name", 24U, 107U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-clear/removed", "removed", 24U, 108U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-set/name", "name", 25U, 109U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-set/value", "value", 25U, 110U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-has/name", "name", 26U, 111U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-has/exists", "exists", 26U, 112U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-timer/name", "name", 27U, 113U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-timer/deadline-tick", "deadline_tick", 27U, 114U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-variable/name", "name", 28U, 115U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/state-variable/value", "value", 28U, 116U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-cancel/name", "name", 29U, 117U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-cancel/canceled", "canceled", 29U, 118U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-start/name", "name", 30U, 119U, 2U, 0U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-start/delay-ms", "delay_ms", 30U, 120U, 2U, 1U, 4294967295U, "", "", false, 1U},
-    SourceMapping{"mapping/timer-start/sequence", "sequence", 30U, 121U, 2U, 2U, 4294967295U, "", "", false, 1U},
-}};

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

@@ -262,8 +262,6 @@ const char* status_name(Status value) noexcept {
         return "publication";
     case Status::luaPublication:
         return "lua_publication";
-    case Status::reload:
-        return "reload";
     }
     return "invalid_input";
 }
@@ -329,11 +327,8 @@ Status stage(const wchar_t* sdkDirectory,
         actor_rsat::Snapshot actorRows{};
         std::vector<std::uint32_t> installedRsats{};
         reader::ScanResult rsatScan{};
-        if (!reader::scan_class(source.directory,
-                                actor_rsat::kActorRsatClass,
-                                &collect_tag,
-                                &installedRsats,
-                                rsatScan)) {
+        if (!reader::scan_class(
+                source, actor_rsat::kActorRsatClass, &collect_tag, &installedRsats, rsatScan)) {
             return Status::actorRsat;
         }
         std::sort(installedRsats.begin(), installedRsats.end());
@@ -468,7 +463,6 @@ Status stage(const wchar_t* sdkDirectory,
         output.identity = identity;
         output.payloadSha256 = payload;
         output.fileBytes = fileBytes;
-        output.luaBytes = luaResult.byteCount;
         output.luaFiles = luaResult.fileCount;
         return Status::ready;
     } catch (...) {
@@ -477,46 +471,4 @@ Status stage(const wchar_t* sdkDirectory,
     }
 }
 
-/** Builds, publishes, and reloads one canonical native SDK generation. */
-Status publish(void* module,
-               const wchar_t* sdkDirectory,
-               const wchar_t* packPath,
-               const reader::Source& source,
-               const pack::Digest& sourceFingerprint,
-               const activity_inventory::Snapshot& activities,
-               topology_inventory::Snapshot& topology,
-               const external_placements::Index& externalPlacements,
-               bool luaDeclarations,
-               CancelProbe cancel,
-               void* cancelContext,
-               Result& output) noexcept {
-    output = {};
-    if (module == nullptr) {
-        return Status::invalidInput;
-    }
-    const Status staged = stage(sdkDirectory,
-                                packPath,
-                                source,
-                                sourceFingerprint,
-                                activities,
-                                topology,
-                                {},
-                                externalPlacements,
-                                luaDeclarations,
-                                cancel,
-                                cancelContext,
-                                nullptr,
-                                nullptr,
-                                output);
-    if (staged != Status::ready) {
-        return staged;
-    }
-    if (!state::activity_sdk::reload(module, output.identity)) {
-        output = {};
-        return Status::reload;
-    }
-    // A reload replaces the catalog the wire descriptors borrow, so they are rebuilt here.
-    return Status::ready;
-}
-
 } // namespace sunrise::client::content::activity::sdk_generation::native_pack_pipeline

+ 0 - 20
Sunrise/src/client/content/activity/activity_sdk_native_pack_pipeline.h

@@ -37,7 +37,6 @@ enum class Status : std::uint8_t {
     luaBuild,
     publication,
     luaPublication,
-    reload,
 };
 
 /** Real pack-building boundaries exposed to presentation without changing generation state. */
@@ -69,7 +68,6 @@ struct Result final {
     state::activity_sdk::identity::Expected identity{};
     pack::Digest payloadSha256{};
     std::uint64_t fileBytes{};
-    std::uint64_t luaBytes{};
     std::uint32_t luaFiles{};
 };
 
@@ -96,22 +94,4 @@ struct Result final {
                            void* progressContext,
                            Result& output) noexcept;
 
-/**
- * Builds and publishes the format-v13 activity and panel SDK from installed content.
- * The topology is consumed as mutable one-shot generation state because activity enrichment adds
- * its final names and exactness bit before later joins run.
- */
-[[nodiscard]] Status publish(void* module,
-                             const wchar_t* sdkDirectory,
-                             const wchar_t* packPath,
-                             const middleware::content::packages::reader::Source& source,
-                             const pack::Digest& sourceFingerprint,
-                             const activity_inventory::Snapshot& activities,
-                             topology_inventory::Snapshot& topology,
-                             const external_placements::Index& externalPlacements,
-                             bool luaDeclarations,
-                             CancelProbe cancel,
-                             void* cancelContext,
-                             Result& output) noexcept;
-
 } // namespace sunrise::client::content::activity::sdk_generation::native_pack_pipeline

+ 5 - 14
Sunrise/src/client/content/activity/activity_sdk_one_shot.cpp

@@ -10,6 +10,7 @@
 #include <string_view>
 #include <utility>
 
+#include "../../../core/filesystem/path.h"
 #include "../../../state/content_manifest/content_manifest_state_runtime.h"
 #include "../items/packages/internal.h"
 #include "activity_sdk_estate_validation.h"
@@ -31,16 +32,6 @@ volatile LONG g_stageSequence{};
     return probe != nullptr && probe(context);
 }
 
-[[nodiscard]] bool copy_fingerprint(void* opaque,
-                                    const state::content_manifest::View& view) noexcept {
-    if (opaque == nullptr || view.buildFingerprint.size() != generated::Digest{}.size()) {
-        return false;
-    }
-    auto& output = *static_cast<generated::Digest*>(opaque);
-    std::copy(view.buildFingerprint.begin(), view.buildFingerprint.end(), output.begin());
-    return true;
-}
-
 void report(OfflineProgressSink sink,
             void* context,
             state::activity_sdk::generation::Status status,
@@ -162,9 +153,8 @@ void report(OfflineProgressSink sink,
             if (written != 0 && written < needed) {
                 buffer.resize(written);
                 // Windows returns the extended prefix; callers store the plain path.
-                constexpr std::wstring_view kExtended = L"\\\\?\\";
-                if (buffer.starts_with(kExtended)) {
-                    buffer.erase(0, kExtended.size());
+                if (buffer.starts_with(core::path::kExtendedPrefix)) {
+                    buffer.erase(0, core::path::kExtendedPrefix.size());
                 }
                 while (buffer.size() > 3U && (buffer.back() == L'\\' || buffer.back() == L'/')) {
                     buffer.pop_back();
@@ -380,7 +370,8 @@ Status run_process_local(std::wstring_view packageDirectory,
     static_cast<void>(cacheArtifactDirectory);
     generated::Digest fingerprint{};
     middleware::content::packages::reader::BlockKeys keys{};
-    if (!state::content_manifest::visit_snapshot(&copy_fingerprint, &fingerprint)
+    // The SDK keys on the installed identity; mods never change it.
+    if (!state::content_manifest::installed_fingerprint(fingerprint)
         || !items::packages::collect_keys(keys)) {
         SecureZeroMemory(&keys, sizeof keys);
         return Status::invalidInput;

+ 53 - 110
Sunrise/src/client/content/activity/activity_sdk_pack_composer.cpp

@@ -30,11 +30,6 @@ namespace {
 
 namespace evidence = state::build_data::scriptables::inline_name_evidence;
 
-/** Format-v9 state rows use bits zero and one for enabled and extraction-complete state. */
-constexpr std::uint32_t kStateEnabled = 0x1U;
-constexpr std::uint32_t kStateExtractionComplete = 0x2U;
-static_assert((kStateEnabled | kStateExtractionComplete) == format::kStateFlagMask);
-
 using detail::PreparedRanges;
 
 /** UTF-8 text is ordered by bytes, independent of the signedness of `char`. */
@@ -289,33 +284,6 @@ template <typename Text> [[nodiscard]] bool keep_legacy(StringLinker& linker, co
     return text_view(input, value) && linker.keep_legacy(value);
 }
 
-template <typename Text>
-[[nodiscard]] bool keep_extension(StringLinker& linker, const Text& input) {
-    std::string_view value{};
-    return text_view(input, value) && linker.keep_extension(value);
-}
-
-template <typename Text>
-[[nodiscard]] bool
-link_text(const StringLinker& linker, const Text& input, format::StringRef& output) noexcept {
-    std::string_view value{};
-    return text_view(input, value) && linker.reference(value, output);
-}
-
-[[nodiscard]] bool link_string(const StringLinker& linker,
-                               std::string_view input,
-                               format::StringRef& output) noexcept {
-    return valid_text(input) && linker.reference(input, output);
-}
-
-[[nodiscard]] bool link_policy_text(const StringLinker& linker,
-                                    const policy_inventory::Snapshot& source,
-                                    policy_inventory::Text input,
-                                    format::StringRef& output) noexcept {
-    std::string_view value{};
-    return policy_text_view(source, input, value) && linker.reference(value, output);
-}
-
 [[nodiscard]] bool all_inputs_present(const Inputs& inputs) noexcept {
     return inputs.activityInventory != nullptr && inputs.activityEnrichment != nullptr
            && inputs.topology != nullptr && inputs.topologyEnrichment != nullptr
@@ -324,6 +292,53 @@ link_text(const StringLinker& linker, const Text& input, format::StringRef& outp
            && inputs.authoredScenes != nullptr && inputs.behaviors != nullptr;
 }
 
+/**
+ * Builds every parent range the row translation reads.
+ * @param topology Topology rows that own bubbles, states, occurrences and slots.
+ * @param policy Policy rows that own capabilities.
+ * @param ranges Receives the ranges; partial on failure.
+ * @return True when every owner index and range is valid.
+ */
+[[nodiscard]] bool make_ranges(const topology_inventory::Snapshot& topology,
+                               const policy_inventory::Snapshot& policy,
+                               PreparedRanges& ranges) {
+    return make_owner_ranges(
+               std::span<const topology_inventory::Bubble>(topology.bubbles),
+               topology.scenarios.size(),
+               [](const topology_inventory::Bubble& row) { return row.scenarioIndex; },
+               ranges.scenarioBubbles)
+           && make_owner_ranges(
+               std::span<const topology_inventory::State>(topology.states),
+               topology.scenarios.size(),
+               [](const topology_inventory::State& row) { return row.scenarioIndex; },
+               ranges.scenarioStates)
+           && make_owner_ranges(
+               std::span<const topology_inventory::Occurrence>(topology.occurrences),
+               topology.scenarios.size(),
+               [](const topology_inventory::Occurrence& row) { return row.scenarioIndex; },
+               ranges.scenarioOccurrences)
+           && make_owner_ranges(
+               std::span<const topology_inventory::State>(topology.states),
+               topology.bubbles.size(),
+               [](const topology_inventory::State& row) { return row.bubbleIndex; },
+               ranges.bubbleStates)
+           && make_owner_ranges(
+               std::span<const topology_inventory::Slot>(topology.slots),
+               topology.objects.size(),
+               [](const topology_inventory::Slot& row) { return row.objectIndex; },
+               ranges.objectSlots)
+           && make_capability_ranges(policy,
+                                     format::SubjectKind::activity,
+                                     topology.activities.size(),
+                                     ranges.activityCapabilities)
+           && make_capability_ranges(
+               policy, format::SubjectKind::slot, topology.slots.size(), ranges.slotCapabilities)
+           && make_capability_ranges(policy,
+                                     format::SubjectKind::hostApi,
+                                     policy.hostSubjects.size(),
+                                     ranges.hostCapabilities);
+}
+
 /** Checks accepted-snapshot markers, fixed counts, and cross-inventory parent identity. */
 [[nodiscard]] bool prepare_inputs(const Inputs& inputs, PreparedRanges& ranges) {
     if (!all_inputs_present(inputs)) {
@@ -437,41 +452,7 @@ link_text(const StringLinker& linker, const Text& input, format::StringRef& outp
         }
     }
 
-    if (!make_owner_ranges(
-            std::span<const topology_inventory::Bubble>(topology.bubbles),
-            topology.scenarios.size(),
-            [](const topology_inventory::Bubble& row) { return row.scenarioIndex; },
-            ranges.scenarioBubbles)
-        || !make_owner_ranges(
-            std::span<const topology_inventory::State>(topology.states),
-            topology.scenarios.size(),
-            [](const topology_inventory::State& row) { return row.scenarioIndex; },
-            ranges.scenarioStates)
-        || !make_owner_ranges(
-            std::span<const topology_inventory::Occurrence>(topology.occurrences),
-            topology.scenarios.size(),
-            [](const topology_inventory::Occurrence& row) { return row.scenarioIndex; },
-            ranges.scenarioOccurrences)
-        || !make_owner_ranges(
-            std::span<const topology_inventory::State>(topology.states),
-            topology.bubbles.size(),
-            [](const topology_inventory::State& row) { return row.bubbleIndex; },
-            ranges.bubbleStates)
-        || !make_owner_ranges(
-            std::span<const topology_inventory::Slot>(topology.slots),
-            topology.objects.size(),
-            [](const topology_inventory::Slot& row) { return row.objectIndex; },
-            ranges.objectSlots)
-        || !make_capability_ranges(policy,
-                                   format::SubjectKind::activity,
-                                   topology.activities.size(),
-                                   ranges.activityCapabilities)
-        || !make_capability_ranges(
-            policy, format::SubjectKind::slot, topology.slots.size(), ranges.slotCapabilities)
-        || !make_capability_ranges(policy,
-                                   format::SubjectKind::hostApi,
-                                   policy.hostSubjects.size(),
-                                   ranges.hostCapabilities)) {
+    if (!make_ranges(topology, policy, ranges)) {
         return false;
     }
 
@@ -507,49 +488,11 @@ link_text(const StringLinker& linker, const Text& input, format::StringRef& outp
 
 /** Derives only the parent ranges required to translate generation-owned rows. */
 [[nodiscard]] bool prepare_generated_ranges(const Inputs& inputs, PreparedRanges& ranges) {
-    if (!all_inputs_present(inputs)) {
-        return false;
-    }
-    const topology_inventory::Snapshot& topology = *inputs.topology;
-    const policy_inventory::Snapshot& policy = *inputs.policy;
-    return make_owner_ranges(
-               std::span<const topology_inventory::Bubble>(topology.bubbles),
-               topology.scenarios.size(),
-               [](const topology_inventory::Bubble& row) { return row.scenarioIndex; },
-               ranges.scenarioBubbles)
-           && make_owner_ranges(
-               std::span<const topology_inventory::State>(topology.states),
-               topology.scenarios.size(),
-               [](const topology_inventory::State& row) { return row.scenarioIndex; },
-               ranges.scenarioStates)
-           && make_owner_ranges(
-               std::span<const topology_inventory::Occurrence>(topology.occurrences),
-               topology.scenarios.size(),
-               [](const topology_inventory::Occurrence& row) { return row.scenarioIndex; },
-               ranges.scenarioOccurrences)
-           && make_owner_ranges(
-               std::span<const topology_inventory::State>(topology.states),
-               topology.bubbles.size(),
-               [](const topology_inventory::State& row) { return row.bubbleIndex; },
-               ranges.bubbleStates)
-           && make_owner_ranges(
-               std::span<const topology_inventory::Slot>(topology.slots),
-               topology.objects.size(),
-               [](const topology_inventory::Slot& row) { return row.objectIndex; },
-               ranges.objectSlots)
-           && make_capability_ranges(policy,
-                                     format::SubjectKind::activity,
-                                     topology.activities.size(),
-                                     ranges.activityCapabilities)
-           && make_capability_ranges(
-               policy, format::SubjectKind::slot, topology.slots.size(), ranges.slotCapabilities)
-           && make_capability_ranges(policy,
-                                     format::SubjectKind::hostApi,
-                                     policy.hostSubjects.size(),
-                                     ranges.hostCapabilities);
+    return all_inputs_present(inputs) && make_ranges(*inputs.topology, *inputs.policy, ranges);
 }
 
-/** Collects only strings referenced by legacy sections zero through twenty-two. */
+/** Collects the strings the pack rows reference. Actor sequence entries go in the extension tier.
+ */
 [[nodiscard]] bool collect_legacy_strings(const Inputs& inputs, StringLinker& linker) {
     const topology_inventory::Snapshot& topology = *inputs.topology;
     const topology_enrichment::Snapshot& enrichment = *inputs.topologyEnrichment;
@@ -808,7 +751,7 @@ bool compose(const Inputs& inputs, Storage& output) noexcept {
         if (!detail::translate_rows(inputs, ranges, strings, pending)) {
             return false;
         }
-        if (!detail::validate_storage(inputs, ranges, pending)) {
+        if (!detail::validate_storage(ranges, pending)) {
             return false;
         }
         output = std::move(pending);
@@ -840,7 +783,7 @@ bool compose_generated(const Inputs& inputs, Storage& output) noexcept {
 }
 
 /**
- * Builds a format-v12 image without changing outputs on failure.
+ * Builds a pack image without changing outputs on failure.
  * @param identity Required pack identity.
  * @param inputs Complete accepted inventories.
  * @param tables Owned final rows.

+ 1 - 3
Sunrise/src/client/content/activity/activity_sdk_pack_composer_internal.h

@@ -36,8 +36,6 @@ struct StringResolver final {
                                   Storage& output);
 
 /** Checks section counts fit u32 and the prepared per-parent range vectors match. */
-[[nodiscard]] bool validate_storage(const Inputs& inputs,
-                                    const PreparedRanges& prepared,
-                                    const Storage& value) noexcept;
+[[nodiscard]] bool validate_storage(const PreparedRanges& prepared, const Storage& value) noexcept;
 
 } // namespace sunrise::client::content::activity::sdk_generation::pack_composer::detail

+ 2 - 35
Sunrise/src/client/content/activity/activity_sdk_pack_composer_rows.cpp

@@ -1,7 +1,7 @@
-#include <array>
 #include <cstddef>
 #include <cstdint>
 #include <limits>
+#include <span>
 #include <string_view>
 
 #include "activity_sdk_activity_inventory.h"
@@ -17,7 +17,7 @@
 namespace sunrise::client::content::activity::sdk_generation::pack_composer {
 namespace {
 
-/** Format-v9 state rows use bits zero and one for enabled and extraction-complete state. */
+/** State rows use bit zero for enabled and bit one for extraction-complete. */
 constexpr std::uint32_t kStateEnabled = 0x1U;
 constexpr std::uint32_t kStateExtractionComplete = 0x2U;
 static_assert((kStateEnabled | kStateExtractionComplete) == format::kStateFlagMask);
@@ -65,39 +65,6 @@ template <typename Text>
     return policy_text_view(source, input, value) && strings.resolve(value, output);
 }
 
-[[nodiscard]] int hexadecimal_digit(char value) noexcept {
-    if (value >= '0' && value <= '9') {
-        return value - '0';
-    }
-    if (value >= 'a' && value <= 'f') {
-        return value - 'a' + 10;
-    }
-    return -1;
-}
-
-/** Decodes the generated lowercase SHA-256 spelling without accepting a partial digest. */
-[[nodiscard]] bool decode_digest(std::string_view input,
-                                 std::array<std::byte, 32>& output) noexcept {
-    output = {};
-    if (input.size() != output.size() * 2U) {
-        return false;
-    }
-    for (std::size_t index = 0; index < output.size(); ++index) {
-        const int high = hexadecimal_digit(input[index * 2U]);
-        const int low = hexadecimal_digit(input[index * 2U + 1U]);
-        if (high < 0 || low < 0) {
-            output = {};
-            return false;
-        }
-        output[index] = static_cast<std::byte>((high << 4) | low);
-    }
-    return true;
-}
-
-template <typename Enum> [[nodiscard]] std::uint32_t semantic_ordinal(Enum value) noexcept {
-    return static_cast<std::uint32_t>(value) + 1U;
-}
-
 /** Appends one canonical tag set and returns its exact range in the shared tag section. */
 [[nodiscard]] bool append_binding_tags(std::span<const std::uint32_t> source,
                                        std::vector<format::ActivityBindingTag>& rows,

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

@@ -7,9 +7,7 @@
 namespace sunrise::client::content::activity::sdk_generation::pack_composer::detail {
 
 /** Checks the retained activity and panel tables at the public composition boundary. */
-bool validate_storage(const Inputs&,
-                      const PreparedRanges& prepared,
-                      const Storage& value) noexcept {
+bool validate_storage(const PreparedRanges& prepared, const Storage& value) noexcept {
     const std::array<std::size_t, format::kSectionCount> counts{
         value.strings.size(),
         value.activities.size(),

+ 2 - 18
Sunrise/src/client/content/activity/activity_sdk_policy_input_validation.cpp

@@ -13,7 +13,7 @@ namespace {
 
 namespace evidence = state::build_data::scriptables::inline_name_evidence;
 
-/** Object/scenario binding keys store each format-v12 index in one 32-bit half. */
+/** Object/scenario binding keys store each index in one 32-bit half. */
 constexpr std::size_t kObjectBindingShift = 32;
 
 /**
@@ -53,26 +53,10 @@ std::size_t StringHash::operator()(std::string_view value) const noexcept {
     return std::hash<std::string_view>{}(value);
 }
 
-std::size_t StringHash::operator()(const std::string& value) const noexcept {
-    return (*this)(std::string_view(value));
-}
-
 bool StringEqual::operator()(std::string_view left, std::string_view right) const noexcept {
     return left == right;
 }
 
-bool StringEqual::operator()(const std::string& left, const std::string& right) const noexcept {
-    return left == right;
-}
-
-bool StringEqual::operator()(const std::string& left, std::string_view right) const noexcept {
-    return std::string_view(left) == right;
-}
-
-bool StringEqual::operator()(std::string_view left, const std::string& right) const noexcept {
-    return left == std::string_view(right);
-}
-
 /**
  * Retains one nonempty failure reason within the fixed gate-set capacity.
  * @param value The optional stable reason code.
@@ -124,7 +108,7 @@ bool can_add(std::size_t current, std::size_t count, std::size_t maximum) noexce
  * Converts one owned interval while retaining the cursor for an empty range.
  * @param first The current child-row cursor.
  * @param count The number of owned rows.
- * @param output Receives the format-v12 range.
+ * @param output Receives the range.
  * @return True when both scalars fit the packed fields.
  */
 bool make_range(std::size_t first, std::size_t count, format::Range& output) noexcept {

+ 11 - 10
Sunrise/src/client/content/activity/activity_sdk_policy_inventory.cpp

@@ -23,6 +23,8 @@ constexpr std::size_t kFixedDeferredStringEstimate = 64;
 
 /** Slot type 31 contributes the currently unimplemented trigger adapter operation. */
 constexpr std::uint32_t kTriggerSlotType = 31;
+/** Auth schema of the type-31 player trigger sensor. */
+constexpr std::uint32_t kTriggerAuthSchema = 0x80809524U;
 
 /**
  * The compiled host inventory contains three implemented and two refused surfaces.
@@ -72,11 +74,10 @@ void Builder::reserve() {
     output_.activityCapabilities.resize(inputs_.activities.size());
     output_.slotAliases.resize(inputs_.slots.size());
     output_.slotCapabilities.resize(inputs_.slots.size());
-    const std::size_t expectedStrings =
-        (std::min)(kMaximumDeferredStringCount,
-                   inputs_.activities.size() * kActivityDeferredStringEstimate
-                       + inputs_.slots.size() * kSlotDeferredStringEstimate
-                       + kFixedDeferredStringEstimate);
+    const std::size_t expectedStrings = (std::min)(
+        kMaximumDeferredStringCount,
+        inputs_.activities.size() * kActivityDeferredStringEstimate
+            + inputs_.slots.size() * kSlotDeferredStringEstimate + kFixedDeferredStringEstimate);
     output_.strings.reserve(expectedStrings);
     stringIndexes_.reserve(expectedStrings);
 }
@@ -112,7 +113,7 @@ bool Builder::intern(std::string_view value, Text& output) {
 /**
  * Appends one nonempty canonical Text row.
  * @param value The UTF-8 row value.
- * @param kind The format-v12 text classification.
+ * @param kind The text classification.
  * @return True when the value is retained within the Text row bound.
  */
 bool Builder::add_text(std::string_view value, format::TextKind kind) {
@@ -256,7 +257,7 @@ bool Builder::add_refusal(std::string_view capabilityId,
  * @param subjectId The stable subject identity.
  * @param operation The stable operation name.
  * @param valueSchemaId The optional value schema identity.
- * @param subjectKind The format-v12 subject domain.
+ * @param subjectKind The subject domain.
  * @param subjectIndex The row within that subject domain.
  * @param exposureFlags The current allowed surfaces.
  * @param candidateExposureFlags The candidate-only surfaces.
@@ -334,8 +335,7 @@ bool Builder::build_activity_capabilities() {
         const std::size_t firstGate = output_.gates.size();
         FailureReasons failures{};
         const bool identityExact = input.joinStatus == ActivityJoinStatus::exact;
-        // The gate publishes the activity's binding reason code. Naming the join status here
-        // instead would invent a second vocabulary for the same fact.
+        // The gate publishes the activity's binding reason code, not the join status name.
         std::string_view identityReason = "activity_root_edge_missing";
         switch (input.joinStatus) {
         case ActivityJoinStatus::exact:
@@ -438,7 +438,8 @@ bool Builder::build_adapter_capability(const SlotInput& input,
     const bool type6 = input.slotType == format::kCinematicSlotType
                        && input.componentClass == format::kCinematicComponentClass
                        && input.authSchema == format::kCinematicAuthSchema;
-    const bool type31 = input.slotType == 31U && input.authSchema == 0x80809524U;
+    const bool type31 =
+        input.slotType == kTriggerSlotType && input.authSchema == kTriggerAuthSchema;
     const bool type53 = input.slotType == format::kDialogueSlotType
                         && input.componentClass == format::kDialogueComponentClass
                         && input.authSchema == format::kDialogueAuthSchema

+ 4 - 4
Sunrise/src/client/content/activity/activity_sdk_policy_inventory.h

@@ -102,7 +102,7 @@ struct Inputs final {
     std::uint32_t objectCount{};
 };
 
-/** One format-v12 Text row before its string offset is linked. */
+/** One Text row before its string offset is linked. */
 struct TextRow final {
     Text value{};
     std::uint32_t kind{};
@@ -111,7 +111,7 @@ struct TextRow final {
     bool operator==(const TextRow&) const = default;
 };
 
-/** One format-v12 Capability row before its strings are linked. */
+/** One Capability row before its strings are linked. */
 struct Capability final {
     Text id{};
     Text operation{};
@@ -134,7 +134,7 @@ struct Capability final {
     }
 };
 
-/** One format-v12 Gate row before its strings are linked. */
+/** One Gate row before its strings are linked. */
 struct Gate final {
     Text gate{};
     Text status{};
@@ -146,7 +146,7 @@ struct Gate final {
     bool operator==(const Gate&) const = default;
 };
 
-/** One format-v12 Refusal row before its strings are linked. */
+/** One Refusal row before its strings are linked. */
 struct Refusal final {
     Text id{};
     Text exposure{};

+ 1 - 6
Sunrise/src/client/content/activity/activity_sdk_policy_inventory_internal.h

@@ -16,12 +16,11 @@ namespace sunrise::client::content::activity::sdk_generation::policy_inventory::
 /** One capability's fixed gate sets produce at most this many unique refusal reasons. */
 inline constexpr std::size_t kMaximumFailureReasonCount = 16;
 
-/** These stable strings are scalar values in canonical format-v12 policy rows. */
+/** These stable strings are scalar values in canonical policy rows. */
 inline constexpr std::string_view kExact = "exact";
 inline constexpr std::string_view kPassed = "passed";
 inline constexpr std::string_view kFailed = "failed";
 inline constexpr std::string_view kUnresolved = "unresolved";
-inline constexpr std::string_view kInspect = "inspect";
 inline constexpr std::string_view kPanelTest = "panel_test";
 inline constexpr std::string_view kScript = "script";
 inline constexpr std::string_view kCandidate = "candidate";
@@ -38,7 +37,6 @@ struct StringHash final {
     using is_transparent = void;
 
     [[nodiscard]] std::size_t operator()(std::string_view value) const noexcept;
-    [[nodiscard]] std::size_t operator()(const std::string& value) const noexcept;
 };
 
 /** Compares owned and borrowed deferred strings without temporary allocations. */
@@ -46,9 +44,6 @@ struct StringEqual final {
     using is_transparent = void;
 
     [[nodiscard]] bool operator()(std::string_view left, std::string_view right) const noexcept;
-    [[nodiscard]] bool operator()(const std::string& left, const std::string& right) const noexcept;
-    [[nodiscard]] bool operator()(const std::string& left, std::string_view right) const noexcept;
-    [[nodiscard]] bool operator()(std::string_view left, const std::string& right) const noexcept;
 };
 
 /** Collects one capability's bounded refusal reasons before canonical sorting. */

+ 4 - 3
Sunrise/src/client/content/activity/activity_sdk_semantic_surface_data.inc

@@ -1,14 +1,14 @@
 /** Generated by interact_mng/tools/export_sunrise_semantic_surfaces.py. */
-/** Semantic policy sha256: 347419c918f27a9bee629a785d64c63181e865557a95b67fc877fe232b511edd. */
+/** Semantic policy sha256: bad2ce56b40639ece1effb5e9f3e1588748227888a364f6fda5747802f7b5e00. */
 constexpr std::string_view kSdkProjectionSchema = "sunrise-mission-semantic-surfaces-v1";
-constexpr std::string_view kRegistrySha256 = "347419c918f27a9bee629a785d64c63181e865557a95b67fc877fe232b511edd";
+constexpr std::string_view kRegistrySha256 = "bad2ce56b40639ece1effb5e9f3e1588748227888a364f6fda5747802f7b5e00";
 
 template <typename Enum> struct EnumDeclaration final {
     Enum value{};
     std::string_view name{};
 };
 
-constexpr std::array<SemanticSurface, 106> kSurfaces{{
+constexpr std::array<SemanticSurface, 107> kSurfaces{{
     SemanticSurface{"mission.action.activity-complete.v1", "activity.complete", "", SurfaceKind::action, "activity", SdkIdentityStatus::exact, kAbsentMessageId, kAbsentMessageId, "", "", "", "duplicate_of_lifetime_set", Evidence::externalVocabulary, Availability::willNotImplement, Acknowledgement::none, StateOwner::missionState, RetryPolicy::unavailable, RecoveryPolicy::unresolved, LateJoinPolicy::unavailable, true, CorrelationPolicy::requiredMissing},
     SemanticSurface{"mission.action.actor-program-run.v1", "slot.run_atoms", "", SurfaceKind::action, "activity.actor", SdkIdentityStatus::exact, kAbsentMessageId, 5U, "", "activity.type2.run-atoms.v1", "server::activity::activity_sdk_devices::run_actor_program_reserved", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::transportStaged, StateOwner::missionStateOutbox, RetryPolicy::boundedNativePreflight, RecoveryPolicy::sameBindingOutbox, LateJoinPolicy::snapshotUnresolved, true, CorrelationPolicy::scriptRequestKey},
     SemanticSurface{"mission.action.actor-retire.v1", "slot.retire_actor", "", SurfaceKind::action, "activity.actor", SdkIdentityStatus::exact, kAbsentMessageId, 5U, "", "activity.type2.retire-actor.v1", "server::activity::activity_sdk_devices::run_actor_program_reserved", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::transportStaged, StateOwner::missionStateOutbox, RetryPolicy::boundedNativePreflight, RecoveryPolicy::sameBindingOutbox, LateJoinPolicy::snapshotUnresolved, true, CorrelationPolicy::scriptRequestKey},
@@ -81,6 +81,7 @@ constexpr std::array<SemanticSurface, 106> kSurfaces{{
     SemanticSurface{"mission.event.program-start.v1", "on_start", "", SurfaceKind::event, "activity", SdkIdentityStatus::exact, kAbsentMessageId, kAbsentMessageId, "", "mission.lifecycle.on-start.v1", "server::activity::mission::mission_script_vm/on_start", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::notApplicable, StateOwner::missionState, RetryPolicy::boundedNativePreflight, RecoveryPolicy::sameBindingState, LateJoinPolicy::snapshotUnresolved, false, CorrelationPolicy::notRequired},
     SemanticSurface{"mission.event.region-changed.v1", "regionChanged", "regionChanged", SurfaceKind::event, "activity.region", SdkIdentityStatus::notRequired, 22U, kAbsentMessageId, "", "activity.msg22.region-changed.v1", "server::activity::mission::make_region_changed", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::validatedInput, StateOwner::membershipState, RetryPolicy::none, RecoveryPolicy::inputFeedSameBinding, LateJoinPolicy::notReplayed, false, CorrelationPolicy::notRequired},
     SemanticSurface{"mission.event.scene-finished.v1", "sceneFinished", "sceneFinished", SurfaceKind::event, "activity.authored-scene", SdkIdentityStatus::notRequired, 6U, kAbsentMessageId, "mission.event.scene-finished.v1", "activity.type43.scene-finished.v1", "server::activity::mission::mission_script_runtime/scene_edge_service", "", Evidence::protocolVerified, Availability::executable, Acknowledgement::validatedInput, StateOwner::activityHost, RetryPolicy::boundedNativePreflight, RecoveryPolicy::inputFeedSameBinding, LateJoinPolicy::notReplayed, false, CorrelationPolicy::notRequired},
+    SemanticSurface{"mission.event.scene-output.v1", "sceneOutput", "sceneOutput", SurfaceKind::event, "activity.authored-scene", SdkIdentityStatus::notRequired, 6U, kAbsentMessageId, "", "activity.type43.scene-output.v1", "server::activity::mission::mission_script_runtime/scene_edge_service", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::validatedInput, StateOwner::activityHost, RetryPolicy::boundedNativePreflight, RecoveryPolicy::inputFeedSameBinding, LateJoinPolicy::notReplayed, false, CorrelationPolicy::notRequired},
     SemanticSurface{"mission.event.scriptable-override-canceled.v1", "scriptableOverrideCanceled", "scriptableOverrideCanceled", SurfaceKind::event, "mission.intent", SdkIdentityStatus::notRequired, kAbsentMessageId, kAbsentMessageId, "", "mission.host-state.scriptable-override-canceled.v1", "server::activity::mission::mission_script_runtime/script_event_service", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::validatedInput, StateOwner::missionStateOutbox, RetryPolicy::boundedNativePreflight, RecoveryPolicy::inputFeedSameBinding, LateJoinPolicy::notReplayed, true, CorrelationPolicy::internalIntentSequence},
     SemanticSurface{"mission.event.scriptable-override-committed.v1", "scriptableOverrideCommitted", "scriptableOverrideCommitted", SurfaceKind::event, "mission.intent", SdkIdentityStatus::notRequired, kAbsentMessageId, kAbsentMessageId, "", "mission.host-state.scriptable-override-committed.v1", "server::activity::mission::mission_script_runtime/script_event_service", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::validatedInput, StateOwner::missionStateOutbox, RetryPolicy::boundedNativePreflight, RecoveryPolicy::inputFeedSameBinding, LateJoinPolicy::notReplayed, true, CorrelationPolicy::internalIntentSequence},
     SemanticSurface{"mission.event.scriptable-override-transport-staged.v1", "scriptableOverrideTransportStaged", "scriptableOverrideTransportStaged", SurfaceKind::event, "mission.intent", SdkIdentityStatus::notRequired, kAbsentMessageId, kAbsentMessageId, "", "mission.host-state.scriptable-override-transport-staged.v1", "server::activity::mission::mission_script_runtime/script_event_service", "", Evidence::runtimeImplemented, Availability::executable, Acknowledgement::validatedInput, StateOwner::missionStateOutbox, RetryPolicy::boundedNativePreflight, RecoveryPolicy::inputFeedSameBinding, LateJoinPolicy::notReplayed, true, CorrelationPolicy::internalIntentSequence},

+ 8 - 28
Sunrise/src/client/content/activity/activity_sdk_squad_config_parse.cpp

@@ -43,20 +43,8 @@ namespace tables = middleware::content::packages::tables;
 
 /** Compares two parsed candidate lanes exactly. */
 [[nodiscard]] bool same_member(const MemberFact& left, const MemberFact& right) noexcept {
-    if (left.memberKey != right.memberKey || left.reservedU32 != right.reservedU32) {
-        return false;
-    }
-    for (std::size_t lane = 0; lane < left.candidates.size(); ++lane) {
-        if (left.candidates[lane].size() != right.candidates[lane].size()) {
-            return false;
-        }
-        for (std::size_t row = 0; row < left.candidates[lane].size(); ++row) {
-            if (left.candidates[lane][row] != right.candidates[lane][row]) {
-                return false;
-            }
-        }
-    }
-    return true;
+    return left.memberKey == right.memberKey && left.reservedU32 == right.reservedU32
+           && left.candidates == right.candidates;
 }
 
 } // namespace
@@ -88,20 +76,12 @@ namespace tables = middleware::content::packages::tables;
 
 /** Compares repeated spawn-rule definitions from different placed paths. */
 [[nodiscard]] bool same_rule(const RuleFact& left, const RuleFact& right) noexcept {
-    if (left.configTag != right.configTag || left.complete != right.complete
-        || left.primaryComponentOffset != right.primaryComponentOffset
-        || left.secondaryComponentOffset != right.secondaryComponentOffset
-        || left.primaryComponentClass != right.primaryComponentClass
-        || left.secondaryComponentClass != right.secondaryComponentClass
-        || left.inlineForm != right.inlineForm || left.points.size() != right.points.size()) {
-        return false;
-    }
-    for (std::size_t index = 0; index < left.points.size(); ++index) {
-        if (left.points[index] != right.points[index]) {
-            return false;
-        }
-    }
-    return true;
+    return left.configTag == right.configTag && left.complete == right.complete
+           && left.primaryComponentOffset == right.primaryComponentOffset
+           && left.secondaryComponentOffset == right.secondaryComponentOffset
+           && left.primaryComponentClass == right.primaryComponentClass
+           && left.secondaryComponentClass == right.secondaryComponentClass
+           && left.inlineForm == right.inlineForm && left.points == right.points;
 }
 
 /** Parses one exact authored type-1 config. */

+ 1 - 11
Sunrise/src/client/content/activity/activity_sdk_squad_graph.cpp

@@ -16,17 +16,7 @@ namespace {
 
 namespace topology = topology_inventory;
 
-/** Formats one bounded canonical identity. */
-template <typename... Values>
-[[nodiscard]] bool format_text(std::string& output, const char* pattern, Values... values) {
-    std::array<char, 192> bytes{};
-    const int count = std::snprintf(bytes.data(), bytes.size(), pattern, values...);
-    if (count < 0 || static_cast<std::size_t>(count) >= bytes.size()) {
-        return false;
-    }
-    output.assign(bytes.data(), static_cast<std::size_t>(count));
-    return true;
-}
+using detail::format_text;
 
 /** Inserts one unique config tag and rejects duplicate normalized definitions. */
 template <typename Row>

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

@@ -13,6 +13,7 @@
 #include "../../../middleware/content/packages/tables/authored_squad_reader.h"
 #include "../../../middleware/crypto/sha256.h"
 #include "activity_sdk_squad_graph_internal.h"
+#include "activity_sdk_squad_inventory_internal.h"
 
 namespace sunrise::client::content::activity::sdk_generation::squad_inventory::detail {
 namespace {
@@ -80,57 +81,6 @@ struct PendingEdge final {
     bool associationExact{};
 };
 
-/** @return True when one topology text owns exactly its declared bytes. */
-[[nodiscard]] bool text_view(const topology::Text& text, std::string_view& output) noexcept {
-    output = {};
-    if (text.length >= text.value.size()) {
-        return false;
-    }
-    output = std::string_view(text.value.data(), text.length);
-    return text.value[text.length] == '\0';
-}
-
-template <typename... Values>
-[[nodiscard]] bool format_text(std::string& output, const char* pattern, Values... values) {
-    std::array<char, 192> bytes{};
-    const int count = std::snprintf(bytes.data(), bytes.size(), pattern, values...);
-    if (count < 0 || static_cast<std::size_t>(count) >= bytes.size()) {
-        return false;
-    }
-    output.assign(bytes.data(), static_cast<std::size_t>(count));
-    return true;
-}
-
-/**
- * Appends one unsigned value in little-endian order.
- * @tparam Value Unsigned type whose whole width is appended.
- * @return False when the buffer cannot grow.
- */
-template <typename Value>
-[[nodiscard]] bool append_little(std::vector<std::byte>& output, Value value) {
-    static_assert(std::is_unsigned_v<Value>);
-    if (output.size() > (std::numeric_limits<std::size_t>::max)() - sizeof(Value)) {
-        return false;
-    }
-    for (std::size_t index = 0; index < sizeof(Value); ++index) {
-        output.push_back(static_cast<std::byte>((value >> (index * 8U)) & Value{0xFFU}));
-    }
-    return true;
-}
-
-/** Appends one length-prefixed hash input part. @return False when the buffer cannot grow. */
-[[nodiscard]] bool append_hash_part(std::vector<std::byte>& output, std::string_view value) {
-    if (!append_little(output, static_cast<std::uint64_t>(value.size()))
-        || value.size() > (std::numeric_limits<std::size_t>::max)() - output.size()) {
-        return false;
-    }
-    output.reserve(output.size() + value.size());
-    for (const unsigned char byte : value) {
-        output.push_back(static_cast<std::byte>(byte));
-    }
-    return true;
-}
-
 /** Hashes one domain and its parts to lowercase hex. @return False when hashing fails. */
 [[nodiscard]] bool
 domain_hash(std::string_view domain, std::span<const std::string_view> parts, std::string& output) {
@@ -228,10 +178,7 @@ template <typename Index> void canonicalize_scenarios(Index& index) {
     }
 }
 
-/**
- * Indexes the exact rule contexts once. The former edge loop rescanned every config context and,
- * for every candidate, every rule point and point context.
- */
+/** Indexes once, by rule and object, the scenarios where every rule point matches exactly. */
 [[nodiscard]] bool build_exact_rule_scenarios(
     const GraphSnapshot& graph,
     std::unordered_map<RuleObjectKey, std::vector<std::uint32_t>, RuleObjectKeyHash>& output) {

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

@@ -11,6 +11,7 @@ namespace sunrise::client::content::activity::sdk_generation::squad_inventory {
  * Appends only scenario-owned authored-squad graph contexts to one otherwise-built shard.
  * Estate-global definitions deliberately remain outside per-scenario shards.
  */
+// TODO: no generation path calls this yet, so every shard publishes empty squad graph contexts.
 [[nodiscard]] bool
 append_scenario_graph_contexts(std::uint32_t scenarioIndex,
                                const GraphSnapshot& graph,

+ 3 - 64
Sunrise/src/client/content/activity/activity_sdk_squad_inventory.cpp

@@ -99,27 +99,9 @@ read_value(std::span<const std::byte> blob, std::size_t offset, Value& output) n
     return true;
 }
 
-/** @return True when one topology text owns exactly its declared bytes. */
-[[nodiscard]] bool text_view(const topology::Text& text, std::string_view& output) noexcept {
-    output = {};
-    if (text.length >= text.value.size()) {
-        return false;
-    }
-    output = std::string_view(text.value.data(), text.length);
-    return text.value[text.length] == '\0';
-}
-
-/** Formats one bounded identity string. */
-template <typename... Values>
-[[nodiscard]] bool format_text(std::string& output, const char* pattern, Values... values) {
-    std::array<char, 192> bytes{};
-    const int count = std::snprintf(bytes.data(), bytes.size(), pattern, values...);
-    if (count < 0 || static_cast<std::size_t>(count) >= bytes.size()) {
-        return false;
-    }
-    output.assign(bytes.data(), static_cast<std::size_t>(count));
-    return true;
-}
+using detail::descriptor_id;
+using detail::format_text;
+using detail::text_view;
 
 /** @return True when a float is finite and its raw lane is retained. */
 [[nodiscard]] bool position_bits(const std::array<float, 3>& position,
@@ -140,28 +122,6 @@ template <typename... Values>
            || (declared >= 0 && static_cast<std::uint32_t>(declared) == entryIndex);
 }
 
-/** Derives one descriptor identity from its exact object and slot tuple. */
-[[nodiscard]] bool descriptor_id(const topology::Snapshot& topology,
-                                 const DescriptorFact& descriptor,
-                                 std::string& output) {
-    if (descriptor.objectIndex >= topology.objects.size()
-        || descriptor.slotIndex >= topology.slots.size()) {
-        return false;
-    }
-    const topology::Object& object = topology.objects[descriptor.objectIndex];
-    const topology::Slot& slot = topology.slots[descriptor.slotIndex];
-    if (slot.objectIndex != descriptor.objectIndex) {
-        return false;
-    }
-    return format_text(output,
-                       "descriptor/%08x/%08x/%08x/%04x/%04x",
-                       static_cast<unsigned>(descriptor.configTag),
-                       static_cast<unsigned>(object.objectTag),
-                       static_cast<unsigned>(descriptor.descriptorOffset),
-                       static_cast<unsigned>(slot.slotIndex),
-                       static_cast<unsigned>(slot.slotType));
-}
-
 /** Inserts one exact parsed spawner or rejects a repeated mismatch. */
 [[nodiscard]] bool merge_spawner(Collector& collector, SpawnerFact value) {
     const auto found = collector.spawners.find(value.configTag);
@@ -703,25 +663,4 @@ bool append_external_placements(const topology::Snapshot& topology,
     return true;
 }
 
-/** Runs package extraction and linking without retaining an intermediate copy. */
-bool build(const topology::Snapshot& topology,
-           TagReader reader,
-           void* readerContext,
-           std::span<const SlotSchemaFact> slotSchemas,
-           ActorResolver actorResolver,
-           void* actorContext,
-           Snapshot& output) noexcept {
-    output = {};
-    Facts facts{};
-    if (!collect_facts(topology, reader, readerContext, facts)) {
-        return false;
-    }
-    try {
-        facts.slotSchemas.assign(slotSchemas.begin(), slotSchemas.end());
-    } catch (...) {
-        return false;
-    }
-    return link(topology, facts, actorResolver, actorContext, output);
-}
-
 } // namespace sunrise::client::content::activity::sdk_generation::squad_inventory

+ 0 - 9
Sunrise/src/client/content/activity/activity_sdk_squad_inventory.h

@@ -279,13 +279,4 @@ struct GraphSnapshot;
                         void* actorContext,
                         Snapshot& output) noexcept;
 
-/** Runs package extraction and linking without retaining an intermediate copy. */
-[[nodiscard]] bool build(const topology_inventory::Snapshot& topology,
-                         TagReader reader,
-                         void* readerContext,
-                         std::span<const SlotSchemaFact> slotSchemas,
-                         ActorResolver actorResolver,
-                         void* actorContext,
-                         Snapshot& output) noexcept;
-
 } // namespace sunrise::client::content::activity::sdk_generation::squad_inventory

+ 68 - 0
Sunrise/src/client/content/activity/activity_sdk_squad_inventory_internal.h

@@ -1,9 +1,77 @@
 #pragma once
 
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <limits>
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <vector>
+
 #include "activity_sdk_squad_inventory.h"
 
 namespace sunrise::client::content::activity::sdk_generation::squad_inventory::detail {
 
+/** @return True when one topology text owns exactly its declared bytes. */
+[[nodiscard]] inline bool text_view(const topology_inventory::Text& text,
+                                    std::string_view& output) noexcept {
+    output = {};
+    if (text.length >= text.value.size()) {
+        return false;
+    }
+    output = std::string_view(text.value.data(), text.length);
+    return text.value[text.length] == '\0';
+}
+
+/** Formats one bounded identity string. */
+template <typename... Values>
+[[nodiscard]] bool format_text(std::string& output, const char* pattern, Values... values) {
+    std::array<char, 192> bytes{};
+    const int count = std::snprintf(bytes.data(), bytes.size(), pattern, values...);
+    if (count < 0 || static_cast<std::size_t>(count) >= bytes.size()) {
+        return false;
+    }
+    output.assign(bytes.data(), static_cast<std::size_t>(count));
+    return true;
+}
+
+/**
+ * Appends one unsigned value in little-endian order.
+ * @tparam Value Unsigned type whose whole width is appended.
+ * @return False when the buffer cannot grow.
+ */
+template <typename Value>
+[[nodiscard]] bool append_little(std::vector<std::byte>& output, Value value) {
+    static_assert(std::is_unsigned_v<Value>);
+    if (output.size() > (std::numeric_limits<std::size_t>::max)() - sizeof(Value)) {
+        return false;
+    }
+    for (std::size_t index = 0; index < sizeof(Value); ++index) {
+        output.push_back(static_cast<std::byte>((value >> (index * 8U)) & Value{0xFFU}));
+    }
+    return true;
+}
+
+/** Appends one length-prefixed hash input part. @return False when the buffer cannot grow. */
+[[nodiscard]] inline bool append_hash_part(std::vector<std::byte>& output, std::string_view value) {
+    if (!append_little(output, static_cast<std::uint64_t>(value.size()))
+        || value.size() > (std::numeric_limits<std::size_t>::max)() - output.size()) {
+        return false;
+    }
+    output.reserve(output.size() + value.size());
+    for (const unsigned char byte : value) {
+        output.push_back(static_cast<std::byte>(byte));
+    }
+    return true;
+}
+
+/** Derives one descriptor identity from its exact object and slot tuple. */
+[[nodiscard]] bool descriptor_id(const topology_inventory::Snapshot& topology,
+                                 const DescriptorFact& descriptor,
+                                 std::string& output);
+
 /** Checks the topology rows consumed by extraction and linking. */
 [[nodiscard]] bool valid_topology(const topology_inventory::Snapshot& source) noexcept;
 

+ 5 - 48
Sunrise/src/client/content/activity/activity_sdk_squad_inventory_link.cpp

@@ -55,7 +55,7 @@ public:
         return algorithm_ != nullptr && hash_ != nullptr;
     }
 
-    /** Adds one span to the running hash. @return False when the hash is not ready or too long. */
+    /** Hashes one whole span. @return False when the hash is not ready or the span is too long. */
     [[nodiscard]] bool hash(std::span<const std::byte> input, crypto::Digest& output) noexcept {
         return ready() && input.size() <= (std::numeric_limits<ULONG>::max)()
                && BCryptHashData(hash_,
@@ -93,53 +93,10 @@ struct PendingSquad final {
     std::vector<SquadAnchor> anchors{};
 };
 
-/** @return True when one topology text owns exactly its declared bytes. */
-[[nodiscard]] bool text_view(const topology::Text& text, std::string_view& output) noexcept {
-    output = {};
-    if (text.length >= text.value.size()) {
-        return false;
-    }
-    output = std::string_view(text.value.data(), text.length);
-    return text.value[text.length] == '\0';
-}
-
-/** Formats one bounded identity string. */
-template <typename... Values>
-[[nodiscard]] bool format_text(std::string& output, const char* pattern, Values... values) {
-    std::array<char, 192> bytes{};
-    const int count = std::snprintf(bytes.data(), bytes.size(), pattern, values...);
-    if (count < 0 || static_cast<std::size_t>(count) >= bytes.size()) {
-        return false;
-    }
-    output.assign(bytes.data(), static_cast<std::size_t>(count));
-    return true;
-}
-
-/** Appends one unsigned integer in little-endian order. */
-template <typename Value>
-[[nodiscard]] bool append_little(std::vector<std::byte>& output, Value value) {
-    static_assert(std::is_unsigned_v<Value>);
-    if (output.size() > (std::numeric_limits<std::size_t>::max)() - sizeof(Value)) {
-        return false;
-    }
-    for (std::size_t index = 0; index < sizeof(Value); ++index) {
-        output.push_back(static_cast<std::byte>((value >> (index * 8U)) & Value{0xFFU}));
-    }
-    return true;
-}
-
-/** Appends one length-prefixed byte string to a domain hash input. */
-[[nodiscard]] bool append_hash_part(std::vector<std::byte>& output, std::string_view value) {
-    if (!append_little(output, static_cast<std::uint64_t>(value.size()))
-        || value.size() > (std::numeric_limits<std::size_t>::max)() - output.size()) {
-        return false;
-    }
-    output.reserve(output.size() + value.size());
-    for (const unsigned char byte : value) {
-        output.push_back(static_cast<std::byte>(byte));
-    }
-    return true;
-}
+using detail::append_hash_part;
+using detail::append_little;
+using detail::format_text;
+using detail::text_view;
 
 /** Replays the SDK builder's typed SHA-256 domain hash. */
 [[nodiscard]] bool domain_hash(SquadIdHasher& hasher,

+ 11 - 27
Sunrise/src/client/content/activity/activity_sdk_squad_inventory_link_support.cpp

@@ -19,32 +19,18 @@ namespace {
 namespace tables = middleware::content::packages::tables;
 namespace topology = topology_inventory;
 
-/** @return True when one topology text owns exactly its declared bytes. */
-[[nodiscard]] bool text_view(const topology::Text& text, std::string_view& output) noexcept {
-    output = {};
-    if (text.length >= text.value.size()) {
-        return false;
-    }
-    output = std::string_view(text.value.data(), text.length);
-    return text.value[text.length] == '\0';
-}
-
-/** Formats one bounded identity string. */
-template <typename... Values>
-[[nodiscard]] bool format_text(std::string& output, const char* pattern, Values... values) {
-    std::array<char, 192> bytes{};
-    const int count = std::snprintf(bytes.data(), bytes.size(), pattern, values...);
-    if (count < 0 || static_cast<std::size_t>(count) >= bytes.size()) {
-        return false;
-    }
-    output.assign(bytes.data(), static_cast<std::size_t>(count));
-    return true;
-}
+} // namespace
 
-/** Derives one descriptor identity from its exact object and slot tuple. */
-[[nodiscard]] bool descriptor_id(const topology::Snapshot& topology,
-                                 const DescriptorFact& descriptor,
-                                 std::string& output) {
+/**
+ * Derives one descriptor identity from its exact object and slot tuple.
+ * @param topology Topology the descriptor indexes.
+ * @param descriptor Descriptor to name.
+ * @param output Receives the identity.
+ * @return False when the descriptor does not name a matching object and slot.
+ */
+bool descriptor_id(const topology::Snapshot& topology,
+                   const DescriptorFact& descriptor,
+                   std::string& output) {
     if (descriptor.objectIndex >= topology.objects.size()
         || descriptor.slotIndex >= topology.slots.size()) {
         return false;
@@ -63,8 +49,6 @@ template <typename... Values>
                        static_cast<unsigned>(slot.slotType));
 }
 
-} // namespace
-
 /** Checks the topology rows needed by the squad inventory. */
 [[nodiscard]] bool valid_topology(const topology::Snapshot& source) noexcept {
     if (!source.ready || source.objects.size() >= format::kAbsentIndex

+ 1 - 9
Sunrise/src/client/content/activity/activity_sdk_topology_inventory.cpp

@@ -13,6 +13,7 @@
 namespace sunrise::client::content::activity::sdk_generation::topology_inventory {
 using detail::build_object;
 using detail::build_occurrence;
+using detail::content_hash;
 using detail::format_text;
 using detail::kActivityTopologyJoinMask;
 using detail::merge_object;
@@ -121,15 +122,6 @@ struct ObjectUpdate final {
     return true;
 }
 
-/** Returns the compact FNV-1 hash used by package-inline names. */
-[[nodiscard]] std::uint32_t content_hash(std::string_view value) noexcept {
-    std::uint32_t hash = 2166136261U;
-    for (const unsigned char byte : value) {
-        hash = (hash * 16777619U) ^ byte;
-    }
-    return hash;
-}
-
 /** Finds one scenario index in the inventory's strict tag order. */
 [[nodiscard]] std::uint32_t scenario_index(const Snapshot& output, std::uint32_t tag) noexcept {
     const auto found = std::lower_bound(

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_topology_inventory.h

@@ -14,7 +14,7 @@
 
 namespace sunrise::client::content::activity::sdk_generation::topology_inventory {
 
-/** A partial source walk leaves every format-v12 object count pending. */
+/** A partial source walk leaves every object count pending. */
 inline constexpr std::uint32_t kObjectConfigCountPending = 0x01U;
 inline constexpr std::uint32_t kObjectPlacedSubblockCountPending = 0x02U;
 inline constexpr std::uint32_t kObjectPlacedLeafCountPending = 0x04U;

+ 1 - 9
Sunrise/src/client/content/activity/activity_sdk_topology_inventory_finish.cpp

@@ -63,6 +63,7 @@ bool valid_open_prefix(const Snapshot& output) noexcept {
 namespace {
 
 using detail::byte_less;
+using detail::content_hash;
 using detail::kActivityTopologyJoinMask;
 using detail::kActivityTopologyReadyMask;
 using detail::text_view;
@@ -76,15 +77,6 @@ struct InlineValue final {
     const std::string* value{};
 };
 
-/** Returns the compact FNV-1 hash used by package-inline names. */
-[[nodiscard]] std::uint32_t content_hash(std::string_view value) noexcept {
-    std::uint32_t hash = 2166136261U;
-    for (const unsigned char byte : value) {
-        hash = (hash * 16777619U) ^ byte;
-    }
-    return hash;
-}
-
 /** Builds the sorted unique inline bank once after every scenario was read. */
 [[nodiscard]] bool build_inline_bank(const Snapshot& source,
                                      std::vector<InlineName>& rows,

+ 9 - 0
Sunrise/src/client/content/activity/activity_sdk_topology_inventory_internal.h

@@ -37,6 +37,15 @@ template <typename... Arguments>
     return true;
 }
 
+/** Returns the compact FNV-1 hash used by package-inline names. */
+[[nodiscard]] inline std::uint32_t content_hash(std::string_view value) noexcept {
+    std::uint32_t hash = 2166136261U;
+    for (const unsigned char byte : value) {
+        hash = (hash * 16777619U) ^ byte;
+    }
+    return hash;
+}
+
 [[nodiscard]] bool text_view(const Text& source, std::string_view& output) noexcept;
 [[nodiscard]] bool byte_less(std::string_view left, std::string_view right) noexcept;
 [[nodiscard]] bool valid_utf8(std::string_view value) noexcept;

+ 3 - 2
Sunrise/src/client/content/activity/activity_sdk_tree_publication.cpp

@@ -362,8 +362,9 @@ Status publish(const wchar_t* stage,
         }
         return Status::rollbackFailure;
     }
-    if ((!hadOutput || remove_tree(backup)) && DeleteFileW(committed.c_str()) != FALSE) {
-        return Status::ready;
+    // The commit is done. A leftover backup or marker is finished by `recover` next time.
+    if (!hadOutput || remove_tree(backup)) {
+        (void)DeleteFileW(committed.c_str());
     }
     return Status::ready;
 }

+ 1 - 1
Sunrise/src/client/content/activity/entity_object_type_build.cpp

@@ -52,7 +52,7 @@ bool build(const reader::Source& source,
     try {
         Context context{source, scratch, {}};
         reader::ScanResult result{};
-        if (!reader::scan_class(source.directory, kRsatClass, &collect, &context, result)) {
+        if (!reader::scan_class(source, kRsatClass, &collect, &context, result)) {
             return false;
         }
         std::sort(context.rows.begin(), context.rows.end(), [](const auto& a, const auto& b) {

+ 3 - 9
Sunrise/src/client/content/activity/entity_position_profile_build.cpp

@@ -144,13 +144,6 @@ read_at(HANDLE file, std::uint64_t offset, std::span<std::byte> output) noexcept
     }
     return true;
 }
-/** The manifest identity includes the installed package builds. */
-bool fingerprint(void* opaque, const state::content_manifest::View& view) noexcept {
-    std::copy(view.buildFingerprint.begin(),
-              view.buildFingerprint.end(),
-              static_cast<profiles::Fingerprint*>(opaque)->begin());
-    return true;
-}
 /** Same-patch name conflicts cannot select an arbitrary package. */
 bool collect_name(void* opaque, const named::Entry& entry) noexcept {
     try {
@@ -370,7 +363,7 @@ bool read(void* opaque, std::uint32_t tag, std::uint32_t expected, Blob& bytes)
 } // namespace
 bool ready() noexcept {
     profiles::Fingerprint identity{};
-    if (!state::content_manifest::visit_snapshot(&fingerprint, &identity)) {
+    if (!state::content_manifest::build_fingerprint(identity)) {
         return false;
     }
     const bool positions = profiles::confirm(identity);
@@ -381,7 +374,8 @@ bool ready() noexcept {
 bool build(const reader::Source& source, reader::Scratch& scratch) noexcept {
     try {
         profiles::Fingerprint identity{};
-        if (!state::content_manifest::visit_snapshot(&fingerprint, &identity)) {
+        // The catalog identity includes the installed package builds.
+        if (!state::content_manifest::build_fingerprint(identity)) {
             return false;
         }
         const bool positions = profiles::confirm(identity);

+ 21 - 1
Sunrise/src/client/content/activity/scriptable_catalog_builder.h

@@ -3,15 +3,30 @@
 #include <cstdint>
 #include <memory>
 #include <string_view>
+#include <vector>
 
 #include "../../../middleware/content/packages/reader/reader.h"
+#include "../../../state/build_data/hash_names/definition.h"
 #include "../../../state/build_data/scriptables/definition.h"
+#include "../../../state/content/content_catalog.h"
 #include "scriptable_catalog_container_index.h"
 
+namespace sunrise::client::content::scenarios {
+class LayoutSet;
+} // namespace sunrise::client::content::scenarios
+
 namespace sunrise::client::content::activity::scriptables::internal {
 
 using BuilderCancelCheck = bool (*)() noexcept;
 
+/** Package-wide tables one scenario build reads. A null member reads the table State publishes. */
+struct BuildTables final {
+    const ::sunrise::client::content::scenarios::LayoutSet* layouts{};
+    const std::vector<state::content::Definition>* definitions{};
+    /** Identifier names, sorted by hash. */
+    const std::vector<state::build_data::hash_names::Name>* hashNames{};
+};
+
 /**
  * Immutable source analyses shared by scenario builds from one installed package estate.
  * Use one instance for one sequential pass. Destroy it before changing the source estate.
@@ -35,6 +50,7 @@ private:
                            const ContainerIndex& containers,
                            middleware::content::packages::reader::Scratch& scratch,
                            ScenarioAnalysisCache& analyses,
+                           const BuildTables& tables,
                            std::uint32_t scenarioTag,
                            std::string_view scenarioName,
                            BuilderCancelCheck cancel);
@@ -53,12 +69,16 @@ build_scenario_catalog(const middleware::content::packages::reader::Source& sour
                        std::string_view scenarioName,
                        BuilderCancelCheck cancel);
 
-/** Builds one scenario while reusing source-only object analysis from earlier pass rows. */
+/**
+ * Builds one scenario while reusing source-only object analysis from earlier pass rows.
+ * @param tables Layouts and names the build reads in place of the tables State publishes.
+ */
 [[nodiscard]] std::shared_ptr<state::build_data::scriptables::Snapshot>
 build_scenario_catalog(const middleware::content::packages::reader::Source& source,
                        const ContainerIndex& containers,
                        middleware::content::packages::reader::Scratch& scratch,
                        ScenarioAnalysisCache& analyses,
+                       const BuildTables& tables,
                        std::uint32_t scenarioTag,
                        std::string_view scenarioName,
                        BuilderCancelCheck cancel);

+ 8 - 1
Sunrise/src/client/content/activity/scriptable_catalog_container_index.cpp

@@ -67,7 +67,7 @@ bool build_container_index(const package_reader::Source& source,
     SweepContext context{&output, cancel, false};
     package_reader::ScanResult scan{};
     const bool scanned = package_reader::scan_class_entries(
-        source.directory, tables::kContainerClass, &collect_entry, &context, scan);
+        source, tables::kContainerClass, &collect_entry, &context, scan);
     if (context.failed || cancelled(cancel)) {
         return false;
     }
@@ -75,4 +75,11 @@ bool build_container_index(const package_reader::Source& source,
     return true;
 }
 
+/** Authoritative generation refuses partial scans and unbound package stems. */
+bool build_complete_container_index(const package_reader::Source& source,
+                                    ContainerIndex& output,
+                                    ContainerIndexCancelCheck cancel) noexcept {
+    return build_container_index(source, output, cancel) && output.complete && output.stemsComplete;
+}
+
 } // namespace sunrise::client::content::activity::scriptables::internal

+ 7 - 1
Sunrise/src/client/content/activity/scriptable_catalog_container_index.h

@@ -37,11 +37,17 @@ struct ContainerIndex final {
  * @param source Package directory and borrowed block keys.
  * @param output Receives the entries and the two completeness flags.
  * @param cancel Optional cancellation probe.
- * @return True when the sweep finished, cancelled or not.
+ * @return False on cancellation or retention failure; partial scans keep explicit flags.
  */
 [[nodiscard]] bool
 build_container_index(const middleware::content::packages::reader::Source& source,
                       ContainerIndex& output,
                       ContainerIndexCancelCheck cancel) noexcept;
 
+/** Authoritative generation refuses partial scans and unbound package stems. */
+[[nodiscard]] bool
+build_complete_container_index(const middleware::content::packages::reader::Source& source,
+                               ContainerIndex& output,
+                               ContainerIndexCancelCheck cancel) noexcept;
+
 } // namespace sunrise::client::content::activity::scriptables::internal

+ 0 - 41
Sunrise/src/client/content/activity/scriptable_catalog_container_placement_cache.cpp

@@ -80,47 +80,6 @@ void copy_config_row(const catalog::ContainerPlacementConfig& source,
     }
 }
 
-/** Replays one exact evidence slice or leaves both destination banks unchanged. */
-[[nodiscard]] CacheReplay
-replay_evidence(BuildContext& context,
-                const ContainerPlacementCacheAccess::Evidence& cached) noexcept {
-    if (cancelled(context)) {
-        return CacheReplay::cancelled;
-    }
-    catalog::Snapshot& output = *context.output;
-    // Snapshot rows are published as u32, so a bank stops at that maximum.
-    constexpr std::size_t maximum = (std::numeric_limits<std::uint32_t>::max)();
-    const std::size_t firstInlineName = output.inlineNameCandidates.size();
-    const std::size_t firstInlineByte = output.inlineNameBytes.size();
-    if (firstInlineName > maximum || cached.rows.size() > maximum - firstInlineName
-        || firstInlineByte > maximum || cached.bytes.size() > maximum - firstInlineByte) {
-        return CacheReplay::unavailable;
-    }
-    try {
-        output.inlineNameCandidates.reserve(firstInlineName + cached.rows.size());
-        output.inlineNameBytes.reserve(firstInlineByte + cached.bytes.size());
-    } catch (...) {
-        return CacheReplay::unavailable;
-    }
-    output.inlineNameBytes.insert(
-        output.inlineNameBytes.end(), cached.bytes.begin(), cached.bytes.end());
-    for (catalog::InlineNameCandidate row : cached.rows) {
-        if (cancelled(context)) {
-            output.inlineNameCandidates.resize(firstInlineName);
-            output.inlineNameBytes.resize(firstInlineByte);
-            return CacheReplay::cancelled;
-        }
-        row.firstByte += static_cast<std::uint32_t>(firstInlineByte);
-        output.inlineNameCandidates.push_back(row);
-    }
-    if (cancelled(context)) {
-        output.inlineNameCandidates.resize(firstInlineName);
-        output.inlineNameBytes.resize(firstInlineByte);
-        return CacheReplay::cancelled;
-    }
-    return CacheReplay::complete;
-}
-
 /** @return True when a cached complete graph fits every remaining scenario bank. */
 [[nodiscard]] bool graph_fits(const catalog::Snapshot& output,
                               const ContainerPlacementCacheAccess::Value& cached) noexcept {

+ 9 - 3
Sunrise/src/client/content/activity/scriptable_catalog_container_placements.cpp

@@ -19,6 +19,7 @@
 #include "../../../middleware/content/packages/tables/component_container_reader.h"
 #include "../../../middleware/content/packages/tables/container_placement_reader.h"
 #include "../../../state/build_data/runtime.h"
+#include "../scenarios/scenario_build.h"
 #include "../spawn_sets/spawn_set_catalog_builder.h"
 #include "scriptable_catalog_container_placements_internal.h"
 #include "scriptable_catalog_inline_names.h"
@@ -31,6 +32,7 @@ using namespace container_placements;
 
 namespace catalog = state::build_data::scriptables;
 namespace package_reader = middleware::content::packages::reader;
+namespace scenario_layouts = ::sunrise::client::content::scenarios;
 namespace tables = middleware::content::packages::tables;
 
 /** Reads one reached tag and keeps all valid inline-name evidence in its blob. */
@@ -55,6 +57,7 @@ void mark_unresolved(BuildContext& context) noexcept {
 }
 
 /** Records a lossless raw edge whose target meaning remains unresolved. */
+// TODO: no raw-edge path calls this yet, so semanticUnresolved always publishes zero.
 void mark_semantic_unresolved(BuildContext& context) noexcept {
     ++context.output->containerPlacementDiagnostics.semanticUnresolved;
 }
@@ -453,12 +456,13 @@ void append_owner(BuildContext& context,
                                                     package_reader::Scratch& scratch,
                                                     ContainerPlacementCache* cache,
                                                     const ContainerIndex& containers,
+                                                    const scenario_layouts::LayoutSet* layouts,
                                                     std::string_view scenarioName,
                                                     catalog::Snapshot& output,
                                                     ContainerPlacementCancelCheck cancel) noexcept {
     output.containerPlacementDiagnostics = {};
     state::build_data::scenarios::Definition scenario{};
-    if (!state::build_data::find_scenario_layout(scenarioName, scenario)) {
+    if (!scenario_layouts::find_layout(layouts, scenarioName, scenario)) {
         output.containerPlacementDiagnostics.unresolvedReads = 1;
         return true;
     }
@@ -528,11 +532,12 @@ ContainerPlacementCache::~ContainerPlacementCache() = default;
 bool append_container_placements(const package_reader::Source& source,
                                  package_reader::Scratch& scratch,
                                  const ContainerIndex& containers,
+                                 const scenario_layouts::LayoutSet* layouts,
                                  std::string_view scenarioName,
                                  catalog::Snapshot& output,
                                  ContainerPlacementCancelCheck cancel) noexcept {
     return append_container_placements_impl(
-        source, scratch, nullptr, containers, scenarioName, output, cancel);
+        source, scratch, nullptr, containers, layouts, scenarioName, output, cancel);
 }
 
 /** Appends container placements with exact pass-local object-list graph reuse. */
@@ -540,11 +545,12 @@ bool append_container_placements(const package_reader::Source& source,
                                  package_reader::Scratch& scratch,
                                  ContainerPlacementCache& cache,
                                  const ContainerIndex& containers,
+                                 const scenario_layouts::LayoutSet* layouts,
                                  std::string_view scenarioName,
                                  catalog::Snapshot& output,
                                  ContainerPlacementCancelCheck cancel) noexcept {
     return append_container_placements_impl(
-        source, scratch, &cache, containers, scenarioName, output, cancel);
+        source, scratch, &cache, containers, layouts, scenarioName, output, cancel);
 }
 
 } // namespace sunrise::client::content::activity::scriptables::internal

+ 14 - 2
Sunrise/src/client/content/activity/scriptable_catalog_container_placements.h

@@ -7,6 +7,10 @@
 #include "../../../state/build_data/scriptables/definition.h"
 #include "scriptable_catalog_container_index.h"
 
+namespace sunrise::client::content::scenarios {
+class LayoutSet;
+} // namespace sunrise::client::content::scenarios
+
 namespace sunrise::client::content::activity::scriptables::internal {
 
 using ContainerPlacementCancelCheck = bool (*)() noexcept;
@@ -29,21 +33,29 @@ private:
     friend struct ContainerPlacementCacheAccess;
 };
 
-/** Appends the selected-stem container placement graph without assigning ClientRef identity. */
+/**
+ * Appends the selected-stem container placement graph without assigning ClientRef identity.
+ * @param layouts Destination layouts to read the stem from, or null for the ones State publishes.
+ */
 [[nodiscard]] bool
 append_container_placements(const middleware::content::packages::reader::Source& source,
                             middleware::content::packages::reader::Scratch& scratch,
                             const ContainerIndex& containers,
+                            const ::sunrise::client::content::scenarios::LayoutSet* layouts,
                             std::string_view scenarioName,
                             state::build_data::scriptables::Snapshot& output,
                             ContainerPlacementCancelCheck cancel) noexcept;
 
-/** Appends container placements with exact pass-local object-list graph reuse. */
+/**
+ * Appends container placements with exact pass-local object-list graph reuse.
+ * @param layouts Destination layouts to read the stem from, or null for the ones State publishes.
+ */
 [[nodiscard]] bool
 append_container_placements(const middleware::content::packages::reader::Source& source,
                             middleware::content::packages::reader::Scratch& scratch,
                             ContainerPlacementCache& cache,
                             const ContainerIndex& containers,
+                            const ::sunrise::client::content::scenarios::LayoutSet* layouts,
                             std::string_view scenarioName,
                             state::build_data::scriptables::Snapshot& output,
                             ContainerPlacementCancelCheck cancel) noexcept;

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

@@ -131,7 +131,6 @@ struct BuildContext final {
     std::vector<std::byte> listBytes{};
     std::vector<std::byte> classBytes{};
     std::vector<std::byte> configBytes{};
-    std::vector<std::byte> resourceBytes{};
     bool failed{};
 };
 

+ 49 - 21
Sunrise/src/client/content/activity/scriptable_catalog_names.cpp

@@ -3,6 +3,7 @@
 #include <algorithm>
 #include <cstddef>
 #include <cstdint>
+#include <span>
 #include <string_view>
 #include <utility>
 #include <vector>
@@ -14,12 +15,14 @@
 #include "../../../middleware/content/packages/tables/slot_descriptor_reader.h"
 #include "../../../middleware/content/packages/tables/static_spatial_candidate_reader.h"
 #include "../../../state/build_data/runtime.h"
+#include "../../../state/build_data/scriptables/inline_name_evidence.h"
 #include "../../../state/content/content_catalog.h"
 
 namespace sunrise::client::content::activity::scriptables::internal {
 namespace {
 
 namespace catalog = state::build_data::scriptables;
+namespace names_state = state::build_data::hash_names;
 namespace tables = middleware::content::packages::tables;
 
 struct HashDefinition final {
@@ -27,19 +30,37 @@ struct HashDefinition final {
     std::uint32_t definitionRow{};
 };
 
-/** @return The compact FNV-1 id used by authored package name hashes. */
-[[nodiscard]] std::uint32_t content_hash(std::string_view value) noexcept {
-    std::uint32_t hash = 2166136261U;
-    for (const unsigned char byte : value) {
-        hash = (hash * 16777619U) ^ byte;
-    }
-    return hash;
-}
-
 [[nodiscard]] bool cancelled(NameCancelCheck check) noexcept {
     return check != nullptr && check();
 }
 
+/**
+ * Finds one identifier name by its hash.
+ * @param hashNames Rows sorted by hash, or null for the table State publishes.
+ * @param hash Name hash.
+ * @param name Receives the matching row.
+ * @return True when the chosen table holds that exact hash.
+ */
+[[nodiscard]] bool find_identifier(const std::vector<names_state::Name>* hashNames,
+                                   std::uint32_t hash,
+                                   names_state::Name& name) noexcept {
+    if (hashNames == nullptr) {
+        return state::build_data::find_hash_name(hash, name);
+    }
+    const auto found =
+        std::lower_bound(hashNames->begin(),
+                         hashNames->end(),
+                         hash,
+                         [](const names_state::Name& row, std::uint32_t value) noexcept {
+                             return row.hash < value;
+                         });
+    if (found == hashNames->end() || found->hash != hash) {
+        return false;
+    }
+    name = *found;
+    return true;
+}
+
 [[nodiscard]] bool same_candidate(const catalog::NameCandidate& candidate,
                                   std::string_view value,
                                   catalog::NameProvenance provenance,
@@ -112,6 +133,7 @@ template <typename NameRow>
                                          std::span<const InlineName> inlineNames,
                                          std::span<const state::content::Definition> definitions,
                                          std::span<const HashDefinition> hashDefinitions,
+                                         const std::vector<names_state::Name>* hashNames,
                                          NameCancelCheck cancel,
                                          std::uint32_t hash) {
     catalog::Name name{};
@@ -180,8 +202,8 @@ template <typename NameRow>
     if (cancelled(cancel)) {
         return false;
     }
-    state::build_data::hash_names::Name identifier{};
-    if (state::build_data::find_hash_name(hash, identifier)
+    names_state::Name identifier{};
+    if (find_identifier(hashNames, hash, identifier)
         && !append_candidate(output,
                              name,
                              std::string_view(identifier.name.data(), identifier.nameLength),
@@ -286,15 +308,21 @@ tag_name_row(const catalog::Snapshot& output, std::uint32_t tag, std::uint32_t c
 /** Resolves authored hashes and exact installed-package definition tags in one snapshot. */
 bool resolve_names(catalog::Snapshot& output,
                    std::span<const InlineName> inlineNames,
+                   const std::vector<state::content::Definition>* installed,
+                   const std::vector<names_state::Name>* hashNames,
                    NameCancelCheck cancel) noexcept {
     try {
-        std::vector<state::content::Definition> definitions(
-            state::content::kDefinitionCatalogCapacity);
-        std::size_t definitionCount = 0;
-        if (!state::content::snapshot(definitions, definitionCount)) {
-            return false;
+        std::vector<state::content::Definition> definitions{};
+        if (installed != nullptr) {
+            definitions = *installed;
+        } else {
+            definitions.resize(state::content::kDefinitionCatalogCapacity);
+            std::size_t definitionCount = 0;
+            if (!state::content::snapshot(definitions, definitionCount)) {
+                return false;
+            }
+            definitions.resize(definitionCount);
         }
-        definitions.resize(definitionCount);
         std::sort(definitions.begin(),
                   definitions.end(),
                   [](const state::content::Definition& first,
@@ -315,9 +343,9 @@ bool resolve_names(catalog::Snapshot& output,
         hashDefinitions.reserve(definitions.size());
         for (std::size_t row = 0; row < definitions.size(); ++row) {
             const state::content::Definition& definition = definitions[row];
-            hashDefinitions.push_back(
-                {content_hash(std::string_view(definition.name.data(), definition.nameLength)),
-                 static_cast<std::uint32_t>(row)});
+            hashDefinitions.push_back({catalog::inline_name_evidence::hash(std::as_bytes(std::span(
+                                           definition.name.data(), definition.nameLength))),
+                                       static_cast<std::uint32_t>(row)});
         }
         std::sort(
             hashDefinitions.begin(),
@@ -366,7 +394,7 @@ bool resolve_names(catalog::Snapshot& output,
         for (const std::uint32_t hash : hashes) {
             if (cancelled(cancel)
                 || !resolve_one_hash_name(
-                    output, inlineNames, definitions, hashDefinitions, cancel, hash)) {
+                    output, inlineNames, definitions, hashDefinitions, hashNames, cancel, hash)) {
                 return false;
             }
         }

+ 10 - 1
Sunrise/src/client/content/activity/scriptable_catalog_names.h

@@ -3,8 +3,11 @@
 #include <cstdint>
 #include <span>
 #include <string>
+#include <vector>
 
+#include "../../../state/build_data/hash_names/definition.h"
 #include "../../../state/build_data/scriptables/definition.h"
+#include "../../../state/content/content_catalog.h"
 
 namespace sunrise::client::content::activity::scriptables::internal {
 
@@ -17,9 +20,15 @@ struct InlineName final {
 
 using NameCancelCheck = bool (*)() noexcept;
 
-/** Resolves authored hashes and exact installed-package definition tags in one snapshot. */
+/**
+ * Resolves authored hashes and exact installed-package definition tags in one snapshot.
+ * @param installed Named package definitions, or null for the catalog State publishes.
+ * @param hashNames Identifier names sorted by hash, or null for the table State publishes.
+ */
 [[nodiscard]] bool resolve_names(state::build_data::scriptables::Snapshot& output,
                                  std::span<const InlineName> inlineNames,
+                                 const std::vector<state::content::Definition>* installed,
+                                 const std::vector<state::build_data::hash_names::Name>* hashNames,
                                  NameCancelCheck cancel) noexcept;
 
 } // namespace sunrise::client::content::activity::scriptables::internal

+ 8 - 3
Sunrise/src/client/content/activity/scriptable_catalog_spatial_graph.cpp

@@ -20,12 +20,14 @@
 #include "../../../middleware/content/packages/tables/component_container_reader.h"
 #include "../../../middleware/content/packages/tables/static_spatial_candidate_reader.h"
 #include "../../../state/build_data/runtime.h"
+#include "../scenarios/scenario_build.h"
 #include "../spawn_sets/spawn_set_catalog_builder.h"
 #include "scriptable_catalog_inline_names.h"
 
 namespace sunrise::client::content::activity::scriptables::internal {
 
 namespace catalog = state::build_data::scriptables;
+namespace scenario_layouts = ::sunrise::client::content::scenarios;
 
 // Fixed capacities bound one cached table's rows and inline bytes.
 constexpr std::size_t kCachedTableCapacity = 2'048;
@@ -683,6 +685,7 @@ namespace {
                                                          package_reader::Scratch& scratch,
                                                          StaticSpatialCache* cache,
                                                          const ContainerIndex& containers,
+                                                         const scenario_layouts::LayoutSet* layouts,
                                                          std::string_view scenarioName,
                                                          catalog::Snapshot& output,
                                                          SpatialCancelCheck cancel) noexcept {
@@ -690,7 +693,7 @@ namespace {
     output.staticSpatialNotApplicable = false;
     output.staticSpatialComplete = false;
     state::build_data::scenarios::Definition scenario{};
-    if (!state::build_data::find_scenario_layout(scenarioName, scenario)) {
+    if (!scenario_layouts::find_layout(layouts, scenarioName, scenario)) {
         ++output.staticSpatialUnresolvedReads;
         return true;
     }
@@ -748,11 +751,12 @@ StaticSpatialCache::~StaticSpatialCache() = default;
 bool append_static_spatial_candidates(const package_reader::Source& source,
                                       package_reader::Scratch& scratch,
                                       const ContainerIndex& containers,
+                                      const scenario_layouts::LayoutSet* layouts,
                                       std::string_view scenarioName,
                                       catalog::Snapshot& output,
                                       SpatialCancelCheck cancel) noexcept {
     return append_static_spatial_candidates_impl(
-        source, scratch, nullptr, containers, scenarioName, output, cancel);
+        source, scratch, nullptr, containers, layouts, scenarioName, output, cancel);
 }
 
 /** Appends package spatial candidates with pass-local table reuse. */
@@ -760,11 +764,12 @@ bool append_static_spatial_candidates(const package_reader::Source& source,
                                       package_reader::Scratch& scratch,
                                       StaticSpatialCache& cache,
                                       const ContainerIndex& containers,
+                                      const scenario_layouts::LayoutSet* layouts,
                                       std::string_view scenarioName,
                                       catalog::Snapshot& output,
                                       SpatialCancelCheck cancel) noexcept {
     return append_static_spatial_candidates_impl(
-        source, scratch, &cache, containers, scenarioName, output, cancel);
+        source, scratch, &cache, containers, layouts, scenarioName, output, cancel);
 }
 
 } // namespace sunrise::client::content::activity::scriptables::internal

+ 14 - 2
Sunrise/src/client/content/activity/scriptable_catalog_spatial_graph.h

@@ -7,6 +7,10 @@
 #include "../../../state/build_data/scriptables/definition.h"
 #include "scriptable_catalog_container_index.h"
 
+namespace sunrise::client::content::scenarios {
+class LayoutSet;
+} // namespace sunrise::client::content::scenarios
+
 namespace sunrise::client::content::activity::scriptables::internal {
 
 using SpatialCancelCheck = bool (*)() noexcept;
@@ -29,21 +33,29 @@ private:
     friend struct StaticSpatialCacheAccess;
 };
 
-/** Appends package spatial candidates whose stem and bubble mask match this scenario. */
+/**
+ * Appends package spatial candidates whose stem and bubble mask match this scenario.
+ * @param layouts Destination layouts to read the stem from, or null for the ones State publishes.
+ */
 [[nodiscard]] bool
 append_static_spatial_candidates(const middleware::content::packages::reader::Source& source,
                                  middleware::content::packages::reader::Scratch& scratch,
                                  const ContainerIndex& containers,
+                                 const ::sunrise::client::content::scenarios::LayoutSet* layouts,
                                  std::string_view scenarioName,
                                  state::build_data::scriptables::Snapshot& output,
                                  SpatialCancelCheck cancel) noexcept;
 
-/** Appends static-spatial rows while reusing complete tables from this package pass. */
+/**
+ * Appends static-spatial rows while reusing complete tables from this package pass.
+ * @param layouts Destination layouts to read the stem from, or null for the ones State publishes.
+ */
 [[nodiscard]] bool
 append_static_spatial_candidates(const middleware::content::packages::reader::Source& source,
                                  middleware::content::packages::reader::Scratch& scratch,
                                  StaticSpatialCache& cache,
                                  const ContainerIndex& containers,
+                                 const ::sunrise::client::content::scenarios::LayoutSet* layouts,
                                  std::string_view scenarioName,
                                  state::build_data::scriptables::Snapshot& output,
                                  SpatialCancelCheck cancel) noexcept;

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

@@ -206,8 +206,8 @@ descriptor_object(const catalog::Snapshot& source, const catalog::Descriptor& de
         active = scenarioActive;
     }
     link.candidateCount = static_cast<std::uint32_t>(retained);
-    link.activeCandidateCount = static_cast<std::uint32_t>((
-        std::min)(active, static_cast<std::uint64_t>((std::numeric_limits<std::uint32_t>::max)())));
+    link.activeCandidateCount = static_cast<std::uint32_t>((std::min)(
+        active, static_cast<std::uint64_t>((std::numeric_limits<std::uint32_t>::max)())));
     if (active == 0) {
         ++output.type23PlacementDiagnostics.zeroActiveCandidates;
     } else if (active > 1) {

+ 23 - 25
Sunrise/src/client/content/activity/scriptable_catalog_worker.cpp

@@ -279,28 +279,6 @@ namespace {
     return count;
 }
 
-/** @return True when one local row bank fits after an existing u32-indexed bank. */
-[[nodiscard]] bool can_append_rows(std::size_t existing, std::size_t incoming) noexcept {
-    // Snapshot rows are published as u32, so a bank stops at that maximum.
-    constexpr std::size_t maximum = (std::numeric_limits<std::uint32_t>::max)();
-    return existing <= maximum && incoming <= maximum - existing;
-}
-
-/** Rebases one present local row while preserving the absent sentinel. */
-[[nodiscard]] bool
-rebase_row(std::uint32_t local, std::uint32_t first, std::uint32_t& output) noexcept {
-    if (local == catalog::kNoRow) {
-        output = catalog::kNoRow;
-        return true;
-    }
-    if (local > (std::numeric_limits<std::uint32_t>::max)() - first) {
-        output = catalog::kNoRow;
-        return false;
-    }
-    output = first + local;
-    return true;
-}
-
 /** Appends one exact scenario placement and its analyzed rows to the snapshot. */
 [[nodiscard]] bool materialize_placement(BuildContext& context,
                                          const tables::Placement& placement) {
@@ -558,6 +536,7 @@ build_scenario_catalog_impl(const package_reader::Source& source,
                             AnalysisMap* sharedAnalyses,
                             internal::ContainerPlacementCache* sharedPlacements,
                             internal::StaticSpatialCache* sharedSpatial,
+                            const internal::BuildTables& tables,
                             std::uint32_t scenarioTag,
                             std::string_view scenarioName,
                             internal::BuilderCancelCheck cancel) {
@@ -624,6 +603,7 @@ build_scenario_catalog_impl(const package_reader::Source& source,
                                                            (*context->scenarioSource.scratch),
                                                            *sharedPlacements,
                                                            containers,
+                                                           tables.layouts,
                                                            scenarioName,
                                                            *output,
                                                            cancel)
@@ -631,6 +611,7 @@ build_scenario_catalog_impl(const package_reader::Source& source,
                                                            source,
                                                            (*context->scenarioSource.scratch),
                                                            containers,
+                                                           tables.layouts,
                                                            scenarioName,
                                                            *output,
                                                            cancel))) {
@@ -647,6 +628,7 @@ build_scenario_catalog_impl(const package_reader::Source& source,
                                                        (*context->scenarioSource.scratch),
                                                        *sharedSpatial,
                                                        containers,
+                                                       tables.layouts,
                                                        scenarioName,
                                                        *output,
                                                        cancel)
@@ -654,6 +636,7 @@ build_scenario_catalog_impl(const package_reader::Source& source,
                                                        source,
                                                        (*context->scenarioSource.scratch),
                                                        containers,
+                                                       tables.layouts,
                                                        scenarioName,
                                                        *output,
                                                        cancel);
@@ -666,7 +649,11 @@ build_scenario_catalog_impl(const package_reader::Source& source,
             set_step_detail(*output, step, cancelled(cancel));
             output->status = catalog::BuildStatus::failed;
         } else if (!internal::canonicalize_inline_name_evidence(*output)
-                   || !internal::resolve_names(*output, context->inlineNames, cancel)) {
+                   || !internal::resolve_names(*output,
+                                               context->inlineNames,
+                                               tables.definitions,
+                                               tables.hashNames,
+                                               cancel)) {
             set_detail(*output, "package name candidates did not fit");
             output->status = catalog::BuildStatus::failed;
         } else {
@@ -706,6 +693,7 @@ ScenarioAnalysisCache::ScenarioAnalysisCache() noexcept {
 
 ScenarioAnalysisCache::~ScenarioAnalysisCache() = default;
 
+/** Builds one scenario catalog from the tables State publishes, with no pass cache. */
 [[nodiscard]] std::shared_ptr<state::build_data::scriptables::Snapshot>
 build_scenario_catalog(const middleware::content::packages::reader::Source& source,
                        const ContainerIndex& containers,
@@ -713,8 +701,16 @@ build_scenario_catalog(const middleware::content::packages::reader::Source& sour
                        std::uint32_t scenarioTag,
                        std::string_view scenarioName,
                        BuilderCancelCheck cancel) {
-    return build_scenario_catalog_impl(
-        source, containers, scratch, nullptr, nullptr, nullptr, scenarioTag, scenarioName, cancel);
+    return build_scenario_catalog_impl(source,
+                                       containers,
+                                       scratch,
+                                       nullptr,
+                                       nullptr,
+                                       nullptr,
+                                       BuildTables{},
+                                       scenarioTag,
+                                       scenarioName,
+                                       cancel);
 }
 
 /** Builds one scenario catalog, reusing the caller's analysis caches when they exist. */
@@ -723,6 +719,7 @@ build_scenario_catalog(const middleware::content::packages::reader::Source& sour
                        const ContainerIndex& containers,
                        middleware::content::packages::reader::Scratch& scratch,
                        ScenarioAnalysisCache& analyses,
+                       const BuildTables& tables,
                        std::uint32_t scenarioTag,
                        std::string_view scenarioName,
                        BuilderCancelCheck cancel) {
@@ -737,6 +734,7 @@ build_scenario_catalog(const middleware::content::packages::reader::Source& sour
                                        shared,
                                        placements,
                                        spatial,
+                                       tables,
                                        scenarioTag,
                                        scenarioName,
                                        cancel);

+ 4 - 1
Sunrise/src/client/content/activity/scriptable_catalog_worker_lifecycle.cpp

@@ -10,6 +10,7 @@
 #include <utility>
 
 #include "../../../core/filesystem/path.h"
+#include "../../../middleware/content/packages/reader/overlay.h"
 #include "../../../state/build_data/scriptables/scriptable_catalog.h"
 #include "../items/packages/internal.h"
 #include "activity_sdk_generation_worker.h"
@@ -124,7 +125,9 @@ DWORD WINAPI thread_main(void* opaque) noexcept {
     std::shared_ptr<catalog::Snapshot> output{};
     std::shared_ptr<catalog::Snapshot> cached{};
     try {
-        if (sdk_generation::load_cached_scenario(
+        // SDK shards hold installed content only; with mod package files the server builds its own.
+        if (package_reader::overlay::files().empty()
+            && sdk_generation::load_cached_scenario(
                 work->request.scenarioTag,
                 std::string_view(work->request.name.data(), work->request.nameLength),
                 cached)) {

+ 92 - 16
Sunrise/src/client/content/hash_names/hash_name_build.cpp

@@ -2,8 +2,10 @@
 
 #include <Windows.h>
 
+#include <algorithm>
 #include <array>
 #include <cstdio>
+#include <memory>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -17,6 +19,7 @@
 namespace sunrise::client::content::hash_names {
 namespace {
 
+namespace layouts = state::build_data::scenarios;
 namespace packages = middleware::content::packages;
 namespace reader = middleware::content::packages::reader;
 
@@ -36,26 +39,38 @@ constexpr std::uint32_t kNamedObjectClass = 0x80809C36U;
 using Rows = std::array<names_state::Name, names_state::kNameCapacity>;
 
 /**
- * One row set per reader, kept across batches so a reader builds up its own best names.
- * Each is a quarter of a megabyte, so they are allocated rather than declared. The pass frees
- * them when it publishes.
+ * One row set per reader for the boot pass, kept across batches so a reader builds up its own
+ * best names. Each is a quarter of a megabyte, so they are allocated. Freed on publish.
  */
 std::vector<Rows> g_rows{};
 
 /** What the readers need to reach the pass. Every reader reads it and none writes it. */
 struct BatchContext {
     Storage* storage{};
+    /** One row set per reader index. */
+    std::vector<Rows>* rows{};
 };
 
-/** @param storage Pass storage. @param workers Readers wanted. @return True when rows are ready. */
-[[nodiscard]] bool prepare_rows(const Storage& storage, std::size_t workers) noexcept {
+/**
+ * Sizes the reader rows for a pass.
+ * @param storage Pass storage.
+ * @param rows Row sets to size.
+ * @param workers Readers wanted.
+ * @return True when rows are ready.
+ */
+[[nodiscard]] bool
+prepare_rows(const Storage& storage, std::vector<Rows>& rows, std::size_t workers) noexcept {
     if (storage.targetCount > names_state::kNameCapacity) {
         return false;
     }
-    if (g_rows.size() < workers) {
-        g_rows.resize(workers);
+    try {
+        if (rows.size() < workers) {
+            rows.resize(workers);
+        }
+    } catch (...) {
+        return false;
     }
-    return g_rows.size() >= workers;
+    return rows.size() >= workers;
 }
 
 /** Frees the reader rows. */
@@ -75,12 +90,13 @@ void collect_blob(void* context,
                   std::size_t worker,
                   std::uint32_t,
                   std::span<const std::byte> blob) noexcept {
-    const Storage& storage = *static_cast<BatchContext*>(context)->storage;
-    if (worker >= g_rows.size()) {
+    const auto& batch = *static_cast<BatchContext*>(context);
+    const Storage& storage = *batch.storage;
+    if (worker >= batch.rows->size()) {
         return;
     }
     MatchState state{{storage.targets.data(), storage.targetCount},
-                     {g_rows[worker].data(), storage.targetCount}};
+                     {(*batch.rows)[worker].data(), storage.targetCount}};
     offer_blob(state, blob);
 }
 
@@ -165,18 +181,25 @@ bool build(const reader::Source& source, reader::Scratch&) noexcept {
     if (!storage.collected) {
         reset(storage);
         storage.startedTick = GetTickCount64();
-        if (!collect_targets(storage)) {
+        // One whole domain of fixed rows, so the snapshot is static rather than a local.
+        static std::array<layouts::Definition, layouts::kDefinitionCapacity> scratch{};
+        std::size_t rows = 0;
+        if (!state::build_data::snapshot_scenario_layouts(scratch, rows)) {
             report(storage, "targets");
             reset(storage);
             return false;
         }
+        // Nothing to name still publishes an empty table; the SDK generator waits for one.
+        if (!collect_targets(storage, std::span(scratch).first(rows))) {
+            return publish(storage, "no_targets");
+        }
         // The package directory names cost no blob read, so they are taken first and a pass that
         // cannot sweep still publishes them.
         packages::named_tags::DirectoryResult directory{};
         (void)packages::named_tags::extract_directory(
-            source.directory, &collect_named, &storage, directory);
+            source.directory, source.installedOnly, &collect_named, &storage, directory);
         reader::ScanResult scan{};
-        if (!reader::scan_class(source.directory, kNamedObjectClass, &collect_tag, &storage, scan)
+        if (!reader::scan_class(source, kNamedObjectClass, &collect_tag, &storage, scan)
             || scan.matches != storage.tagCount) {
             return publish(storage, "sweep");
         }
@@ -189,11 +212,16 @@ bool build(const reader::Source& source, reader::Scratch&) noexcept {
     // and one reader per share overlaps those waits. The call returns once all of them have
     // joined, so nothing outlives the batch.
     const std::size_t workers = reader::parallel::worker_count();
-    if (!prepare_rows(storage, workers)) {
+    if (!prepare_rows(storage, g_rows, workers)) {
         return publish(storage, "readers");
     }
+    // A boot slice never waits for the reader; the next slice tries again.
+    const reader::parallel::Lease lease{reader::parallel::kTryAcquire};
+    if (!lease.held()) {
+        return false;
+    }
     const std::size_t batch = (std::min)(kBatchTags, storage.tagCount - storage.cursor);
-    BatchContext context{&storage};
+    BatchContext context{&storage, &g_rows};
     if (!reader::parallel::read_tags(
             source, {storage.tags.data() + storage.cursor, batch}, &collect_blob, &context)) {
         return publish(storage, "readers");
@@ -213,4 +241,52 @@ bool build(const reader::Source& source, reader::Scratch&) noexcept {
     return published;
 }
 
+/** Finds the names of the given hashes from installed packages only, in one call. */
+bool build_private(const reader::Source& source,
+                   std::span<const layouts::Definition> definitions,
+                   std::vector<names_state::Name>& output) noexcept {
+    output.clear();
+    const reader::Source installed{source.directory, source.keys, true};
+    try {
+        // Over a megabyte, so it lives on the heap.
+        const auto storage = std::make_unique<Storage>();
+        if (!collect_targets(*storage, definitions)) {
+            return true;
+        }
+        packages::named_tags::DirectoryResult directory{};
+        (void)packages::named_tags::extract_directory(
+            installed.directory, installed.installedOnly, &collect_named, storage.get(), directory);
+        reader::ScanResult scan{};
+        if (!reader::scan_class(installed, kNamedObjectClass, &collect_tag, storage.get(), scan)
+            || scan.matches != storage->tagCount) {
+            return false;
+        }
+        std::vector<Rows> rows{};
+        if (!prepare_rows(*storage, rows, reader::parallel::worker_count())) {
+            return false;
+        }
+        BatchContext context{storage.get(), &rows};
+        bool read = false;
+        {
+            const reader::parallel::ScopedRead lease{};
+            read = reader::parallel::read_tags(
+                installed, {storage->tags.data(), storage->tagCount}, &collect_blob, &context);
+        }
+        if (!read) {
+            return false;
+        }
+        for (const Rows& worker : rows) {
+            merge(*storage, {worker.data(), storage->targetCount});
+        }
+        finish(*storage);
+        output.assign(storage->resolved.begin(),
+                      storage->resolved.begin()
+                          + static_cast<std::ptrdiff_t>(storage->resolvedCount));
+        return true;
+    } catch (...) {
+        output.clear();
+        return false;
+    }
+}
+
 } // namespace sunrise::client::content::hash_names

+ 19 - 0
Sunrise/src/client/content/hash_names/hash_name_build.h

@@ -1,6 +1,12 @@
 #pragma once
 
+#include <cstdint>
+#include <span>
+#include <vector>
+
 #include "../../../middleware/content/packages/reader/reader.h"
+#include "../../../state/build_data/hash_names/definition.h"
+#include "../../../state/build_data/scenarios/definition.h"
 
 namespace sunrise::client::content::hash_names {
 
@@ -15,4 +21,17 @@ namespace sunrise::client::content::hash_names {
 [[nodiscard]] bool build(const middleware::content::packages::reader::Source& source,
                          middleware::content::packages::reader::Scratch& scratch) noexcept;
 
+/**
+ * Names the bubble and spawn-set hashes from installed packages only, in one call.
+ * Publishes nothing. Waits for the parallel reader, so never call it from a boot slice.
+ * @param source Package directory and block keys; mod files are ignored whatever it says.
+ * @param definitions Installed destination layouts whose bubble hashes are named.
+ * @param output Receives the named rows in ascending hash order; empty on failure.
+ * @return True when the whole pass ran, or when there was nothing to name.
+ */
+[[nodiscard]] bool
+build_private(const middleware::content::packages::reader::Source& source,
+              std::span<const state::build_data::scenarios::Definition> definitions,
+              std::vector<state::build_data::hash_names::Name>& output) noexcept;
+
 } // namespace sunrise::client::content::hash_names

+ 40 - 40
Sunrise/src/client/content/hash_names/hash_name_matcher.cpp

@@ -1,28 +1,17 @@
 #include "hash_name_matcher.h"
 
 #include <algorithm>
+#include <vector>
 
 #include "../../../state/build_data/runtime.h"
+#include "../../../state/build_data/scriptables/inline_name_evidence.h"
 
 namespace sunrise::client::content::hash_names {
 namespace {
 
 namespace layouts = state::build_data::scenarios;
 namespace spawn_state = state::build_data::spawn_sets;
-
-/** FNV-1 32-bit offset basis. The engine hashes names with FNV-1, not FNV-1a. */
-constexpr std::uint32_t kHashBasis = 0x811C9DC5U;
-/** FNV-1 32-bit prime. */
-constexpr std::uint32_t kHashPrime = 0x01000193U;
-
-/** @return The FNV-1 hash of text, which is what a bubble carries. */
-[[nodiscard]] std::uint32_t hash_of(std::string_view text) noexcept {
-    std::uint32_t value = kHashBasis;
-    for (const char character : text) {
-        value = (value * kHashPrime) ^ static_cast<std::uint8_t>(character);
-    }
-    return value;
-}
+namespace evidence = state::build_data::scriptables::inline_name_evidence;
 
 /**
  * Records one candidate against its target row.
@@ -60,6 +49,28 @@ target_slot(const MatchState& state, std::uint32_t hash, std::size_t& slot) noex
     return true;
 }
 
+/** Sorts the targets and drops duplicates. */
+void compact_targets(Storage& storage) noexcept {
+    const auto end = storage.targets.begin() + static_cast<std::ptrdiff_t>(storage.targetCount);
+    std::sort(storage.targets.begin(), end);
+    storage.targetCount = static_cast<std::size_t>(std::unique(storage.targets.begin(), end)
+                                                   - storage.targets.begin());
+}
+
+/** Adds one nonzero hash. Layouts repeat hashes, so a full table drops duplicates first. */
+void add_target(Storage& storage, std::uint32_t hash) noexcept {
+    if (hash == 0) {
+        return;
+    }
+    if (storage.targetCount == storage.targets.size()) {
+        compact_targets(storage);
+    }
+    // Past capacity the extra hashes stay unnamed.
+    if (storage.targetCount < storage.targets.size()) {
+        storage.targets[storage.targetCount++] = hash;
+    }
+}
+
 } // namespace
 
 /** Clears one extraction pass without freeing process-owned outer storage. */
@@ -74,43 +85,31 @@ void reset(Storage& storage) noexcept {
     storage.invalid = false;
 }
 
-/** Records every hash the published domains declare: bubble names and spawn-set names. */
-bool collect_targets(Storage& storage) noexcept {
+/** Records every hash the given layouts and the spawn sets declare. */
+bool collect_targets(Storage& storage, std::span<const layouts::Definition> definitions) noexcept {
     storage.targets.fill(0);
     storage.targetCount = 0;
-    // One whole domain of fixed rows, so the snapshot is static rather than a local.
-    static std::array<layouts::Definition, layouts::kDefinitionCapacity> scratch{};
-    std::size_t rows = 0;
-    if (!state::build_data::snapshot_scenario_layouts(scratch, rows)) {
-        return false;
-    }
-    for (std::size_t row = 0; row < rows; ++row) {
-        const layouts::Definition& layout = scratch[row];
+    for (const layouts::Definition& layout : definitions) {
         const std::size_t declared =
             (std::min)(static_cast<std::size_t>(layout.bubbleCount), layout.bubbleHashes.size());
         for (std::size_t bubble = 0; bubble < declared; ++bubble) {
-            const std::uint32_t hash = layout.bubbleHashes[bubble];
-            if (hash == 0 || storage.targetCount >= storage.targets.size()) {
-                continue;
-            }
-            storage.targets[storage.targetCount++] = hash;
+            add_target(storage, layout.bubbleHashes[bubble]);
         }
     }
     // The spawn sets name themselves the same way, and this pass already reads every blob that
     // could carry one, so their hashes ride along at no extra read.
-    static std::array<spawn_state::NameHash, spawn_state::kNameHashCapacity> spawns{};
-    std::size_t spawnCount = 0;
-    if (state::build_data::snapshot_spawn_name_hashes(spawns, spawnCount)) {
-        for (std::size_t row = 0; row < spawnCount; ++row) {
-            if (spawns[row].value != 0 && storage.targetCount < storage.targets.size()) {
-                storage.targets[storage.targetCount++] = spawns[row].value;
+    try {
+        std::vector<spawn_state::NameHash> spawns(spawn_state::kNameHashCapacity);
+        std::size_t spawnCount = 0;
+        if (state::build_data::snapshot_spawn_name_hashes(spawns, spawnCount)) {
+            for (std::size_t row = 0; row < spawnCount; ++row) {
+                add_target(storage, spawns[row].value);
             }
         }
+    } catch (...) {
+        // The bubble hashes still name; the spawn sets stay unnamed.
     }
-    auto end = storage.targets.begin() + static_cast<std::ptrdiff_t>(storage.targetCount);
-    std::sort(storage.targets.begin(), end);
-    storage.targetCount = static_cast<std::size_t>(std::unique(storage.targets.begin(), end)
-                                                   - storage.targets.begin());
+    compact_targets(storage);
     return storage.targetCount != 0;
 }
 
@@ -138,7 +137,8 @@ void offer(MatchState& state, std::string_view text) noexcept {
             }
             const std::string_view candidate = text.substr(begin, end - begin);
             std::size_t slot = 0;
-            if (target_slot(state, hash_of(candidate), slot)) {
+            // A bubble carries the FNV-1 hash of its name.
+            if (target_slot(state, evidence::hash(candidate), slot)) {
                 keep(state, candidate, slot);
             }
         }

+ 8 - 4
Sunrise/src/client/content/hash_names/hash_name_matcher.h

@@ -7,6 +7,7 @@
 #include <string_view>
 
 #include "../../../state/build_data/hash_names/definition.h"
+#include "../../../state/build_data/scenarios/definition.h"
 
 namespace sunrise::client::content::hash_names {
 
@@ -66,11 +67,14 @@ struct Storage {
 void reset(Storage& storage) noexcept;
 
 /**
- * Records every hash the published domains declare: bubble names and spawn-set names.
- * @param storage Pass storage receiving the ascending unique target set.
- * @return True when both domains are readable and their hashes fit.
+ * Records every hash the given layouts and the published spawn sets declare.
+ * @param storage Pass storage receiving the ascending unique nonzero target set.
+ * @param definitions Destination layouts whose bubble hashes are named.
+ * @return True when at least one target was found.
  */
-[[nodiscard]] bool collect_targets(Storage& storage) noexcept;
+[[nodiscard]] bool
+collect_targets(Storage& storage,
+                std::span<const state::build_data::scenarios::Definition> definitions) noexcept;
 
 /**
  * Offers one id, keeping the shortest name that gives a target hash.

+ 1 - 4
Sunrise/src/client/content/investment/investment_refresh.cpp

@@ -39,14 +39,11 @@ bool requires_package_sweep() noexcept {
 /** Publishes every installed equipment mapping domain. */
 bool refresh() noexcept {
     if (ready()) {
-        // The same lock as the extraction path. A cache write holds its own lock across file
-        // calls, so a held thread stopped inside one would deadlock the freeze below.
         const std::lock_guard lock(g_refreshLock);
         const bool persisted = state::ensure_profile_item_identities()
                                && state::ensure_character_subclasses() && emote_collection_settled()
                                && state::build_data::persist();
-        // Nothing reads a package again until the next boot, so the open files and the held
-        // tables go back now rather than at process exit.
+        // Nothing reads a package again until the next boot, so release the files and tables here.
         middleware::content::packages::reader::release_caches();
         core::ui::busy::end(core::ui::busy::Task::contentExtraction);
         return persisted;

+ 0 - 9
Sunrise/src/client/content/items/layout.h

@@ -14,8 +14,6 @@ inline constexpr std::size_t kTableFirstRowOffset = 48;
 inline constexpr std::size_t kRowTargetHandleOffset = 16;
 /** Native item index rows are 24 bytes. */
 inline constexpr std::size_t kItemIndexRowSize = 24;
-/** A loaded gameplay definition stores its inventory bucket at byte 184. */
-inline constexpr std::size_t kDefinitionBucketOffset = 184;
 
 /** One dense native item index row. */
 struct ItemIndexRow {
@@ -35,16 +33,9 @@ struct ItemTable {
     ItemIndexRow firstRow{};
 };
 
-/** Fixed prefix through the gameplay definition's bucket field. */
-struct ItemDefinition {
-    std::array<std::byte, kDefinitionBucketOffset> opaque00{};
-    std::uint8_t bucketId{};
-};
-
 static_assert(offsetof(ItemIndexRow, targetHandle) == kRowTargetHandleOffset);
 static_assert(sizeof(ItemIndexRow) == kItemIndexRowSize);
 static_assert(offsetof(ItemTable, rowCount) == kTableRowCountOffset);
 static_assert(offsetof(ItemTable, firstRow) == kTableFirstRowOffset);
-static_assert(offsetof(ItemDefinition, bucketId) == kDefinitionBucketOffset);
 
 } // namespace sunrise::client::content::items::layout

+ 3 - 0
Sunrise/src/client/content/items/packages/build.h

@@ -19,4 +19,7 @@ namespace sunrise::client::content::items::packages {
  */
 [[nodiscard]] bool readable() noexcept;
 
+/** Exports once at Client activation only when the startup setting requests it. */
+void export_keys_if_requested() noexcept;
+
 } // namespace sunrise::client::content::items::packages

+ 0 - 1
Sunrise/src/client/content/items/packages/internal.h

@@ -138,7 +138,6 @@ struct Storage {
     DetailRequests detailRequests{};
     std::array<std::uint16_t, kDetailCapacity> requestedDetailIndices{};
     std::vector<state::build_data::items::details::Definition> details{};
-    AuthoredHashes authoredHashes{};
     std::vector<std::byte> abilityTable{};
     std::vector<std::byte> abilityPool{};
     std::array<state::build_data::abilities::Definition,

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.