From b54555da62d09db4cfd5e74b6e59dced84c170b1 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 5 Jul 2026 16:33:41 +0200 Subject: [PATCH] fix #174: RemoveLinkAnimations seam is HandleEnterWorld (strip + drain) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retail CPhysicsObj::RemoveLinkAnimations (0x0050fe20) is a tailcall to CPartArray::HandleEnterWorld (0x00517d70) -> MotionTableManager:: HandleEnterWorld (0x0051bdd0): remove_all_link_animations PLUS a full pending_animations drain (while (head) AnimationDone(0)), each pop relaying MotionDone so CMotionInterp pops its pending_motions node in lockstep. acdream bound the seam to the bare sequence strip, so every jump's LeaveGround removed the animations that queued manager nodes were counting down on — orphaning them (NumAnims>0, anims gone) and permanently damming BOTH queues. MotionsPending() then never drained (probe round: last player pending=False at the first MovementJump press; old jump motions still completing at rest minutes later) and BeginTurnToHeading/BeginMoveForward's verbatim motions_pending gates starved every armed moveto: ACE's mt-6 walk-to-door armed but the body never walked (wire-proven, seqs 98-101); the close-range use turn never completed so the deferred action was silently eaten. Doors only worked on a fresh session (shallow queue). Rebind both production sites (remote EnsureRemoteMotionBindings + the player's EnterPlayerModeNow block) to Manager.HandleEnterWorld(); the sequencer wrapper was a pure passthrough so the manager call is a strict superset. All six interp seam sites (LeaveGround, HitGround, Dead, and the detached-object guards) are the same retail chain. Harness mirrors updated; pins: Issue174LinkStripDrainTests (the seam drains both queues; fresh motions queue and complete after). Suites: Core 2535 / App 713 / UI 425 / Net 385 green. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 19 +++- src/AcDream.App/Rendering/GameWindow.cs | 24 ++++- .../Input/PlayerMoveToCutoverTests.cs | 4 +- .../Input/W6EdgeDrivenMovementTests.cs | 4 +- .../Motion/Issue174LinkStripDrainTests.cs | 97 +++++++++++++++++++ .../Motion/RemoteChaseEndToEndHarnessTests.cs | 5 +- 6 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 068448a7..a7b1e78a 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,9 +46,24 @@ Copy this block when adding a new issue: --- -## #174 — Door Use dies after the first attempt: close-range deferred uses silently swallowed (player MoveToManager never completes) +## #174 — Door Use dies after the first jump: the RemoveLinkAnimations seam stripped animations without retail's queue drain -**Status:** OPEN — root under investigation (wire + log evidence in hand; needs one `ACDREAM_PROBE_AUTOWALK=1` round to pick between the two candidate mechanisms) +**Status:** 🟡 FIX SHIPPED 2026-07-05 (same session) — pending user gate (jump around, then use the Facility Hub door from close AND from ~3 m). +**FIX:** the `MotionInterpreter.RemoveLinkAnimations` seam is retail +`CPhysicsObj::RemoveLinkAnimations` 0x0050fe20 — a tailcall to +`CPartArray::HandleEnterWorld` 0x00517d70 → `MotionTableManager:: +HandleEnterWorld` 0x0051bdd0: strip the sequence's link animations AND drain +`pending_animations` completely (each pop relays MotionDone → the interp pops +its `pending_motions` node in lockstep). acdream bound the seam to the BARE +sequence strip (`RemoveAllLinkAnimations`), so every jump's LeaveGround +removed the animations that queued manager nodes were counting down on — +orphaning them and permanently damming BOTH queues; `MotionsPending()` then +starved every armed moveto (the far-range walk-to-door and the close-range +use turn — both door faces below). Rebound at both production sites +(GameWindow remote bindings + the player's EnterPlayerModeNow block) to +`Manager.HandleEnterWorld()`; harness mirrors updated; pins +`Issue174LinkStripDrainTests` (seam drains both queues; new motions queue + +complete after). The `UseDone` (0x01C7) display gap stays open below. **Severity:** HIGH (can't open doors reliably → blocks the #137 door acceptance + normal play) **Filed:** 2026-07-05 **Component:** interaction — B.4b Use pipeline / AP-23 speculative moveto deferral (R5-V5 facade) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d470d95a..cd90737a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -4288,7 +4288,20 @@ public sealed class GameWindow : IDisposable rmForSink.ObservedOmega = System.Numerics.Vector3.Zero, }; rm.Motion.DefaultSink = rm.Sink; - rm.Motion.RemoveLinkAnimations = ae.Sequencer.RemoveAllLinkAnimations; + // #174 (2026-07-05): the RemoveLinkAnimations seam is retail + // CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to + // CPartArray::HandleEnterWorld 0x00517d70 → + // MotionTableManager::HandleEnterWorld 0x0051bdd0: strip the + // sequence's link animations AND drain pending_animations + // completely (each pop fires MotionDone → CMotionInterp pops its + // pending_motions node in lockstep). The previous binding was the + // bare sequence strip — every LeaveGround (jump) stripped the + // animations that queued manager nodes were counting down on, + // orphaning them (NumAnims>0, anims gone) and permanently damming + // BOTH queues: MotionsPending() never drained again, so every + // armed moveto (ACE's walk-to-door mt-6, the close-range use turn) + // starved — the "door only works on a fresh session" bug. + rm.Motion.RemoveLinkAnimations = () => ae.Sequencer.Manager.HandleEnterWorld(); rm.Motion.InitializeMotionTables = () => ae.Sequencer.Manager.InitializeState(); // R3-W5: the per-op zero-tick flush (CPhysicsObj::CheckForCompletedMotions // 0x0050fe30 -> MotionTableManager.CheckForCompletedMotions). @@ -13697,7 +13710,14 @@ public sealed class GameWindow : IDisposable // R3-W4: bind the player interp's retail seams to the player // sequencer — LeaveGround/HitGround strip transition links here // (the K-fix18 replacement). - _playerController.Motion.RemoveLinkAnimations = playerSeq.RemoveAllLinkAnimations; + // #174 (2026-07-05): the seam is HandleEnterWorld (strip + FULL + // queue drain), not the bare sequence strip — see the remote + // bind site's comment (retail 0x0050fe20 → 0x00517d70 → + // 0x0051bdd0). The bare strip orphaned every pending manager + // node on each jump's LeaveGround; the local player's + // pending_motions then never drained and every armed moveto + // starved (#174: doors unusable after the first jump/run). + _playerController.Motion.RemoveLinkAnimations = () => playerSeq.Manager.HandleEnterWorld(); _playerController.Motion.InitializeMotionTables = () => playerSeq.Manager.InitializeState(); _playerController.Motion.CheckForCompletedMotions = diff --git a/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs index e5146d20..1a32b33d 100644 --- a/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs +++ b/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs @@ -122,7 +122,9 @@ public class PlayerMoveToCutoverTests // the A9 pending_motions node (a null sink orphans it and the // MoveToManager wait-for-anims gate never opens — the live stall). controller.Motion.DefaultSink = new MotionTableDispatchSink(seq); - controller.Motion.RemoveLinkAnimations = seq.RemoveAllLinkAnimations; + // #174: production wiring — HandleEnterWorld (strip + drain), not + // the bare sequence strip (which orphaned pending manager nodes). + controller.Motion.RemoveLinkAnimations = () => seq.Manager.HandleEnterWorld(); controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState(); controller.Motion.CheckForCompletedMotions = seq.Manager.CheckForCompletedMotions; controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); diff --git a/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs b/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs index 3198da4a..aa80104e 100644 --- a/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs +++ b/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs @@ -103,7 +103,9 @@ public class W6EdgeDrivenMovementTests // dispatch, else its A9 pending_motions node is orphaned). controller.Motion.DefaultSink = new MotionTableDispatchSink(seq); var s = seq; - controller.Motion.RemoveLinkAnimations = s.RemoveAllLinkAnimations; + // #174: production wiring — HandleEnterWorld (strip + drain), not + // the bare sequence strip (which orphaned pending manager nodes). + controller.Motion.RemoveLinkAnimations = () => s.Manager.HandleEnterWorld(); controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState(); controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions; controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); diff --git a/tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs b/tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs new file mode 100644 index 00000000..dadba9a3 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs @@ -0,0 +1,97 @@ +using AcDream.Core.Physics; +using AcDream.Core.Physics.Motion; +using Xunit; +using Xunit.Abstractions; + +namespace AcDream.Core.Tests.Physics.Motion; + +// ───────────────────────────────────────────────────────────────────────────── +// #174 pin (2026-07-05): the RemoveLinkAnimations seam must be retail +// CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to +// CPartArray::HandleEnterWorld 0x00517d70 → MotionTableManager:: +// HandleEnterWorld 0x0051bdd0: CSequence::remove_all_link_animations PLUS a +// full pending_animations drain (`while (head) AnimationDone(0)`), each pop +// relaying MotionDone → CMotionInterp pops its pending_motions node in +// lockstep. +// +// The pre-fix binding was the bare sequence strip: every LeaveGround (jump) +// removed the link animations that queued MotionTableManager nodes were +// counting down on, orphaning them (NumAnims > 0, animations gone). Both +// queues then dammed permanently — MotionsPending() never drained at rest — +// and BeginTurnToHeading/BeginMoveForward (retail 0x00529b90 motions_pending +// gate) starved every armed moveto: ACE's walk-to-door mt-6 armed but the +// body never walked; the close-range Use turn never completed so the +// deferred action was silently eaten. Live evidence: launch-174-autowalk.log +// (last player pending=False at the first MovementJump press; old jump +// motions still completing at rest minutes later). +// ───────────────────────────────────────────────────────────────────────────── +public class Issue174LinkStripDrainTests +{ + private readonly ITestOutputHelper _out; + public Issue174LinkStripDrainTests(ITestOutputHelper output) => _out = output; + + /// + /// The jam repro: queue motions (link + cycle nodes land in BOTH the + /// interp's pending_motions and the manager's pending_animations), then + /// fire the LeaveGround-side seam. With the retail HandleEnterWorld + /// binding both queues drain to empty; the pre-fix bare-strip binding + /// left both non-empty forever. + /// + [Fact] + public void RemoveLinkAnimationsSeam_DrainsBothQueues() + { + var h = new RemoteChaseHarness(_out); + + // Drive a motion burst — walk, run, stop — the shape a player's + // pre-jump input produces. Each successful dispatch pairs an interp + // node with a manager node. + var p = new MovementParameters(); + h.Interp.DoMotion(MotionCommand.WalkForward, p); + h.Interp.set_hold_run(true, interrupt: false); + h.Interp.StopMotion(MotionCommand.WalkForward, p); + + Assert.True(h.Interp.MotionsPending(), + "precondition: the burst must leave pending interp nodes"); + Assert.NotEmpty(h.Seq.Manager.PendingAnimations); + + // The LeaveGround seam (retail CMotionInterp::LeaveGround 0x00528b00 + // fires CPhysicsObj::RemoveLinkAnimations). + h.Interp.RemoveLinkAnimations!.Invoke(); + + Assert.False(h.Interp.MotionsPending(), + "HandleEnterWorld's drain must pop every pending interp node " + + "(retail: each AnimationDone(0) relays MotionDone)"); + Assert.Empty(h.Seq.Manager.PendingAnimations); + } + + /// + /// The post-jump livability pin: after the seam fires mid-activity, a + /// NEW moveto-style dispatch must be able to queue and complete — the + /// #174 symptom was that BeginTurnToHeading's motions_pending gate never + /// re-opened after a jump, permanently starving armed movetos. + /// + [Fact] + public void AfterSeamDrain_NewMotionsQueueAndComplete() + { + var h = new RemoteChaseHarness(_out); + var p = new MovementParameters(); + + // Pre-jump activity, then the jump's LeaveGround strip+drain. + h.Interp.DoMotion(MotionCommand.WalkForward, p); + h.Interp.RemoveLinkAnimations!.Invoke(); + Assert.False(h.Interp.MotionsPending()); + + // A fresh dispatch (the armed moveto's turn) queues... + h.Interp.DoMotion(MotionCommand.TurnRight, p); + Assert.True(h.Interp.MotionsPending()); + + // ...and the normal completion path (the manager's queue feeding + // MotionDone) drains it — the gate re-opens. + while (h.Seq.Manager.PendingAnimations.GetEnumerator() is var e && e.MoveNext()) + h.Seq.Manager.AnimationDone(success: true); + + h.Seq.Manager.CheckForCompletedMotions(); + Assert.False(h.Interp.MotionsPending(), + "the normal AnimationDone → MotionDone chain must drain the new node"); + } +} diff --git a/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs b/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs index 1c113938..a1480435 100644 --- a/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs +++ b/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs @@ -174,7 +174,10 @@ internal sealed class RemoteChaseHarness TurnStopped = () => ObservedOmega = Vector3.Zero, }; Interp.DefaultSink = Sink; - Interp.RemoveLinkAnimations = Seq.RemoveAllLinkAnimations; + // #174: production binds the seam to Manager.HandleEnterWorld + // (strip + full queue drain, retail 0x0050fe20 → 0x0051bdd0) — + // the bare sequence strip orphaned pending manager nodes. + Interp.RemoveLinkAnimations = () => Seq.Manager.HandleEnterWorld(); Interp.InitializeMotionTables = () => Seq.Manager.InitializeState(); Interp.CheckForCompletedMotions = Seq.Manager.CheckForCompletedMotions;