diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index ce5bf927..3484c7f0 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -4442,6 +4442,54 @@ public sealed class GameWindow : IDisposable
return false;
}
+ ///
+ /// R4-V5 (extracted from the V4 NPC tick block, now shared with the
+ /// grounded player-remote pipeline — the glide fix): the P4
+ /// TargetTracker delivery + the per-tick MoveToManager drive. Feeds
+ /// HandleUpdateTarget(Ok) from the live entity table when the
+ /// tracked target moved beyond the voyeur radius (ExitWorld on
+ /// despawn), then runs UseTime() — steering, arrival,
+ /// fail-distance. Safe for any remote: a manager with no armed moveto
+ /// no-ops, and airborne bodies are held by UseTime's contact gate.
+ ///
+ private void TickRemoteMoveTo(RemoteMotion rm)
+ {
+ if (rm.MoveTo is not { } mtm)
+ return;
+
+ if (rm.TrackedTargetGuid != 0)
+ {
+ if (_entitiesByServerGuid.TryGetValue(rm.TrackedTargetGuid, out var trackedEnt))
+ {
+ var tpos = trackedEnt.Position;
+ if (!rm.TrackedTargetFedOnce
+ || System.Numerics.Vector3.Distance(tpos, rm.TrackedTargetLastFedPos)
+ > rm.TrackedTargetRadius)
+ {
+ rm.TrackedTargetFedOnce = true;
+ rm.TrackedTargetLastFedPos = tpos;
+ var tp = new AcDream.Core.Physics.Position(
+ rm.CellId, tpos, System.Numerics.Quaternion.Identity);
+ mtm.HandleUpdateTarget(new AcDream.Core.Physics.Motion.TargetInfo(
+ rm.TrackedTargetGuid,
+ AcDream.Core.Physics.Motion.TargetStatus.Ok,
+ tp, tp));
+ }
+ }
+ else
+ {
+ var lp = new AcDream.Core.Physics.Position(
+ rm.CellId, rm.TrackedTargetLastFedPos,
+ System.Numerics.Quaternion.Identity);
+ mtm.HandleUpdateTarget(new AcDream.Core.Physics.Motion.TargetInfo(
+ rm.TrackedTargetGuid,
+ AcDream.Core.Physics.Motion.TargetStatus.ExitWorld,
+ lp, lp));
+ }
+ }
+ mtm.UseTime();
+ }
+
///
/// Phase 6.6: the server says an entity's motion has changed. Look up
/// the AnimatedEntity for that guid, re-resolve the idle cycle with the
@@ -4741,7 +4789,24 @@ public sealed class GameWindow : IDisposable
// 6/7/8/9 route to MoveToManager.PerformMovement (which
// cancels again itself — retail does both) while ONLY type 0
// flows through the interpreted-state funnel below.
- if (_remoteDeadReckon.TryGetValue(update.Guid, out var remoteMot))
+ //
+ // R4-V5 door fix (2026-07-03 user report: doors stopped
+ // animating): entities that never receive an UpdatePosition
+ // (doors, levers — static animated objects) never got a
+ // RemoteMotion (created only in the UP handler), so since
+ // the L.2g S2b funnel cutover NOTHING applied their UM
+ // forward-command (the old direct SetCycle became the
+ // rm-only funnel) — a used door flipped ETHEREAL but never
+ // played its On/Off cycle. Retail runs the SAME unpack →
+ // CMotionInterp pipeline for every entity class; create the
+ // rm on first UM exactly like the UP handler does.
+ if (!_remoteDeadReckon.TryGetValue(update.Guid, out var remoteMot))
+ {
+ remoteMot = new RemoteMotion();
+ remoteMot.Body.Orientation = entity.Rotation;
+ remoteMot.Body.Position = entity.Position;
+ _remoteDeadReckon[update.Guid] = remoteMot;
+ }
{
var sink = EnsureRemoteMotionBindings(remoteMot, ae, update.Guid);
@@ -9525,7 +9590,19 @@ public sealed class GameWindow : IDisposable
if (ae.Sequencer is not null
&& serverGuid != 0
&& serverGuid != _playerServerGuid
- && _remoteDeadReckon.TryGetValue(serverGuid, out var rm))
+ && _remoteDeadReckon.TryGetValue(serverGuid, out var rm)
+ // R4-V5 door fix companion: rm entries now also exist for
+ // static animated objects (doors/levers — created on first
+ // UM so the funnel can play their On/Off cycles). Those
+ // must NOT enter this dead-reckoning block: it force-
+ // asserts ground contact, zeroes/integrates velocity, and
+ // ground-resolves the body — position machinery for
+ // entities the server MOVES. "Has ever received an
+ // UpdatePosition" is exactly that distinction (there is
+ // nothing to dead-reckon between UPs that don't exist);
+ // their animation still plays via the sequencer's own
+ // TickAnimations advance.
+ && rm.LastServerPosTime > 0)
{
if (IsPlayerGuid(serverGuid) && !rm.Airborne)
{
@@ -9583,6 +9660,21 @@ public sealed class GameWindow : IDisposable
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Active;
}
+ // R4-V5 glide fix (2026-07-03 user report: a retail
+ // player using an object GLIDES to it — position moves
+ // via the UP queue but no walk/run legs): remote
+ // PLAYERS' MoveToManagers were armed by mt-6 but never
+ // TICKED — the V4 UseTime slot lived only in the
+ // NPC/legacy branch below, gated !IsPlayerGuid
+ // (inherited from the deleted RemoteMoveToDriver's
+ // NPC-only scope). Retail ticks every entity's
+ // MovementManager (UpdateObjectInternal has no entity-
+ // class fork). The manager's dispatches produce the
+ // locomotion cycle through the funnel sink (the LEGS);
+ // position stays queue-chased per the L.3 M2 spec, so
+ // the two compose exactly like an NPC's tick.
+ TickRemoteMoveTo(rm);
+
// Step 2 (M3): queue + anim translation via PositionManager.
// ComputeOffset returns:
// - Vector3.Zero when queue is empty AND seqVel is zero
@@ -9804,40 +9896,7 @@ public sealed class GameWindow : IDisposable
// apply_current_movement recomputes velocity from
// whatever the manager dispatched — the same
// per-tick shape ordinary locomotion uses.
- if (!IsPlayerGuid(serverGuid) && rm.MoveTo is { } mtm)
- {
- if (rm.TrackedTargetGuid != 0)
- {
- if (_entitiesByServerGuid.TryGetValue(rm.TrackedTargetGuid, out var trackedEnt))
- {
- var tpos = trackedEnt.Position;
- if (!rm.TrackedTargetFedOnce
- || System.Numerics.Vector3.Distance(tpos, rm.TrackedTargetLastFedPos)
- > rm.TrackedTargetRadius)
- {
- rm.TrackedTargetFedOnce = true;
- rm.TrackedTargetLastFedPos = tpos;
- var tp = new AcDream.Core.Physics.Position(
- rm.CellId, tpos, System.Numerics.Quaternion.Identity);
- mtm.HandleUpdateTarget(new AcDream.Core.Physics.Motion.TargetInfo(
- rm.TrackedTargetGuid,
- AcDream.Core.Physics.Motion.TargetStatus.Ok,
- tp, tp));
- }
- }
- else
- {
- var lp = new AcDream.Core.Physics.Position(
- rm.CellId, rm.TrackedTargetLastFedPos,
- System.Numerics.Quaternion.Identity);
- mtm.HandleUpdateTarget(new AcDream.Core.Physics.Motion.TargetInfo(
- rm.TrackedTargetGuid,
- AcDream.Core.Physics.Motion.TargetStatus.ExitWorld,
- lp, lp));
- }
- }
- mtm.UseTime();
- }
+ TickRemoteMoveTo(rm);
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
}
}