diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs
index 61530c2a..d60c82a2 100644
--- a/src/AcDream.App/Input/PlayerMovementController.cs
+++ b/src/AcDream.App/Input/PlayerMovementController.cs
@@ -344,6 +344,19 @@ public sealed class PlayerMovementController
/// Update — heading reads/writes go through Yaw, not this).
internal Quaternion BodyOrientation => _body.Orientation;
+ /// R4-V5 wedge fix — the P1 unpack store
+ /// (CPhysics::SetObjectMovement @00509730:
+ /// last_move_was_autonomous = arg7, 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.
+ internal void SetLastMoveWasAutonomous(bool autonomous)
+ => _body.LastMoveWasAutonomous = autonomous;
+
///
/// Wire the player's AnimationSequencer current cycle velocity into
/// . When attached,
@@ -497,6 +510,22 @@ public sealed class PlayerMovementController
// §3 placement decision).
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
// no physics is resolved. Return a zero-movement result so the caller
// can detect the frozen state (MotionStateChanged = false, no commands).
@@ -582,6 +611,20 @@ public sealed class PlayerMovementController
else if (!input.TurnRight && !input.TurnLeft
&& (_prevTurnRightHeld || _prevTurnLeftHeld))
{ _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;
@@ -655,16 +698,16 @@ public sealed class PlayerMovementController
// that adjust_motion ran above. The hand-mirrored formulas are gone
// (register TS-22 retired).
var stateVel = _motion.get_state_velocity();
- // R3-W6: autonomous=true flags input-driven motion (the default
- // false silently cleared LastMoveWasAutonomous and flipped the
- // A3 dual dispatch to the interpreted branch). R4-V5: while the
- // MoveToManager is driving, the last movement EVENT was the
- // server's non-autonomous moveto — retail stores the wire
- // autonomous byte on the unpack path (P1 pin,
- // last_move_was_autonomous), so the per-frame stamp must not
- // overwrite that with true mid-moveto.
- bool autonomousMove = MoveTo is null || !MoveTo.IsMovingTo();
- _body.set_local_velocity(new Vector3(stateVel.X, stateVel.Y, savedWorldVz), autonomous: autonomousMove);
+ // R4-V5 wedge fix: PRESERVE the flag — retail's
+ // last_move_was_autonomous is stored at EVENT boundaries only
+ // (input DoMotion/StopMotion edges above, the P1 wire-unpack
+ // store, LeaveGround's own autonomous=1 write), never re-stamped
+ // by the per-tick velocity write. V5's first cut stamped it per
+ // frame here, which mis-routed the pump's A3 dual dispatch on
+ // event-transition frames (apply_raw clobbering a just-armed
+ // moveto's dispatched motion with the raw Ready state).
+ _body.set_local_velocity(new Vector3(stateVel.X, stateVel.Y, savedWorldVz),
+ autonomous: _body.LastMoveWasAutonomous);
}
// ── 3. Jump (charged) ─────────────────────────────────────────────────
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 2ac789d0..85354ccf 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -4690,6 +4690,19 @@ public sealed class GameWindow : IDisposable
// R5/MovementManager scope — V0-pins.md P1 adjacent seam).
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.UnstickFromObject?.Invoke();
@@ -12336,6 +12349,13 @@ public sealed class GameWindow : IDisposable
? AcDream.Core.Physics.MovementType.TurnToObject
: 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);
}
diff --git a/src/AcDream.Core/Physics/InterpretedMotionFunnel.cs b/src/AcDream.Core/Physics/InterpretedMotionFunnel.cs
index f276013d..8c0dea7e 100644
--- a/src/AcDream.Core/Physics/InterpretedMotionFunnel.cs
+++ b/src/AcDream.Core/Physics/InterpretedMotionFunnel.cs
@@ -56,6 +56,22 @@ public interface IInterpretedMotionSink
/// — retail's CPhysicsObj::StopInterpretedMotion also returns a
/// result that gates the post-stop add_to_queue + state-removal.
bool StopMotion(uint motion);
+
+ ///
+ /// R4-V5 wedge fix — retail CPhysicsObj::StopCompletely_Internal
+ /// (0x0050ead0) → CPartArray::StopCompletelyInternal (0x00518890)
+ /// → MotionTableManager::PerformMovement(MovementStruct{type=5}):
+ /// the ANIMATION-side full stop dispatched from the middle of
+ /// CMotionInterp::StopCompletely (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 StopCompletely enqueues right after this
+ /// call — without it the A9 node is an orphan and MotionsPending
+ /// never drains at idle (the 2026-07-03 moveto wedge). Default
+ /// implementation returns true (the null-sink "no animation
+ /// backend" posture — bare tests and sink-less interps are unaffected).
+ ///
+ bool StopCompletely() => true;
}
///
diff --git a/src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs b/src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs
index 55546fa1..8ffb1990 100644
--- a/src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs
+++ b/src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs
@@ -69,4 +69,21 @@ public sealed class MotionTableDispatchSink : IInterpretedMotionSink
uint result = _sequencer.PerformMovement(MotionTableMovement.StopInterpreted(motion, 1f));
return result == MotionTableManagerError.Success;
}
+
+ ///
+ /// R4-V5 wedge fix — retail CPartArray::StopCompletelyInternal
+ /// (0x00518890): MotionTableManager::PerformMovement(type 5).
+ /// 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
+ /// 's turn-class branch.
+ ///
+ public bool StopCompletely()
+ {
+ TurnStopped?.Invoke();
+
+ uint result = _sequencer.PerformMovement(MotionTableMovement.StopCompletely());
+ return result == MotionTableManagerError.Success;
+ }
}
diff --git a/src/AcDream.Core/Physics/MotionInterpreter.cs b/src/AcDream.Core/Physics/MotionInterpreter.cs
index a0048089..fd6fed65 100644
--- a/src/AcDream.Core/Physics/MotionInterpreter.cs
+++ b/src/AcDream.Core/Physics/MotionInterpreter.cs
@@ -1221,7 +1221,28 @@ public sealed class MotionInterpreter : IMotionDoneSink
InterpretedState.SideStepCommand = 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);
AddToQueue(contextId: 0, MotionCommand.Ready, (uint)jumpSnapshot);
diff --git a/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs
index 22952dad..5ed5d54d 100644
--- a/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs
+++ b/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs
@@ -109,12 +109,15 @@ public class PlayerMoveToCutoverTests
/// The EnterPlayerModeNow bind set, verbatim shape: real sink,
/// Yaw-authoritative heading seams (P5 bridge), Contact bit, false
/// isInterpolating, SimTimeSeconds clock, TS-36 interrupt binding.
- private static Rig MakeRig()
+ private static Rig MakeRig() => MakeRig(out _);
+
+ 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;
controller.Motion.DefaultSink = new MotionTableDispatchSink(seq);
controller.Motion.RemoveLinkAnimations = seq.RemoveAllLinkAnimations;
controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState();
@@ -168,6 +171,10 @@ public class PlayerMoveToCutoverTests
var c = rig.Controller;
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
{
Type = MovementType.MoveToPosition,
@@ -204,6 +211,10 @@ public class PlayerMoveToCutoverTests
var rig = MakeRig();
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
{
Type = MovementType.TurnToHeading,
@@ -232,6 +243,10 @@ public class PlayerMoveToCutoverTests
var rig = MakeRig();
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
{
Type = MovementType.MoveToPosition,
@@ -255,12 +270,69 @@ public class PlayerMoveToCutoverTests
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]
public void JumpRelease_CancelsMoveTo()
{
var rig = MakeRig();
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
{
Type = MovementType.MoveToPosition,