fix(R4-V5): remote-player movetos never ticked (the glide) + door UMs dropped since the funnel cutover

Two more live findings from the user's verify pass (the walk-up itself
works since 24569fd2):

GLIDE (a retail player using an object slides to it with no legs):
remote PLAYERS' MoveToManagers were armed by mt-6 but never TICKED -
the V4 UseTime slot lived only in the NPC/legacy per-tick branch,
gated !IsPlayerGuid (inherited from the deleted RemoteMoveToDriver's
NPC-only scope). Retail's UpdateObjectInternal has no entity-class
fork. The P4 tracker feed + UseTime is extracted into a shared
TickRemoteMoveTo(rm) and now also runs in the grounded player-remote
pipeline (L.3 M2): the manager's dispatches produce the locomotion
cycle through the funnel sink (the legs) while position stays
queue-chased per the M2 spec - the same composition an NPC gets.

DOORS (used doors flip ETHEREAL but never animate; a door opened by a
retail client does not animate either): static animated objects never
receive an UpdatePosition, so they never got a RemoteMotion (created
only in the UP handler) - and since the L.2g S2b funnel cutover the
UM motion apply is rm-only (the old direct SetCycle became the
funnel), so their On/Off forward-commands were parsed and dropped.
Confirmed live: door UMs arriving with cmd=0x000B/0x000C while the
door sequencer stayed frozen. Fix: create the rm on first UM (the
retail one-pipeline-for-every-entity-class shape), with a
LastServerPosTime>0 gate keeping UP-less entities OUT of the
dead-reckoning tick block (it force-asserts ground contact and
ground-resolves the body - position machinery for entities the server
moves; a wall-mounted door must not be terrain-snapped). Their cycles
play via the sequencer's own TickAnimations advance.

Remote jump-landing pose (third user report) not addressed here -
instrumentation next if it survives this build's retest.

Full suite green: 3,958.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 14:51:59 +02:00
parent 24569fd2a1
commit 006cf6597a

View file

@ -4442,6 +4442,54 @@ public sealed class GameWindow : IDisposable
return false;
}
/// <summary>
/// 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
/// <c>HandleUpdateTarget(Ok)</c> from the live entity table when the
/// tracked target moved beyond the voyeur radius (ExitWorld on
/// despawn), then runs <c>UseTime()</c> — 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.
/// </summary>
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();
}
/// <summary>
/// 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);
}
}