Browse Source

Select Ember landing spawn set and honor explicit spawn overrides

Millie 4 days ago
parent
commit
7c048464e3

+ 23 - 0
MISSION_EMBER_STATUS.md

@@ -1,5 +1,28 @@
 # Ember implementation status
 
+## Landing spawn selection — 2026-09-05
+
+The next playtest confirmed the movie completes and the player spawns, but in the wrong place
+under the map. Logs now resolve both cinematic notifications and arm the transition to state 64.
+The installed arrival override selected the cinematic region without an explicit spawn set.
+
+The native build-data catalogue contains a three-point landing set whose hash matches
+`powerhouse_landing_mercury` (`0x9C58857A`). Its points align with the SDK landing anchors and
+the opening area in the reference video. Settings now explicitly select that set while retaining
+bubble 6/state 49 for the movie; Lua still selects playable state 64 after termination.
+
+The spawn-binding filter previously discarded explicit sets when their activity package was
+absent from the scenario's direct package list. That list does not inventory transitive
+references. Explicit overrides now retain their documented precedence; inferred spawn sets
+keep the existing package filter. Both roster and global-state publishers use this helper.
+
+Validation: all four portable CTest tests passed, including explicit override precedence,
+absent/zero overrides and the existing inferred-set filtering behavior. The Release DLL built
+successfully and the native settings probe accepted the updated configuration with SDK and
+mission scripting enabled. Installed and verified the root DLL and arrival setting, preserving
+other preferences. Backup: `build/landing-spawn-backup-20260905-081125`.
+The selected landing position still requires a fresh in-game test after the cinematic.
+
 ## Cinematic handoff fix — 2026-09-05
 
 Playtest confirmed the arrival movie plays, but the player remained black after fade-out.

+ 2 - 1
Sunrise/resources/default_settings.json

@@ -108,7 +108,8 @@
         {
           "package_name": "mission_ember",
           "bubble": 6,
-          "slice_set": 49
+          "slice_set": 49,
+          "spawn_set_hash": "0x9C58857A"
         },
         {
           "package_name": "raid_envy_v310",

+ 4 - 2
Sunrise/src/state/activity/destination/activity_destination_spawn_binding.cpp

@@ -137,11 +137,13 @@ std::uint16_t spawn_set_slice_set(const DestinationSelection& selection,
     return arrivalSliceSet;
 }
 
-/** Drops a spawn set the destination cannot load. Only a proved miss is dropped. */
+/** Filters inferred spawn sets while preserving explicit authored/operator overrides. */
 std::uint32_t attachable_spawn_set_hash(const DestinationSelection& selection,
                                         std::uint32_t fallback) noexcept {
     const std::uint32_t hash = resolve_spawn_set_hash(selection, fallback);
-    if (hash == 0 || hash == kAbsentSpawnSetHash) {
+    // The direct scenario package list is not a transitive dependency inventory. An explicit
+    // override may name a set in a referenced activity package, so it must win over this filter.
+    if (selection.hasSpawnSetOverride || hash == 0 || hash == kAbsentSpawnSetHash) {
         return hash;
     }
     const std::string_view name = name_of(selection);

+ 4 - 4
Sunrise/src/state/activity/destination/activity_destination_spawn_binding.h

@@ -7,12 +7,12 @@
 namespace sunrise::state::activity::destination {
 
 /**
- * Picks the spawn-set hash to send, dropping one the destination does not load.
- * A set declared only by a package the destination never loads cannot attach, and the player
- * arrives with no spawn point. The absent hash goes out instead, so the Client picks its own.
+ * Picks the spawn-set hash to send, preserving explicit authored/operator overrides.
+ * Inferred hashes are filtered against the known map/direct activity packages. The list is not
+ * a transitive dependency inventory, so a missing package must not veto an explicit override.
  * @param selection Committed destination.
  * @param fallback Authored fallback hash.
- * @return The resolved hash, or the absent hash when the set cannot attach.
+ * @return The explicit override, or the inferred hash after package filtering.
  */
 [[nodiscard]] std::uint32_t attachable_spawn_set_hash(const DestinationSelection& selection,
                                                       std::uint32_t fallback) noexcept;

+ 7 - 4
scripts/README.md

@@ -2,8 +2,8 @@
 
 This controller is a development playtest slice, not complete 1AU. It selects the authored
 arrival-cinematic state (49), activates its bookend when held, and hands off to powerhouse
-state 64 on the matching termination event. The supplied reference video starts in gameplay;
-it cannot establish the arrival movie’s identity.
+state 64 on the matching termination event. The separate arrival reference establishes the
+movie sequence, and the latest playtest confirmed playback and the termination handoff.
 
 The landing encounter uses eleven authored Mercury squads with their default counts.
 Bridge devices reset only when the playable region is held. Arrival dialogue waits for spawn
@@ -21,8 +21,11 @@ refused alternate spawn-rule bindings. Never substitute arbitrary enemies or coo
 
 Copy these scripts to `Sunrise/scripts` beside `Sunrise/sdk/lua`. Enable
 `server.activation.mission_scripting`. For cinematic arrival, the settings override for
-`mission_ember` must use `bubble: 6` and `slice_set: 49`; gameplay is selected by the script
-after the cinematic. Restart after updating arrival settings.
+`mission_ember` must use `bubble: 6`, `slice_set: 49` and
+`spawn_set_hash: "0x9C58857A"` (the authored Mercury landing set). Gameplay is selected by
+the script after the cinematic. Use the current native build so the explicit spawn override
+is preserved even when its package is absent from the direct scenario package list.
+Restart after updating arrival settings.
 
 Region-less client deltas preserve encounter ownership. Duplicate events and script reload
 do not repeat the movie, spawn requests, dialogue or bridge reset. Mock-context tests verify

+ 8 - 0
tests/CMakeLists.txt

@@ -34,3 +34,11 @@ else()
     target_compile_options(incident_payload_test PRIVATE -Wall -Wextra -Werror -UNDEBUG)
 endif()
 add_test(NAME incident_payload COMMAND incident_payload_test)
+add_executable(spawn_override_test spawn_override_test.cpp)
+target_compile_features(spawn_override_test PRIVATE cxx_std_20)
+if(MSVC)
+    target_compile_options(spawn_override_test PRIVATE /W4 /WX /UNDEBUG)
+else()
+    target_compile_options(spawn_override_test PRIVATE -Wall -Wextra -Werror -UNDEBUG)
+endif()
+add_test(NAME spawn_override COMMAND spawn_override_test)

+ 59 - 0
tests/spawn_override_test.cpp

@@ -0,0 +1,59 @@
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+
+#include "../Sunrise/src/state/activity/destination/activity_destination_spawn_binding.cpp"
+
+namespace data = sunrise::state::build_data;
+namespace dest = sunrise::state::activity::destination;
+static data::scenarios::Definition layout;
+static data::spawn_sets::NameHash spawn;
+static bool available = true;
+namespace sunrise::state::build_data {
+bool find_scenario_layout(std::string_view, scenarios::Definition& out) noexcept {
+    out = layout;
+    return available;
+}
+bool find_spawn_sets(std::string_view,
+                     std::span<spawn_sets::NameHash> out,
+                     std::size_t& count) noexcept {
+    count = 1;
+    out[0] = spawn;
+    return true;
+}
+} // namespace sunrise::state::build_data
+namespace sunrise::core::log {
+void write(Channel, Level, std::string_view) noexcept {}
+} // namespace sunrise::core::log
+int main() {
+    dest::DestinationSelection selection{};
+    selection.packageName[0] = 'x';
+    selection.packageNameLength = 1;
+    selection.hasSpawnSetHash = true;
+    selection.spawnSetHash = 0x12345678;
+    layout.spawnStem[0] = 'x';
+    layout.spawnStemLength = 1;
+    layout.packageCount = 1;
+    layout.packages[0] = 10;
+    spawn.value = selection.spawnSetHash;
+    spawn.activityPackageCount = 1;
+    spawn.activityPackages[0] = 20; // A dependency is absent from the direct package list.
+    assert(dest::attachable_spawn_set_hash(selection, 0) == dest::kAbsentSpawnSetHash);
+    selection.hasSpawnSetOverride = true;
+    selection.spawnSetOverride = spawn.value;
+    assert(dest::attachable_spawn_set_hash(selection, 0) == spawn.value);
+    selection.spawnSetOverride = dest::kAbsentSpawnSetHash;
+    assert(dest::attachable_spawn_set_hash(selection, 0) == dest::kAbsentSpawnSetHash);
+    selection.spawnSetOverride = 0;
+    assert(dest::attachable_spawn_set_hash(selection, 0) == 0);
+    selection.hasSpawnSetOverride = false;
+    spawn.activityPackages[0] = 10;
+    assert(dest::attachable_spawn_set_hash(selection, 0) == spawn.value);
+    spawn.activityPackages[0] = 20;
+    spawn.inMapPackage = 1;
+    assert(dest::attachable_spawn_set_hash(selection, 0) == spawn.value);
+    spawn.inMapPackage = 0;
+    available = false;
+    assert(dest::attachable_spawn_set_hash(selection, 0) == spawn.value);
+    std::cout << "Explicit spawn override precedence and inferred-set filtering passed\n";
+}