fix(R4-V5): moveto wedge - StopCompletely's missing animation dispatch orphaned pending_motions; + the P1 autonomous store family
Live bug (2026-07-03, user door test): a server MoveTo armed (movingTo=True) but the local body never moved; the retail observer saw ACE walk the server-side player to the door; the next local input reasserted the stale position (rubber-band). Log evidence: the player's pending_motions queue was non-empty on 92/94 MotionDone pops (remotes: 0/40) - MoveToManager's retail wait-for-anims gate (BeginTurnToHeading's MotionsPending check) never opened. ROOT CAUSE (found via a production-faithful repro test - the original suite force-drained the queue, masking it): the R3 port MISIDENTIFIED retail's StopCompletely_Internal body. It is NOT a physics velocity zero - CPhysicsObj::StopCompletely_Internal (0x0050ead0) tailcalls CPartArray::StopCompletelyInternal (0x00518890) which is MotionTableManager::PerformMovement(type 5): an ANIMATION-side full stop whose UNCONDITIONALLY-queued pending_animations entry is the completable partner of the A9 pending_motions node CMotionInterp::StopCompletely enqueues (raw @00527e90 between the state writes and add_to_queue). Without the dispatch, every StopCompletely (login/teleport SetPosition + every MoveToManager PerformMovement head) left an orphan node with no completion - the queue never drained at idle. (The R3 code comment claimed a register row for the stand-in; none was ever added - porting the real mechanism closes that violation without a row.) Fix set (all retail-anchored): - IInterpretedMotionSink gains StopCompletely() (default true = the null-sink posture); MotionTableDispatchSink routes it to the R2 manager's already-ported type-5 op (StopObjectCompletely + Ready-sentinel entry) + TurnStopped for the ObservedOmega seam. - MotionInterpreter.StopCompletely calls DefaultSink?.StopCompletely() at the exact retail slot (after the state writes, before add_to_queue); the immediate set_velocity(Zero) is kept as the documented physics-effect stand-in. - PlayerMovementController ticks CheckForCompletedMotions after UseTime - retail's per-tick CPartArray::HandleMovement slot (0x00517d60 = MotionTableManager::UseTime = CheckForCompletedMotions tailcall, called every tick from UpdateObjectInternal @005159a4). NOTE: a first-cut per-tick apply_current_movement pump was tried and REVERTED - retail's apply callers are all event-driven (hold-key, HitGround/LeaveGround, ReportExhaustion), not per-tick; the remote block's per-tick apply is a pre-existing adaptation outside this fix's scope. - The P1 autonomous-store family, completing the pin's port shape (the gate landed in V5; the STORE side was missing): (a) the unpack store (00509730: last_move_was_autonomous = wire byte) in the GameWindow player branch - local player only for now (remote interps have no WeenieObj, which A3 reads as IsThePlayer; storing a remote's autonomous byte would mis-route their per-tick apply onto the raw branch); (b) input edges store =1 at the controller edge block - retail's CPhysicsObj::DoMotion @00510030 / StopMotion @005100e0 / TakeControlFromServer @006b32f4 input-boundary stores; (c) section 2's per-frame velocity write now PRESERVES the flag (V5's first cut stamped it per frame, the wrong altitude - retail stores it at event boundaries only); (d) InstallSpeculativeTurnToTarget stores false (models the wire mt-6's unpack store; part of the AP-23 adaptation) so the event-driven applies (e.g. HitGround mid-moveto) route interpreted and cannot clobber the manager's dispatched motions with idle raw state. Tests: new ServerMoveToPosition_ProductionCompletionFeed_WalksToArrival drives the controller under the PRODUCTION completion feed (sequencer.Advance -> MotionDoneTarget) instead of force-draining - this is the test that reproduced the live wedge (queue stuck at the two A9 Ready orphans) and now proves the drain. Existing cutover tests mirror the wire-path autonomous store before PerformMovement. Full suite green: 3,957. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b87726dc2c
commit
c2dc1a889c
6 changed files with 201 additions and 12 deletions
|
|
@ -344,6 +344,19 @@ public sealed class PlayerMovementController
|
||||||
/// Update — heading reads/writes go through Yaw, not this).</summary>
|
/// Update — heading reads/writes go through Yaw, not this).</summary>
|
||||||
internal Quaternion BodyOrientation => _body.Orientation;
|
internal Quaternion BodyOrientation => _body.Orientation;
|
||||||
|
|
||||||
|
/// <summary>R4-V5 wedge fix — the P1 unpack store
|
||||||
|
/// (<c>CPhysics::SetObjectMovement</c> @00509730:
|
||||||
|
/// <c>last_move_was_autonomous = arg7</c>, written on the unpack path
|
||||||
|
/// for every applied movement event). GameWindow's local-player 0xF74C
|
||||||
|
/// branch stores the wire autonomous byte here (always false — the P1
|
||||||
|
/// gate drops autonomous echoes); the speculative use install stores
|
||||||
|
/// false too (it models the wire mt-6 ACE sends moments later). Routes
|
||||||
|
/// the per-tick pump's A3 dual dispatch to the INTERPRETED branch
|
||||||
|
/// during a server moveto so apply_raw can't clobber the manager's
|
||||||
|
/// dispatched motions with the idle raw state.</summary>
|
||||||
|
internal void SetLastMoveWasAutonomous(bool autonomous)
|
||||||
|
=> _body.LastMoveWasAutonomous = autonomous;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wire the player's AnimationSequencer current cycle velocity into
|
/// Wire the player's AnimationSequencer current cycle velocity into
|
||||||
/// <see cref="MotionInterpreter.GetCycleVelocity"/>. When attached,
|
/// <see cref="MotionInterpreter.GetCycleVelocity"/>. When attached,
|
||||||
|
|
@ -497,6 +510,22 @@ public sealed class PlayerMovementController
|
||||||
// §3 placement decision).
|
// §3 placement decision).
|
||||||
MoveTo?.UseTime();
|
MoveTo?.UseTime();
|
||||||
|
|
||||||
|
// R4-V5 wedge fix (2026-07-03 live door bug), retail's per-tick
|
||||||
|
// completion slot: CPartArray::HandleMovement — a tailcall chain to
|
||||||
|
// MotionTableManager::CheckForCompletedMotions (0x00517d60 →
|
||||||
|
// 0x0051bfd0) — runs every tick right AFTER MovementManager::UseTime
|
||||||
|
// in UpdateObjectInternal (@005159a4). It flushes pending_animations
|
||||||
|
// entries whose animations already ran out so their AnimationDone →
|
||||||
|
// MotionDone pops land each tick even when no op fired that frame.
|
||||||
|
// (apply_current_movement is NOT part of the per-tick order — its
|
||||||
|
// retail callers are all event-driven: hold-key toggles, HitGround/
|
||||||
|
// LeaveGround, ReportExhaustion.) Full tick ordering remains R6
|
||||||
|
// scope; the companion fix is StopCompletely's previously-missing
|
||||||
|
// StopCompletely_Internal animation dispatch (see
|
||||||
|
// MotionInterpreter.StopCompletely), whose completable entry this
|
||||||
|
// slot drains.
|
||||||
|
_motion.CheckForCompletedMotions?.Invoke();
|
||||||
|
|
||||||
// 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).
|
||||||
|
|
@ -582,6 +611,20 @@ public sealed class PlayerMovementController
|
||||||
else if (!input.TurnRight && !input.TurnLeft
|
else if (!input.TurnRight && !input.TurnLeft
|
||||||
&& (_prevTurnRightHeld || _prevTurnLeftHeld))
|
&& (_prevTurnRightHeld || _prevTurnLeftHeld))
|
||||||
{ _motion.StopMotion(MotionCommand.TurnRight, p); motionEdgeFired = true; }
|
{ _motion.StopMotion(MotionCommand.TurnRight, p); motionEdgeFired = true; }
|
||||||
|
|
||||||
|
// Retail stores last_move_was_autonomous = 1 at the CPhysicsObj
|
||||||
|
// INPUT boundary — CPhysicsObj::DoMotion @00510030 /
|
||||||
|
// CPhysicsObj::StopMotion @005100e0 (per call, before routing
|
||||||
|
// to MovementManager) and CommandInterpreter::
|
||||||
|
// TakeControlFromServer @006b32f4. The MoveToManager's
|
||||||
|
// _DoMotion goes through CMotionInterp internals and NEVER
|
||||||
|
// touches the flag; the wire unpack path stores the wire byte
|
||||||
|
// (P1, 00509730). This edge block IS acdream's input boundary,
|
||||||
|
// so an edge firing is exactly retail's store site. The flag
|
||||||
|
// routes the per-tick pump's A3 dual dispatch (raw for
|
||||||
|
// input-driven motion, interpreted for server/manager-driven).
|
||||||
|
if (motionEdgeFired)
|
||||||
|
_body.LastMoveWasAutonomous = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_prevForwardHeld = input.Forward;
|
_prevForwardHeld = input.Forward;
|
||||||
|
|
@ -655,16 +698,16 @@ public sealed class PlayerMovementController
|
||||||
// that adjust_motion ran above. The hand-mirrored formulas are gone
|
// that adjust_motion ran above. The hand-mirrored formulas are gone
|
||||||
// (register TS-22 retired).
|
// (register TS-22 retired).
|
||||||
var stateVel = _motion.get_state_velocity();
|
var stateVel = _motion.get_state_velocity();
|
||||||
// R3-W6: autonomous=true flags input-driven motion (the default
|
// R4-V5 wedge fix: PRESERVE the flag — retail's
|
||||||
// false silently cleared LastMoveWasAutonomous and flipped the
|
// last_move_was_autonomous is stored at EVENT boundaries only
|
||||||
// A3 dual dispatch to the interpreted branch). R4-V5: while the
|
// (input DoMotion/StopMotion edges above, the P1 wire-unpack
|
||||||
// MoveToManager is driving, the last movement EVENT was the
|
// store, LeaveGround's own autonomous=1 write), never re-stamped
|
||||||
// server's non-autonomous moveto — retail stores the wire
|
// by the per-tick velocity write. V5's first cut stamped it per
|
||||||
// autonomous byte on the unpack path (P1 pin,
|
// frame here, which mis-routed the pump's A3 dual dispatch on
|
||||||
// last_move_was_autonomous), so the per-frame stamp must not
|
// event-transition frames (apply_raw clobbering a just-armed
|
||||||
// overwrite that with true mid-moveto.
|
// moveto's dispatched motion with the raw Ready state).
|
||||||
bool autonomousMove = MoveTo is null || !MoveTo.IsMovingTo();
|
_body.set_local_velocity(new Vector3(stateVel.X, stateVel.Y, savedWorldVz),
|
||||||
_body.set_local_velocity(new Vector3(stateVel.X, stateVel.Y, savedWorldVz), autonomous: autonomousMove);
|
autonomous: _body.LastMoveWasAutonomous);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Jump (charged) ─────────────────────────────────────────────────
|
// ── 3. Jump (charged) ─────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -4690,6 +4690,19 @@ public sealed class GameWindow : IDisposable
|
||||||
// R5/MovementManager scope — V0-pins.md P1 adjacent seam).
|
// R5/MovementManager scope — V0-pins.md P1 adjacent seam).
|
||||||
if (_playerController is not null)
|
if (_playerController is not null)
|
||||||
{
|
{
|
||||||
|
// P1 tail (00509730): the unpack path stores the wire
|
||||||
|
// autonomous byte BEFORE unpack_movement — always false
|
||||||
|
// here (the gate above dropped autonomous events). This
|
||||||
|
// is what routes the controller's per-tick pump (A3
|
||||||
|
// dual dispatch) to the INTERPRETED branch during a
|
||||||
|
// server moveto. LOCAL PLAYER ONLY for now: remotes'
|
||||||
|
// interps have no WeenieObj, which A3 treats as
|
||||||
|
// IsThePlayer — storing a remote player's autonomous
|
||||||
|
// byte would flip their per-tick apply onto the raw
|
||||||
|
// branch and clobber their funnel state; the remote
|
||||||
|
// store lands with real remote weenies (R5+).
|
||||||
|
_playerController.SetLastMoveWasAutonomous(update.IsAutonomous);
|
||||||
|
|
||||||
_playerController.Motion.InterruptCurrentMovement?.Invoke();
|
_playerController.Motion.InterruptCurrentMovement?.Invoke();
|
||||||
_playerController.Motion.UnstickFromObject?.Invoke();
|
_playerController.Motion.UnstickFromObject?.Invoke();
|
||||||
|
|
||||||
|
|
@ -12336,6 +12349,13 @@ public sealed class GameWindow : IDisposable
|
||||||
? AcDream.Core.Physics.MovementType.TurnToObject
|
? AcDream.Core.Physics.MovementType.TurnToObject
|
||||||
: AcDream.Core.Physics.MovementType.MoveToObject,
|
: AcDream.Core.Physics.MovementType.MoveToObject,
|
||||||
};
|
};
|
||||||
|
// Part of this install's AP-23 adaptation: store the autonomy flag
|
||||||
|
// exactly as the wire mt-6 this install anticipates would (the P1
|
||||||
|
// unpack store, IsAutonomous=false) — without it the per-tick
|
||||||
|
// pump's A3 dispatch stays on the raw branch (the user's last input
|
||||||
|
// set it autonomous) and clobbers this moveto's dispatched motions
|
||||||
|
// with the idle raw state.
|
||||||
|
_playerController.SetLastMoveWasAutonomous(false);
|
||||||
playerMoveTo.PerformMovement(ms);
|
playerMoveTo.PerformMovement(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,22 @@ public interface IInterpretedMotionSink
|
||||||
/// — retail's <c>CPhysicsObj::StopInterpretedMotion</c> also returns a
|
/// — retail's <c>CPhysicsObj::StopInterpretedMotion</c> also returns a
|
||||||
/// result that gates the post-stop <c>add_to_queue</c> + state-removal.</returns>
|
/// result that gates the post-stop <c>add_to_queue</c> + state-removal.</returns>
|
||||||
bool StopMotion(uint motion);
|
bool StopMotion(uint motion);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R4-V5 wedge fix — retail <c>CPhysicsObj::StopCompletely_Internal</c>
|
||||||
|
/// (0x0050ead0) → <c>CPartArray::StopCompletelyInternal</c> (0x00518890)
|
||||||
|
/// → <c>MotionTableManager::PerformMovement(MovementStruct{type=5})</c>:
|
||||||
|
/// the ANIMATION-side full stop dispatched from the middle of
|
||||||
|
/// <c>CMotionInterp::StopCompletely</c> (raw @00527e90). The manager
|
||||||
|
/// queues its pending_animations entry UNCONDITIONALLY for type 5, and
|
||||||
|
/// that entry's AnimationDone → MotionDone is the matched pop for the A9
|
||||||
|
/// pending_motions node <c>StopCompletely</c> enqueues right after this
|
||||||
|
/// call — without it the A9 node is an orphan and <c>MotionsPending</c>
|
||||||
|
/// never drains at idle (the 2026-07-03 moveto wedge). Default
|
||||||
|
/// implementation returns <c>true</c> (the null-sink "no animation
|
||||||
|
/// backend" posture — bare tests and sink-less interps are unaffected).
|
||||||
|
/// </summary>
|
||||||
|
bool StopCompletely() => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -69,4 +69,21 @@ public sealed class MotionTableDispatchSink : IInterpretedMotionSink
|
||||||
uint result = _sequencer.PerformMovement(MotionTableMovement.StopInterpreted(motion, 1f));
|
uint result = _sequencer.PerformMovement(MotionTableMovement.StopInterpreted(motion, 1f));
|
||||||
return result == MotionTableManagerError.Success;
|
return result == MotionTableManagerError.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R4-V5 wedge fix — retail <c>CPartArray::StopCompletelyInternal</c>
|
||||||
|
/// (0x00518890): <c>MotionTableManager::PerformMovement(type 5)</c>.
|
||||||
|
/// The manager stops everything and queues its Ready-sentinel
|
||||||
|
/// pending_animations entry UNCONDITIONALLY — the completable partner
|
||||||
|
/// of the interp's A9 pending_motions node. A full stop ends any turn
|
||||||
|
/// cycle, so the ObservedOmega seam is notified like
|
||||||
|
/// <see cref="StopMotion"/>'s turn-class branch.
|
||||||
|
/// </summary>
|
||||||
|
public bool StopCompletely()
|
||||||
|
{
|
||||||
|
TurnStopped?.Invoke();
|
||||||
|
|
||||||
|
uint result = _sequencer.PerformMovement(MotionTableMovement.StopCompletely());
|
||||||
|
return result == MotionTableManagerError.Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1221,7 +1221,28 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
InterpretedState.SideStepCommand = 0;
|
InterpretedState.SideStepCommand = 0;
|
||||||
InterpretedState.TurnCommand = 0;
|
InterpretedState.TurnCommand = 0;
|
||||||
|
|
||||||
// StopCompletely_Internal (0x0050f5a0) stand-in.
|
// StopCompletely_Internal — R4-V5 wedge-fix CORRECTION of the R3
|
||||||
|
// misread: retail's body (0x0050ead0, tailcall
|
||||||
|
// CPartArray::StopCompletelyInternal 0x00518890) is
|
||||||
|
// MotionTableManager::PerformMovement(type 5) through the ANIMATION
|
||||||
|
// stack — NOT a physics-side velocity zero (the "0x0050f5a0" the R3
|
||||||
|
// note cited is a different function's interior). The manager
|
||||||
|
// queues its type-5 pending_animations entry UNCONDITIONALLY, and
|
||||||
|
// that entry's AnimationDone → MotionDone is the matched pop for
|
||||||
|
// the A9 pending_motions node queued below. Without this dispatch
|
||||||
|
// every StopCompletely (login/teleport SetPosition + every
|
||||||
|
// MoveToManager PerformMovement head) left an orphan node,
|
||||||
|
// MotionsPending never drained at idle, and BeginTurnToHeading's
|
||||||
|
// wait-for-anims gate wedged every armed moveto (the 2026-07-03
|
||||||
|
// live door bug — server walked the player, the local body never
|
||||||
|
// moved, rubber-band on the next input).
|
||||||
|
DefaultSink?.StopCompletely();
|
||||||
|
|
||||||
|
// Immediate velocity zero: kept from the R3 stand-in. Retail
|
||||||
|
// reaches zero through the Ready state's next velocity apply;
|
||||||
|
// acdream's controller altitude performs that apply in its grounded
|
||||||
|
// section-2 write, so the immediate zero preserves the established
|
||||||
|
// teleport/stop behavior for same-frame readers.
|
||||||
PhysicsObj.set_velocity(Vector3.Zero);
|
PhysicsObj.set_velocity(Vector3.Zero);
|
||||||
|
|
||||||
AddToQueue(contextId: 0, MotionCommand.Ready, (uint)jumpSnapshot);
|
AddToQueue(contextId: 0, MotionCommand.Ready, (uint)jumpSnapshot);
|
||||||
|
|
|
||||||
|
|
@ -109,12 +109,15 @@ public class PlayerMoveToCutoverTests
|
||||||
/// <summary>The EnterPlayerModeNow bind set, verbatim shape: real sink,
|
/// <summary>The EnterPlayerModeNow bind set, verbatim shape: real sink,
|
||||||
/// Yaw-authoritative heading seams (P5 bridge), Contact bit, false
|
/// Yaw-authoritative heading seams (P5 bridge), Contact bit, false
|
||||||
/// isInterpolating, SimTimeSeconds clock, TS-36 interrupt binding.</summary>
|
/// isInterpolating, SimTimeSeconds clock, TS-36 interrupt binding.</summary>
|
||||||
private static Rig MakeRig()
|
private static Rig MakeRig() => MakeRig(out _);
|
||||||
|
|
||||||
|
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.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||||
controller.Yaw = 0f; // heading 90 = facing +X
|
controller.Yaw = 0f; // heading 90 = facing +X
|
||||||
var seq = MakeSequencer();
|
var seq = MakeSequencer();
|
||||||
|
seqOut = seq;
|
||||||
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();
|
||||||
|
|
@ -168,6 +171,10 @@ public class PlayerMoveToCutoverTests
|
||||||
var c = rig.Controller;
|
var c = rig.Controller;
|
||||||
var dest = new Vector3(104f, 96f, 50f); // 8 m dead ahead (heading 90)
|
var dest = new Vector3(104f, 96f, 50f); // 8 m dead ahead (heading 90)
|
||||||
|
|
||||||
|
// Mirrors the GameWindow wire path's P1 unpack store (00509730):
|
||||||
|
// a server moveto arrives with IsAutonomous=false, routing the
|
||||||
|
// per-tick pump's A3 dual dispatch to the interpreted branch.
|
||||||
|
rig.Controller.SetLastMoveWasAutonomous(false);
|
||||||
rig.MoveTo.PerformMovement(new MovementStruct
|
rig.MoveTo.PerformMovement(new MovementStruct
|
||||||
{
|
{
|
||||||
Type = MovementType.MoveToPosition,
|
Type = MovementType.MoveToPosition,
|
||||||
|
|
@ -204,6 +211,10 @@ public class PlayerMoveToCutoverTests
|
||||||
var rig = MakeRig();
|
var rig = MakeRig();
|
||||||
var c = rig.Controller;
|
var c = rig.Controller;
|
||||||
|
|
||||||
|
// Mirrors the GameWindow wire path's P1 unpack store (00509730):
|
||||||
|
// a server moveto arrives with IsAutonomous=false, routing the
|
||||||
|
// per-tick pump's A3 dual dispatch to the interpreted branch.
|
||||||
|
rig.Controller.SetLastMoveWasAutonomous(false);
|
||||||
rig.MoveTo.PerformMovement(new MovementStruct
|
rig.MoveTo.PerformMovement(new MovementStruct
|
||||||
{
|
{
|
||||||
Type = MovementType.TurnToHeading,
|
Type = MovementType.TurnToHeading,
|
||||||
|
|
@ -232,6 +243,10 @@ public class PlayerMoveToCutoverTests
|
||||||
var rig = MakeRig();
|
var rig = MakeRig();
|
||||||
var c = rig.Controller;
|
var c = rig.Controller;
|
||||||
|
|
||||||
|
// Mirrors the GameWindow wire path's P1 unpack store (00509730):
|
||||||
|
// a server moveto arrives with IsAutonomous=false, routing the
|
||||||
|
// per-tick pump's A3 dual dispatch to the interpreted branch.
|
||||||
|
rig.Controller.SetLastMoveWasAutonomous(false);
|
||||||
rig.MoveTo.PerformMovement(new MovementStruct
|
rig.MoveTo.PerformMovement(new MovementStruct
|
||||||
{
|
{
|
||||||
Type = MovementType.MoveToPosition,
|
Type = MovementType.MoveToPosition,
|
||||||
|
|
@ -255,12 +270,69 @@ public class PlayerMoveToCutoverTests
|
||||||
Assert.True(result.MotionStateChanged);
|
Assert.True(result.MotionStateChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServerMoveToPosition_ProductionCompletionFeed_WalksToArrival()
|
||||||
|
{
|
||||||
|
// The 2026-07-03 live wedge repro: same scenario as
|
||||||
|
// ServerMoveToPosition_WalksToArrival_WithNoUserInput, but the
|
||||||
|
// pending_motions queue drains the way PRODUCTION drains it — the
|
||||||
|
// sequencer's Advance() fires MotionDoneTarget → Motion.MotionDone
|
||||||
|
// (the TickAnimations R3-W2 bind) — instead of this suite's
|
||||||
|
// force-drain. Live symptom: the moveto armed (movingTo=True) but
|
||||||
|
// the body never moved; BeginTurnToHeading's wait-for-anims gate
|
||||||
|
// (MotionsPending) never opened because the player's queue never
|
||||||
|
// emptied (launch.log: pending=True on 92/94 player MOTIONDONE
|
||||||
|
// pops; remotes 0/40).
|
||||||
|
var rig = MakeRig(out var seq);
|
||||||
|
var c = rig.Controller;
|
||||||
|
seq.MotionDoneTarget = (m, ok) => c.Motion.MotionDone(m, ok);
|
||||||
|
var dest = new Vector3(104f, 96f, 50f); // 8 m dead ahead (heading 90)
|
||||||
|
|
||||||
|
// A few idle frames first — production always has render ticks
|
||||||
|
// between login (SetPosition → StopCompletely queues the A9 node
|
||||||
|
// with NO dispatch) and the first Use.
|
||||||
|
for (int f = 0; f < 30; f++)
|
||||||
|
{
|
||||||
|
c.Update(1f / 60f, new MovementInput());
|
||||||
|
seq.Advance(1f / 60f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors the GameWindow wire path's P1 unpack store (00509730):
|
||||||
|
// a server moveto arrives with IsAutonomous=false, routing the
|
||||||
|
// per-tick pump's A3 dual dispatch to the interpreted branch.
|
||||||
|
rig.Controller.SetLastMoveWasAutonomous(false);
|
||||||
|
rig.MoveTo.PerformMovement(new MovementStruct
|
||||||
|
{
|
||||||
|
Type = MovementType.MoveToPosition,
|
||||||
|
Pos = new Position(c.CellId, dest, Quaternion.Identity),
|
||||||
|
Params = new MovementParameters { UseSpheres = false, DistanceToObject = 0.6f },
|
||||||
|
});
|
||||||
|
Assert.True(rig.MoveTo.IsMovingTo());
|
||||||
|
|
||||||
|
for (int f = 0; f < 1200 && rig.MoveTo.IsMovingTo(); f++)
|
||||||
|
{
|
||||||
|
c.Update(1f / 60f, new MovementInput());
|
||||||
|
seq.Advance(1f / 60f);
|
||||||
|
}
|
||||||
|
|
||||||
|
string queueDump = string.Join(", ",
|
||||||
|
System.Linq.Enumerable.Select(c.Motion.PendingMotions, n => $"0x{n.Motion:X8}"));
|
||||||
|
Assert.False(rig.MoveTo.IsMovingTo(),
|
||||||
|
$"moveto wedged: pending_motions never drained under the production "
|
||||||
|
+ $"completion feed (queue: [{queueDump}])");
|
||||||
|
Assert.Equal(1, rig.MoveToCompleteCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void JumpRelease_CancelsMoveTo()
|
public void JumpRelease_CancelsMoveTo()
|
||||||
{
|
{
|
||||||
var rig = MakeRig();
|
var rig = MakeRig();
|
||||||
var c = rig.Controller;
|
var c = rig.Controller;
|
||||||
|
|
||||||
|
// Mirrors the GameWindow wire path's P1 unpack store (00509730):
|
||||||
|
// a server moveto arrives with IsAutonomous=false, routing the
|
||||||
|
// per-tick pump's A3 dual dispatch to the interpreted branch.
|
||||||
|
rig.Controller.SetLastMoveWasAutonomous(false);
|
||||||
rig.MoveTo.PerformMovement(new MovementStruct
|
rig.MoveTo.PerformMovement(new MovementStruct
|
||||||
{
|
{
|
||||||
Type = MovementType.MoveToPosition,
|
Type = MovementType.MoveToPosition,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue