fix #174: RemoveLinkAnimations seam is HandleEnterWorld (strip + drain)
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 <noreply@anthropic.com>
This commit is contained in:
parent
62b6fa52e8
commit
b54555da62
6 changed files with 146 additions and 7 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue