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:
parent
c2dc1a889c
commit
24569fd2a1
4 changed files with 90 additions and 26 deletions
|
|
@ -198,6 +198,8 @@ public sealed class PlayerMovementController
|
||||||
private bool _prevTurnLeftHeld;
|
private bool _prevTurnLeftHeld;
|
||||||
private bool _prevTurnRightHeld;
|
private bool _prevTurnRightHeld;
|
||||||
private bool _prevRunHeld;
|
private bool _prevRunHeld;
|
||||||
|
// TEMPORARY diagnostic clock (2026-07-03 moveto-stall investigation).
|
||||||
|
private float _lastAutowalkGateLogTime = float.MinValue;
|
||||||
|
|
||||||
// Heartbeat timer.
|
// Heartbeat timer.
|
||||||
// Cadence is 1.0 sec to match holtburger's
|
// Cadence is 1.0 sec to match holtburger's
|
||||||
|
|
@ -526,6 +528,22 @@ public sealed class PlayerMovementController
|
||||||
// slot drains.
|
// slot drains.
|
||||||
_motion.CheckForCompletedMotions?.Invoke();
|
_motion.CheckForCompletedMotions?.Invoke();
|
||||||
|
|
||||||
|
// TEMPORARY diagnostic (2026-07-03 moveto-stall investigation,
|
||||||
|
// strip when closed): every gate input the armed moveto's per-tick
|
||||||
|
// drive depends on, ~2 Hz while armed, ProbeAutoWalk-gated.
|
||||||
|
if (PhysicsDiagnostics.ProbeAutoWalkEnabled
|
||||||
|
&& MoveTo is { } mtDiag && mtDiag.IsMovingTo()
|
||||||
|
&& _simTimeSeconds - _lastAutowalkGateLogTime > 0.5f)
|
||||||
|
{
|
||||||
|
_lastAutowalkGateLogTime = _simTimeSeconds;
|
||||||
|
var pmParts = new System.Collections.Generic.List<string>();
|
||||||
|
foreach (var n in _motion.PendingMotions) pmParts.Add($"0x{n.Motion:X8}");
|
||||||
|
var nodeParts = new System.Collections.Generic.List<string>();
|
||||||
|
foreach (var n in mtDiag.PendingActions) nodeParts.Add(n.Type.ToString());
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[autowalk-gate] type={mtDiag.MovementTypeState} init={mtDiag.Initialized} tlid=0x{mtDiag.TopLevelObjectId:X8} contact={_body.InContact} motionsPending={_motion.MotionsPending()} pm=[{string.Join(",", pmParts)}] nodes=[{string.Join(",", nodeParts)}] curCmd=0x{mtDiag.CurrentCommand:X8} auxCmd=0x{mtDiag.AuxCommand:X8} fwd=0x{_motion.InterpretedState.ForwardCommand:X8} pos=({Position.X:F1},{Position.Y:F1}) yaw={Yaw:F2}"));
|
||||||
|
}
|
||||||
|
|
||||||
// Portal-space guard: while teleporting, no input is processed and
|
// Portal-space guard: while teleporting, no input is processed and
|
||||||
// no physics is resolved. Return a zero-movement result so the caller
|
// no physics is resolved. Return a zero-movement result so the caller
|
||||||
// can detect the frozen state (MotionStateChanged = false, no commands).
|
// can detect the frozen state (MotionStateChanged = false, no commands).
|
||||||
|
|
|
||||||
|
|
@ -7927,6 +7927,11 @@ public sealed class GameWindow : IDisposable
|
||||||
_playerMoveToTargetGuid,
|
_playerMoveToTargetGuid,
|
||||||
AcDream.Core.Physics.Motion.TargetStatus.Ok,
|
AcDream.Core.Physics.Motion.TargetStatus.Ok,
|
||||||
tp, tp));
|
tp, tp));
|
||||||
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[autowalk-feed] Ok guid=0x{_playerMoveToTargetGuid:X8} pos=({tpos.X:F1},{tpos.Y:F1}) init={playerMtm.Initialized}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -13058,6 +13063,35 @@ public sealed class GameWindow : IDisposable
|
||||||
int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f);
|
int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f);
|
||||||
pinitCellId = ((uint)plbX << 24) | ((uint)plbY << 16) | 0x0001u;
|
pinitCellId = ((uint)plbX << 24) | ((uint)plbY << 16) | 0x0001u;
|
||||||
}
|
}
|
||||||
|
// R4-V5 moveto-stall fix #2 (2026-07-03, the [autowalk-gate] probe's
|
||||||
|
// one-immortal-node finding): the sequencer/sink bind block MUST run
|
||||||
|
// BEFORE the initial SetPosition. SetPosition → StopCompletely
|
||||||
|
// enqueues the A9 pending_motions node whose completable partner is
|
||||||
|
// the DefaultSink's type-5 motion-table entry — with the sink still
|
||||||
|
// null at login, the node was ORPHANED, and pending_motions never
|
||||||
|
// reached empty again (head-pop-any just relabels a backlog), so
|
||||||
|
// the MoveToManager's wait-for-anims gate never opened: every
|
||||||
|
// server MoveTo armed but the body never moved.
|
||||||
|
if (_animatedEntities.TryGetValue(playerEntity.Id, out var playerAE)
|
||||||
|
&& playerAE.Sequencer is { } playerSeq)
|
||||||
|
{
|
||||||
|
_playerController.AttachCycleVelocityAccessor(() => playerSeq.CurrentVelocity);
|
||||||
|
// 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;
|
||||||
|
_playerController.Motion.InitializeMotionTables =
|
||||||
|
() => playerSeq.Manager.InitializeState();
|
||||||
|
_playerController.Motion.CheckForCompletedMotions =
|
||||||
|
playerSeq.Manager.CheckForCompletedMotions;
|
||||||
|
// R3-W6: the player's cycles are now driven through the SAME
|
||||||
|
// dispatch sink remotes use (TurnApplied/TurnStopped omitted —
|
||||||
|
// ObservedOmega is a remote-DR-only concept; local rotation is
|
||||||
|
// the controller's Yaw integration).
|
||||||
|
_playerController.Motion.DefaultSink =
|
||||||
|
new AcDream.Core.Physics.Motion.MotionTableDispatchSink(playerSeq);
|
||||||
|
}
|
||||||
|
|
||||||
var initResult = _physicsEngine.Resolve(
|
var initResult = _physicsEngine.Resolve(
|
||||||
playerEntity.Position, pinitCellId,
|
playerEntity.Position, pinitCellId,
|
||||||
System.Numerics.Vector3.Zero, 100f);
|
System.Numerics.Vector3.Zero, 100f);
|
||||||
|
|
@ -13072,27 +13106,6 @@ public sealed class GameWindow : IDisposable
|
||||||
playerEntity.SetPosition(initResult.Position);
|
playerEntity.SetPosition(initResult.Position);
|
||||||
playerEntity.ParentCellId = initResult.CellId;
|
playerEntity.ParentCellId = initResult.CellId;
|
||||||
|
|
||||||
if (_animatedEntities.TryGetValue(playerEntity.Id, out var playerAE)
|
|
||||||
&& playerAE.Sequencer is { } playerSeq)
|
|
||||||
{
|
|
||||||
_playerController.AttachCycleVelocityAccessor(() => playerSeq.CurrentVelocity);
|
|
||||||
// R3-W4: bind the player interp's retail seams to the player
|
|
||||||
// sequencer — LeaveGround/HitGround strip transition links here
|
|
||||||
// (the K-fix18 replacement); DefaultSink stays null until R3-W6
|
|
||||||
// (UpdatePlayerAnimation still drives the player's cycles).
|
|
||||||
_playerController.Motion.RemoveLinkAnimations = playerSeq.RemoveAllLinkAnimations;
|
|
||||||
_playerController.Motion.InitializeMotionTables =
|
|
||||||
() => playerSeq.Manager.InitializeState();
|
|
||||||
_playerController.Motion.CheckForCompletedMotions =
|
|
||||||
playerSeq.Manager.CheckForCompletedMotions;
|
|
||||||
// R3-W6: the player's cycles are now driven through the SAME
|
|
||||||
// dispatch sink remotes use (TurnApplied/TurnStopped omitted —
|
|
||||||
// ObservedOmega is a remote-DR-only concept; local rotation is
|
|
||||||
// the controller's Yaw integration).
|
|
||||||
_playerController.Motion.DefaultSink =
|
|
||||||
new AcDream.Core.Physics.Motion.MotionTableDispatchSink(playerSeq);
|
|
||||||
}
|
|
||||||
|
|
||||||
var q = playerEntity.Rotation;
|
var q = playerEntity.Rotation;
|
||||||
float rawYaw = MathF.Atan2(
|
float rawYaw = MathF.Atan2(
|
||||||
2f * (q.W * q.Z + q.X * q.Y),
|
2f * (q.W * q.Z + q.X * q.Y),
|
||||||
|
|
|
||||||
|
|
@ -114,14 +114,19 @@ public class PlayerMoveToCutoverTests
|
||||||
private static Rig MakeRig(out AnimationSequencer seqOut)
|
private static Rig MakeRig(out AnimationSequencer seqOut)
|
||||||
{
|
{
|
||||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
|
||||||
controller.Yaw = 0f; // heading 90 = facing +X
|
|
||||||
var seq = MakeSequencer();
|
var seq = MakeSequencer();
|
||||||
seqOut = seq;
|
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.DefaultSink = new MotionTableDispatchSink(seq);
|
||||||
controller.Motion.RemoveLinkAnimations = seq.RemoveAllLinkAnimations;
|
controller.Motion.RemoveLinkAnimations = seq.RemoveAllLinkAnimations;
|
||||||
controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState();
|
controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState();
|
||||||
controller.Motion.CheckForCompletedMotions = seq.Manager.CheckForCompletedMotions;
|
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 rig = new Rig { Controller = controller, MoveTo = null! };
|
||||||
var moveTo = new MoveToManager(
|
var moveTo = new MoveToManager(
|
||||||
|
|
@ -270,6 +275,31 @@ public class PlayerMoveToCutoverTests
|
||||||
Assert.True(result.MotionStateChanged);
|
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]
|
[Fact]
|
||||||
public void ServerMoveToPosition_ProductionCompletionFeed_WalksToArrival()
|
public void ServerMoveToPosition_ProductionCompletionFeed_WalksToArrival()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -95,15 +95,18 @@ public class W6EdgeDrivenMovementTests
|
||||||
private static PlayerMovementController MakeControllerWithRealSink(out AnimationSequencer seq)
|
private static PlayerMovementController MakeControllerWithRealSink(out AnimationSequencer seq)
|
||||||
{
|
{
|
||||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
|
||||||
controller.Yaw = 0f;
|
|
||||||
seq = MakeSequencer();
|
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);
|
controller.Motion.DefaultSink = new MotionTableDispatchSink(seq);
|
||||||
var s = seq;
|
var s = seq;
|
||||||
controller.Motion.RemoveLinkAnimations = s.RemoveAllLinkAnimations;
|
controller.Motion.RemoveLinkAnimations = s.RemoveAllLinkAnimations;
|
||||||
controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState();
|
controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState();
|
||||||
controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions;
|
controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions;
|
||||||
|
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||||
|
controller.Yaw = 0f;
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue