From 006cf6597aba138333866bfa2efa4a02896839da Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 3 Jul 2026 14:51:59 +0200 Subject: [PATCH] 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 --- src/AcDream.App/Rendering/GameWindow.cs | 131 +++++++++++++++++------- 1 file changed, 95 insertions(+), 36 deletions(-) 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); } }