Explorar o código

Correct ending teleport guard, beam states and climb scorch filter

Ending. The intra-slice-set guard read reported_slice_set(), which holds the
newest D6 host teleport: still region 64 from the mission's opening move, since
every later area was reached on a z-leg. The guard never matched, the teleport
was armed with slice-set index 1 carrying slice set 0's name hash, and the run
logged the transition start with no "Finished synchronizing" before hitching in
network_send. The slice set the client holds is now derived from the region it
reports, rounded down to the slice-set factor. That run also confirms the roster
half of the earlier fix: no seed refusal was logged at any point.

Beam. Toggling the placed ring objects removed the weapon's structure instead of
dimming it, so the beam cycled between absent and present and finished the
mission gone. The three objects are now placed once and never deactivated, and
the beam has three authored states driven by its two devices: powered off but
still installed once the cell is in, powered and running as the baseline, and
powered and driven open as the surge before an exposure.

Climb scorch. The apex-object hot-pipe filter was pointed at globals-object
volumes. It published cleanly but attached nothing. The filter that lives in the
same object as those volumes is the pairing that delivered contact damage before,
so the scorch uses it; only the effect resource differs from the original.
Millie hai 3 días
pai
achega
05b15d7423

+ 15 - 6
Sunrise/src/server/activity/mission/mission_script_runtime_dispatch.cpp

@@ -5,6 +5,7 @@
 #include <span>
 #include <string_view>
 
+#include "../../../middleware/content/packages/tables/scenario_reader.h"
 #include "../../../state/activity/membership/activity_membership_query.h"
 #include "../../../state/build_data/runtime.h"
 #include "../../../state/build_data/spawn_sets/spawn_set_catalog.h"
@@ -214,12 +215,20 @@ void arm_state_region_teleport(RuntimeInstance& instance,
     }
     // A region is `sliceSetIndex + stateOrdinal`, so a sibling state sits in the slice set the
     // client already holds and there is nothing to transition to. Arming anyway hands the client
-    // a slice-set index that is not a slice set (region 1 with slice set 0's name hash), and it
-    // starts a teleportation it can never finish. Clear the arm and let the roster publish the
-    // new state's groups into the world that is already standing.
-    const std::int32_t heldSliceSet =
-        membership::reported_slice_set(instance.view.binding.sessionId);
-    if (heldSliceSet >= 0 && heldSliceSet == static_cast<std::int32_t>(plan.sliceSetIndex)) {
+    // a slice-set index that is not a slice set (region 1 carrying slice set 0's name hash), and
+    // it starts a teleportation it can never finish: the client logs the transition and never
+    // reaches "Finished synchronizing". Clear the arm and let the roster publish the new state's
+    // groups into the world that is already standing.
+    //
+    // The slice set the client holds is derived from the region it reports, not from
+    // `reported_slice_set()`: that reads the newest D6 host teleport, which still named the
+    // mission's opening move to region 64 long after later areas were reached on z-legs.
+    // Slice-set indices are multiples of the factor, so a region's own slice set is that region
+    // rounded down to it.
+    const auto factor = middleware::content::packages::tables::kSliceSetIndexFactor;
+    if (reported >= 0
+        && static_cast<std::uint32_t>(reported) - (static_cast<std::uint32_t>(reported) % factor)
+               == plan.sliceSetIndex) {
         static_cast<void>(membership::arm_host_teleport(
             instance.view.binding.sessionId, membership::kAbsentSliceSetIndex, 0));
         log_line(core::log::Level::info, &instance, "state_region", "teleport_not_required");

+ 42 - 31
scripts/mission_ember/apex.lua

@@ -50,24 +50,27 @@ return function(m, a, ending)
         end
         a.device(c, "REACTOR_SHIELD_DEVICE", open, snap)
     end
-    -- The three placed ring objects carry the beam's own authored effect graphs; the two
-    -- devices are its mechanical lanes. Holding all three objects active for the whole fight
-    -- is what made the beam look constant, so the surge is those authored objects coming up
-    -- with their devices at the warning and going dark again at recovery.
-    -- The core is the weapon's own body and stays present; the laser and ring are what
-    -- brighten, so only those two are modulated.
-    local ring_objects = {"SPECOPS_APEX_RING_LASER_OBJECT", "SPECOPS_APEX_RING_RING_OBJECT"}
-    local function beam(c, s, active, snap)
+    -- The three placed ring objects ARE the weapon's structure and stay active for the whole
+    -- mission: deactivating them removes the beam and everything built around it, rather than
+    -- dimming it. The two devices are its lanes, so the beam has three authored states:
+    --   off   -- present but dead: powered off, the pose the weapon holds once the cell is in.
+    --   idle  -- powered and running, the beam's baseline between exposures.
+    --   surge -- powered and driven open, the intensification before the shutters expose a target.
+    local ring_objects = {"SPECOPS_APEX_RING_LASER_OBJECT", "SPECOPS_APEX_RING_CORE_OBJECT",
+        "SPECOPS_APEX_RING_RING_OBJECT"}
+    local BEAM_OFF, BEAM_IDLE, BEAM_SURGE = 0, 1, 2
+    local function beam(c, s, mode, snap)
         -- Idempotent: the cycle re-asserts the same pose every step, and each redundant
         -- publication would spend intents from the callback's native budget.
-        if s:variable("ember.apex.beam") == active then return end
-        c:set_variable("ember.apex.beam", active)
-        a.objects(c, ring_objects, active)
-        a.device(c, "SPECOPS_APEX_RING_LASER_DEVICE", active, snap)
-        a.device(c, "SPECOPS_APEX_RING_RING_DEVICE", active, snap)
-        -- FX power is independent of mechanical position; pulse both authored graphs.
-        lane(c, "SPECOPS_APEX_RING_LASER_DEVICE", active and "power_on" or "power_off", snap)
-        lane(c, "SPECOPS_APEX_RING_RING_DEVICE", active and "power_on" or "power_off", snap)
+        if s:variable("ember.apex.beam") == mode then return end
+        c:set_variable("ember.apex.beam", mode)
+        local powered = mode ~= BEAM_OFF
+        for _, name in ipairs({"SPECOPS_APEX_RING_LASER_DEVICE", "SPECOPS_APEX_RING_RING_DEVICE"}) do
+            a.device(c, name, mode == BEAM_SURGE, snap)
+            -- FX power is independent of mechanical position, so the weapon can stay
+            -- installed and lit, installed and dark, or driven wide open.
+            lane(c, name, powered and "power_on" or "power_off", snap)
+        end
     end
     -- `REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON` and `FOUNDRY_THERMAL_DOT_HOP_ON` both reference
     -- effect resource 80C1D9E0 -- the burn already working in the Foundry. The hot-pipe and
@@ -78,10 +81,16 @@ return function(m, a, ending)
     local function rail_filter(c) return {players = true, inside = a.slot(c, "SLOT_019E")} end
     local function hazards(c, s, mode)
         if mode == "climb" then
-            -- The five narrow authored pipe volumes on the way up to the deposit. Slot 7 is a
-            -- broad kill volume well below the walkable route and is deliberately not a pipe.
+            -- The five narrow authored pipe volumes on the way up to the deposit; their heights
+            -- track the climb the mother-brain dialogue volumes walk through, from z~172 at
+            -- x=-395 up to z~186 at x=-460. Slot 7 is a broad kill volume well below the
+            -- walkable route and is deliberately not a pipe.
+            --
+            -- The filter is the one that lives in the same object as those volumes. Pointing the
+            -- apex-object filter at globals-object volumes published cleanly but attached
+            -- nothing; this is the pairing that demonstrably delivered contact damage before.
             a.effect(c, s, "REACTOR_COFFIN_INTERIOR_THERMAL_HOP_ON",
-                "REACTOR_MOTHER_BRAIN_HOT_PIPES_OBJECT_FILTER_80B3C21C",
+                "REACTOR_MOTHER_BRAIN_HOT_PIPES_OBJECT_FILTER_80B3C09F",
                 {players = true, inside_any = {
                     a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_02_TRIGGER_VOLUME"),
                     a.slot(c, "REACTOR_MOTHER_BRAIN_HOT_PIPES_03_TRIGGER_VOLUME"),
@@ -123,14 +132,14 @@ return function(m, a, ending)
         c:set_variable("ember.apex.vent_step", step)
         c:set_variable("ember.apex.vents_open", step == "open")
         if step == "warning" then
-            beam(c, s, true)
+            beam(c, s, BEAM_SURGE)
             if phase(s) == 3 then
                 for _, side in ipairs(sides) do
                     if not dead(s, side) then a.slot(c, "REACTOR_CLAMSHELL_" .. side .. "_ALARM_SEQUENCE"):play_sequence{} end
                 end
             else a.slot(c, "REACTOR_COFFIN_ALARM_SEQUENCE"):play_sequence{} end
         else
-            if step == "closed" then beam(c, s, false, snap) end
+            if step == "closed" then beam(c, s, BEAM_IDLE, snap) end
             if phase(s) == 3 then
                 for _, side in ipairs(sides) do if not dead(s, side) then doors(c, side, step == "open", snap) end end
             elseif phase(s) == 4 then
@@ -152,7 +161,7 @@ return function(m, a, ending)
         lane(c, "MOTHER_BRAIN_DOOR_DEVICE", "lock")
         unlock(c, "SPECOPS_APEX_RING_LASER_DEVICE")
         unlock(c, "SPECOPS_APEX_RING_RING_DEVICE")
-        beam(c, s, false, true)
+        beam(c, s, BEAM_IDLE, true)
     end
     local function start_reactor(c, s)
         if phase(s) >= 3 then return end
@@ -178,10 +187,10 @@ return function(m, a, ending)
             a.device(c, "MOTHER_BRAIN_ENGINE_RIGHT_DEVICE", true)
             a.device(c, "REACTOR_GETAWAY_SHIP_DEVICE", true)
             a.objects(c, {"REACTOR_GETAWAY_SHIP_OBJECT", "SUNBURN_DAMAGE_OBJECT"}, true)
-            -- The weapon is dead once the cell is in: the beam shuts down and stays down,
-            -- including across an escape checkpoint restart. The previous code opened both
-            -- devices here, which left it running through the whole escape.
-            beam(c, s, false, true)
+            -- The weapon is dead once the cell is in: powered off but still installed, so
+            -- the beam and everything built around it stay in the world. The previous code
+            -- opened both devices here, which left it running through the whole escape.
+            beam(c, s, BEAM_OFF, true)
             c:cancel_timer(vent_timer(s))
             a.scene(c, "MOTHER_BRAIN_HOLE_EXPLOSION_SCENE")
             arm_escape(c)
@@ -199,8 +208,9 @@ return function(m, a, ending)
         a.device(c, "ACCESS_DOOR_OUTER_DEVICE", true)
         a.device(c, "SECURITY_DOOR_DEVICE", false, true)
         a.slot(c, "SECURITY_PLACED_INTERCEPTOR_OBJECT"):set_interactable_object{generation = 1}
-        -- Only the core is held on here; `beam()` owns the laser and ring so they can surge.
-        a.objects(c, {"SPECOPS_APEX_RING_CORE_OBJECT"}, true)
+        -- The weapon's structure is placed once and stays for the whole mission; `beam()`
+        -- only changes its power and drive.
+        a.objects(c, ring_objects, true)
         for _, side in ipairs(sides) do
             doors(c, side, false, true)
             a.device(c, "CLAMSHELL_PIPES_" .. side .. "_DEVICE", false, true)
@@ -279,8 +289,9 @@ return function(m, a, ending)
             set(c, 5)
             c:cancel_timer(vent_timer(s)); c:cancel_timer("ember.apex.explain." .. generation(s))
             coffin_doors(c, true)
-            -- The weapon keeps firing until the cell goes in, so the beam stays up here.
-            beam(c, s, true)
+            -- The weapon keeps running until the cell goes in; the exposure cycle is over,
+            -- so it settles back to its baseline rather than holding the surge.
+            beam(c, s, BEAM_IDLE)
             -- Arm the climb scorch now: the pipes burn while the cell is carried up.
             c:start_timer("ember.apex.hazards", 1)
             unlock(c, "MOTHER_BRAIN_DOOR_DEVICE")
@@ -363,7 +374,7 @@ return function(m, a, ending)
             set(c, 6)
             -- The weapon is already dead at this checkpoint: restoring it must leave the
             -- beam off and re-arm the escape's own progress triggers.
-            beam(c, s, false, true)
+            beam(c, s, BEAM_OFF, true)
             arm_escape(c)
             a.darkness(c, s, true)
         else

+ 13 - 3
tests/mission_ember_routes_test.lua

@@ -258,6 +258,9 @@ timer('ember.apex.explain.');assert(vars['ember.r.cue.41'] and vars['ember.music
 assert(timers['ember.apex.vents.1']==14000)
 timer('ember.apex.vents.') -- Surge precedes any target exposure.
 assert(vars['ember.apex.vent_step']=='warning' and timers['ember.apex.vents.1']==6000)
+-- The baseline is a running weapon, not a dark one, so the surge is a drive change.
+assert(vars['ember.apex.beam']==2,'warning must surge the beam')
+assert(transition('SPECOPS_APEX_RING_LASER_DEVICE', true).transition=='open')
 assert(transition('SPECOPS_APEX_RING_LASER_DEVICE').transition=='power_on')
 assert(transition('REACTOR_CLAMSHELL_EAST_DOOR_A_DEVICE').transition=='close')
 timer('ember.apex.vents.')
@@ -339,8 +342,15 @@ for i=#calls,1,-1 do
     if calls[i][1]=='set_object_filter' then escapeFilter=calls[i][2];break end
 end
 assert(escapeFilter=='aod_reactor_rail_top_object_filter','escape hazard must use the rail top')
--- The weapon is dead once the cell is in: the beam goes down and stays down.
-assert(vars['ember.apex.beam']==false,'beam must shut down after the deposit')
+-- The weapon is dead once the cell is in: powered off, but still installed. Deactivating the
+-- ring objects would take the beam and its surrounding structure out of the world entirely.
+assert(vars['ember.apex.beam']==0,'beam must be powered off after the deposit')
+local ringRemoved=false
+for _,row in ipairs(calls)do
+    if row[1]=='set_object_active' and row[3] and row[3].active==false
+        and tostring(row[2]):find('specops_apex_ring',1,true) then ringRemoved=true end
+end
+assert(not ringRemoved,'the weapon structure must never be deactivated')
 -- Each authored explosion set is armed so it can fire as the player reaches it.
 for _,set in ipairs({'A','B','C','D'})do
     local name='ember_apex_explosion_sequence_prefab.explosion_set_'..set:lower()..'_player_trigger'
@@ -352,7 +362,7 @@ for _,set in ipairs({'A','B','C','D'})do
 end
 reset_check('escape',function()
     assert(vars['ember.apex.phase']==6 and vars['ember.apex.dead.COFFIN'])
-    assert(vars['ember.apex.beam']==false,'escape restart must leave the beam off')
+    assert(vars['ember.apex.beam']==0,'escape restart must leave the beam powered off')
 end)
 trigger('APEX_DIRECTIVE_REACTOR_RAILS_ESCAPE_PLAYER_TRIGGER')
 -- Regions 0, 1 and 2 are sibling states of one slice set, so the client never reports a new