Selaa lähdekoodia

Remove the delay before Ember returns to orbit

Millie 2 päivää sitten
vanhempi
commit
ae0e169c44

+ 1 - 2
Sunrise/src/client/hooks/ember_movies/orbit_return.cpp

@@ -108,12 +108,11 @@ void poll(int region,int step) noexcept {
     if (ready) api.activity(&selected);
     const bool ember=exact && selected==link.binding.destination.activityIndex;
     const bool complete=exact && api.lifetime()==6;
-    const auto before=progress.stage;
     const auto action=progress.observe(GetTickCount64(),{exact,ember,selected==0,complete,ready,
         region,step,manager?field<int>(manager,0x3A0):-1,sameWorld});
-    if (before!=OrbitStage::banner && progress.stage==OrbitStage::banner) report("completion_accepted");
     switch(action) {
     case OrbitAction::select:
+        report("completion_accepted");
         if (select_orbit()) report("selection_queued");
         else { progress.stage=OrbitStage::finished;report("orbit_selection_unavailable"); }
         break;

+ 6 - 8
Sunrise/src/client/hooks/ember_movies/orbit_rules.h

@@ -2,7 +2,7 @@
 #include <cstdint>
 namespace sunrise::client::hooks::ember_movies {
 enum class OrbitAction { none, select, cleanup, orbitSetup, canceled, timedOut };
-enum class OrbitStage { idle, completion, banner, selection, leaving, finished };
+enum class OrbitStage { idle, completion, selection, leaving, finished };
 struct OrbitObservation {
     bool exactOwner{}, emberSelected{}, orbitSelected{}, complete{}, ready{};
     int region{-1}, step{-1}, pendingStep{-1};
@@ -10,20 +10,18 @@ struct OrbitObservation {
 };
 struct OrbitReturn {
     OrbitStage stage{OrbitStage::idle};
-    std::uint64_t began{}, bannerAt{};
-    void arm(std::uint64_t now) { stage=OrbitStage::completion; began=now; bannerAt=0; }
+    std::uint64_t began{};
+    void arm(std::uint64_t now) { stage=OrbitStage::completion; began=now; }
     bool active() const { return stage!=OrbitStage::idle && stage!=OrbitStage::finished; }
     OrbitAction observe(std::uint64_t now, const OrbitObservation& s) {
         if (!active()) return OrbitAction::none;
         auto end=[this](OrbitAction a) { stage=OrbitStage::finished; return a; };
         if (now-began>90000) return end(OrbitAction::timedOut);
-        if (stage==OrbitStage::completion || stage==OrbitStage::banner) {
+        if (stage==OrbitStage::completion) {
             if (!s.exactOwner || !s.emberSelected || s.region!=0 || s.step!=38)
                 return end(OrbitAction::canceled);
-            if (!s.complete) return OrbitAction::none;
-            if (stage==OrbitStage::completion) { stage=OrbitStage::banner; bannerAt=now; }
-            // Let the accepted mission-complete presentation show before leaving.
-            if (now-bannerAt<8000 || !s.ready || s.pendingStep!=38) return OrbitAction::none;
+            // Return on the first ready frame after completion, with no banner delay.
+            if (!s.complete || !s.ready || s.pendingStep!=38) return OrbitAction::none;
             stage=OrbitStage::selection;
             return OrbitAction::select;
         }

+ 2 - 2
docs/mission-ember-prerendered-ending.md

@@ -33,7 +33,7 @@ The native CPU movie-frame routine (`41D140`) is observed after its original rou
 
 Escape disables scorch and starts STM without selecting another mission state. Apex remains loaded while the native player owns video presentation. A bounded Lua timer reads playback status every 250 ms, so advancing the movies does not depend on a new client-state delta. Confirmed STM completion queues CNN. Only confirmed CNN completion sets `ember.complete`, phase 100, and native lifetime state 6. Gameplay route callbacks remain gated while the ending is active.
 
-After both movies complete for the same owner, `orbit_return` waits for native lifetime 6 to be applied. It allows eight seconds for the completion banner and queues the native return selection once. The request requires the same session, ActivityClient generation, Apex world arrival, and current Ember destination. Movie failure, completion of only STM, another activity, or a new mission run cannot start it.
+After both movies complete for the same owner, `orbit_return` queues the native return selection on the first ready frame with native lifetime 6 applied. There is no completion-banner delay. The request requires the same session, ActivityClient generation, Apex world arrival, and current Ember destination. Movie failure, completion of only STM, another activity, or a new mission run cannot start it.
 
 The return operations are recovered from native `E19D50`, also used by the UI path at `157727F`: initialize the 0x120-byte local descriptor with `BF84B0`, construct the default orbit selection with `C06330`, clear selections (`BF95D0`), select at priority 0 (`BFB1F0`), and commit reason 2 (`BF97D0`). Before any selection mutation, the descriptor must have reason 2 and activity index 0. A missing or non-orbit default is refused.
 
@@ -45,4 +45,4 @@ This removes the failing transition from the ending route. General world teardow
 
 All 23 portable tests and five Lua mission suites pass. Coverage rejects queue/preparation as completion, wrong assets, decoder errors, stale type-6 incidents, duplicate completion, and mission completion after only the first movie. It also checks that the ending requests no world selection and that native completion following a skip follows the same sequence. All playback, resource, surface-publication and UI signatures and relative-call targets match the saved executable image.
 
-The user confirmed both movie audio tracks in sequence and the mission-complete presentation on `6540584`, but reported black video with the HUD visible and no automatic return to orbit. The subsequent surface/HUD correction and automatic return require a new in-game test. Diagnostics use `ev=ember_movie` for playback and `ev=ember_orbit` with `awaiting_completion`, `completion_accepted`, `selection_queued`, optional `cleanup_requested`, and `orbit_setup`, or a specific cancellation/failure reason. Offline checks verify all native return signatures/call targets; portable cases cover completion/banner ordering, duplicate frames, retirement of the old link, native cleanup already in progress, stale ownership, world replacement and timeout.
+The user confirmed both movie audio tracks in sequence and the mission-complete presentation on `6540584`, but reported black video with the HUD visible and no automatic return to orbit. The subsequent surface/HUD correction and automatic return require a new in-game test. Diagnostics use `ev=ember_movie` for playback and `ev=ember_orbit` with `awaiting_completion`, `completion_accepted`, `selection_queued`, optional `cleanup_requested`, and `orbit_setup`, or a specific cancellation/failure reason. Offline checks verify all native return signatures/call targets; portable cases cover immediate return after completion, native readiness, duplicate frames, retirement of the old link, native cleanup already in progress, stale ownership, world replacement and timeout.

+ 1 - 1
scripts/mission_ember/ending.lua

@@ -31,7 +31,7 @@ return function(m)
                 c:set_variable("ember.ending", 3)
                 c:set_variable("ember.complete", true)
                 -- The movie bridge observes this native lifetime receipt after both movies,
-                -- allows the completion banner, then commits a guarded return to orbit.
+                -- then immediately commits a guarded return to orbit without a banner delay.
                 c.lifetime:set{state = c.sdk.lifetime_states:at(6)}
                 c:set_phase(100)
             end

+ 15 - 13
tests/ending_retirement_test.cpp

@@ -31,19 +31,17 @@ int main() {
     orbit.arm(1000);
     assert(orbit.observe(9000,inApex)==OA::none); // Host must actually publish completion.
     inApex.complete=true;
-    assert(orbit.observe(10000,inApex)==OA::none);
-    assert(orbit.observe(17999,inApex)==OA::none); // Full banner window.
-    assert(orbit.observe(18000,inApex)==OA::select);
-    assert(orbit.observe(18001,inApex)==OA::none); // No duplicate commit.
+    assert(orbit.observe(10000,inApex)==OA::select); // First completion frame: no banner delay.
+    assert(orbit.observe(10001,inApex)==OA::none); // No duplicate commit.
     auto inOrbit=inApex; inOrbit.emberSelected=false;inOrbit.orbitSelected=true;
     inOrbit.exactOwner=false; // Committing orbit may already retire the old ActivityClient link.
-    assert(orbit.observe(18002,inOrbit)==OA::cleanup); // Requires destination readback.
-    assert(orbit.observe(18003,inOrbit)==OA::none); // No repeated cleanup request.
+    assert(orbit.observe(10002,inOrbit)==OA::cleanup); // Requires destination readback.
+    assert(orbit.observe(10003,inOrbit)==OA::none); // No repeated cleanup request.
     movies::OrbitObservation betweenWorlds{};betweenWorlds.step=28;
-    assert(orbit.observe(18004,betweenWorlds)==OA::none);
+    assert(orbit.observe(10004,betweenWorlds)==OA::none);
     assert(orbit.active());
     inOrbit.step=29;
-    assert(orbit.observe(18100,inOrbit)==OA::orbitSetup);
+    assert(orbit.observe(10100,inOrbit)==OA::orbitSetup);
     assert(!orbit.active() && orbit.observe(20000,inApex)==OA::none);
     orbit.arm(30000);
     auto stale=inApex;stale.exactOwner=false;
@@ -54,15 +52,19 @@ int main() {
     orbit.arm(50000);inApex.complete=false;
     assert(orbit.observe(140001,inApex)==OA::timedOut);
     orbit.arm(150000);inApex.complete=true;
+    inApex.ready=false;
     assert(orbit.observe(150000,inApex)==OA::none);
-    assert(orbit.observe(158000,inApex)==OA::select);
-    assert(orbit.observe(158001,departed)==OA::none); // Native cleanup already began.
-    assert(orbit.observe(158100,inOrbit)==OA::orbitSetup);
+    inApex.ready=true;
+    assert(orbit.observe(150001,inApex)==OA::select); // No delay once native readiness returns.
+    assert(orbit.observe(150002,departed)==OA::none); // Native cleanup already began.
+    assert(orbit.observe(150100,inOrbit)==OA::orbitSetup);
     orbit.arm(160000);
+    inApex.pendingStep=28;
     assert(orbit.observe(160000,inApex)==OA::none);
-    assert(orbit.observe(168000,inApex)==OA::select);
+    inApex.pendingStep=38;
+    assert(orbit.observe(160001,inApex)==OA::select);
     auto replaced=inOrbit;replaced.step=38;replaced.sameWorld=false;
-    assert(orbit.observe(168001,replaced)==OA::canceled);
+    assert(orbit.observe(160002,replaced)==OA::canceled);
     movies::SurfaceRegistrations surfaces{};
     for (unsigned i=1;i<=6;++i) surfaces[i].entries[0]=movies::movie_surface_definitions[i-1];
     // Live black-video capture: old candidate handles remain, but no container