fix(R4-V5): moveto stall #2 - login SetPosition ran before the sink bind, orphaning one immortal pending_motions node

The c2dc1a88 wedge fix paired every StopCompletely A9 node with a
completable motion-table type-5 entry via DefaultSink?.StopCompletely()
- but EnterPlayerModeNow called the initial SetPosition (whose teleport
idle is StopCompletely, R3-W6) BEFORE the sequencer/DefaultSink bind
block, so the login A9 node dispatched against a NULL sink and stayed
an orphan. Head-pop-any semantics mean a queue with one orphan never
reaches empty again (later completions just relabel the backlog), so
MotionsPending stayed true at every UseTime and the MoveToManager's
retail wait-for-anims gate (BeginTurnToHeading) never opened: every
server MoveTo armed (movingTo=True, Initialized=True, tracker fed,
node plan built) and the body never moved.

Pinned live by the [autowalk-gate] probe: type=MoveToObject init=True
contact=True motionsPending=True pm=[0x41000003] nodes=[TurnToHeading,
MoveToPosition] curCmd=0 - identical every half-second, forever.

Fix: the sequencer/sink bind block in EnterPlayerModeNow moves ABOVE
the initial physics Resolve + SetPosition (no data dependency - the
block only needs playerEntity/_animatedEntities). Teleport-arrival
SetPositions were already safe (sink long bound). Test rigs
(PlayerMoveToCutoverTests.MakeRig, W6EdgeDrivenMovementTests) mirror
the fixed order, and a new LoginQueue_DrainsToEmpty_UnderProductionFeed
test pins the invariant the probe caught: after login, pending_motions
must reach EMPTY under the production completion feed.

The [autowalk-gate]/[autowalk-feed] diagnostics stay in for the live
verify pass (TEMPORARY-tagged; strip when #5 closes). Full suite
green: 3,958.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 14:35:44 +02:00
parent c2dc1a889c
commit 24569fd2a1
4 changed files with 90 additions and 26 deletions

View file

@ -114,14 +114,19 @@ public class PlayerMoveToCutoverTests
private static Rig MakeRig(out AnimationSequencer seqOut)
{
var controller = new PlayerMovementController(MakeFlatEngine());
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
controller.Yaw = 0f; // heading 90 = facing +X
var seq = MakeSequencer();
seqOut = seq;
// Production (EnterPlayerModeNow) order: the sink binds BEFORE the
// initial SetPosition — SetPosition → StopCompletely needs the sink
// for its type-5 motion-table dispatch, the completable partner of
// 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;
controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState();
controller.Motion.CheckForCompletedMotions = seq.Manager.CheckForCompletedMotions;
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
controller.Yaw = 0f; // heading 90 = facing +X
var rig = new Rig { Controller = controller, MoveTo = null! };
var moveTo = new MoveToManager(
@ -270,6 +275,31 @@ public class PlayerMoveToCutoverTests
Assert.True(result.MotionStateChanged);
}
[Fact]
public void LoginQueue_DrainsToEmpty_UnderProductionFeed()
{
// The second live stall (2026-07-03, [autowalk-gate] probe): ONE
// immortal Ready node in pending_motions — login SetPosition's
// StopCompletely fired before the sink bind, orphaning its A9 node
// (no completable partner). Head-pop-any relabels but never empties
// a queue with an orphan, and MotionsPending==true wedges every
// subsequent moveto. This pins the invariant: after login (rig
// construction, production bind order) the queue must reach EMPTY
// under the production completion feed.
var rig = MakeRig(out var seq);
var c = rig.Controller;
seq.MotionDoneTarget = (m, ok) => c.Motion.MotionDone(m, ok);
for (int f = 0; f < 300 && c.Motion.MotionsPending(); f++)
{
c.Update(1f / 60f, new MovementInput());
seq.Advance(1f / 60f);
}
Assert.False(c.Motion.MotionsPending(),
"login pending_motions must drain to empty under the production feed");
}
[Fact]
public void ServerMoveToPosition_ProductionCompletionFeed_WalksToArrival()
{

View file

@ -95,15 +95,18 @@ public class W6EdgeDrivenMovementTests
private static PlayerMovementController MakeControllerWithRealSink(out AnimationSequencer seq)
{
var controller = new PlayerMovementController(MakeFlatEngine());
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
controller.Yaw = 0f;
seq = MakeSequencer();
// The full W6 GameWindow bind set (EnterPlayerModeNow equivalent).
// The full W6 GameWindow bind set (EnterPlayerModeNow equivalent) —
// sink binds BEFORE SetPosition, matching the R4-V5 stall-fix order
// (SetPosition → StopCompletely needs the sink for its type-5
// dispatch, else its A9 pending_motions node is orphaned).
controller.Motion.DefaultSink = new MotionTableDispatchSink(seq);
var s = seq;
controller.Motion.RemoveLinkAnimations = s.RemoveAllLinkAnimations;
controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState();
controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions;
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
controller.Yaw = 0f;
return controller;
}