feat(anim): Phase L.1c port MoveTo path data + per-tick steer
Root-causing the user-reported "monsters disappearing some time +
laggy/jittery locomotion" via systematic-debugging Phase 1: our
UpdateMotion parser kept only speed/runRate/flags from a movementType
6/7 packet and discarded Origin (destination), targetGuid, and the
distance/walkRunThreshold/desiredHeading half of MovementParameters.
The integrator consequently held Body.Velocity at zero during MoveTo
("incomplete state" stabilizer 882a07c), so the body froze with legs
animating until UpdatePosition snap-teleported it — sometimes outside
the visible window (disappearing) — and constant-velocity drift along
the old heading between snaps produced jitter on every UP correction.
The 882a07c stabilizer was deliberately conservative because the state
WAS incomplete. Completing the data plumbing makes its restriction
moot: with the full MoveTo payload captured, the body solver has every
field retail's MoveToManager::HandleMoveToPosition (0x00529d80) reads.
Why: server re-emits MoveTo packets ~1 Hz with refreshed Origin while
chasing — verified in the live log (guid 0x800003B5 seq 0x01FE→0x0204
all show different cell/xyz floats). Those are heading updates we'd
been throwing away. With the full payload retained, the per-tick driver
steers body orientation toward Origin (±20° snap tolerance, π/2 rad/s
turn rate above tolerance) and lets apply_current_movement fill in
Velocity from the existing RunForward cycle — no new motion path,
just the right heading.
Scope is the minimum viable subset: target re-tracking, sticky/StickTo,
fail-distance progress detector, and sphere-cylinder distance are
server-side concerns we don't need (server's emit cadence handles all
of them). MoveToObject_Internal target-guid resolution is also skipped
— Origin is refreshed each packet, so the effective target tracks the
real entity even without a guid lookup.
Cross-references:
- docs/research/named-retail/acclient_2013_pseudo_c.txt — MoveToManager
+ MovementParameters::UnPackNet (0x0052ac50) + apply_run_to_command
(0x00527be0). 18,366 named PDB symbols make this the primary oracle.
- references/ACE/Source/ACE.Server/Physics/Animation/MoveToManager.cs
— port aid; flagged divergences (WalkRunThreshold default, set_heading
snap, inRange one-shot) called out in the new pseudocode doc.
- docs/research/2026-04-28-remote-moveto-pseudocode.md — pseudocode +
ACE divergence flags + out-of-scope list per CLAUDE.md mandatory
workflow (decompile → cross-reference → pseudocode → port).
Tests: 1404 → 1412 (parser type-7 path retention + type-6 target guid
retention; driver arrival, in-tolerance snap, beyond-tolerance step,
behind-target shortest-path turn, arrival preserves orientation,
Origin→world landblock-grid arithmetic).
Pending visual sign-off — handoff stabilizer 882a07c was the last
commit the user tested.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
882a07cfde
commit
186a584404
7 changed files with 917 additions and 19 deletions
|
|
@ -226,11 +226,50 @@ public sealed class GameWindow : IDisposable
|
|||
/// <summary>
|
||||
/// True while a server MoveToObject/MoveToPosition packet is the
|
||||
/// active locomotion source. Retail runs these through MoveToManager
|
||||
/// and CMotionInterp using the packet's runRate; until we port the
|
||||
/// full target solver, use this only to protect packet-derived
|
||||
/// animation speed from velocity-cycle clobbering.
|
||||
/// and CMotionInterp; the per-tick remote driver consults this to
|
||||
/// decide whether to feed body steering through
|
||||
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> instead of
|
||||
/// the InterpretedMotionState path.
|
||||
/// </summary>
|
||||
public bool ServerMoveToActive;
|
||||
|
||||
/// <summary>
|
||||
/// True once a MoveTo packet's full path payload (Origin + thresholds)
|
||||
/// has been parsed and the world-converted destination is stored on
|
||||
/// <see cref="MoveToDestinationWorld"/>. Cleared on arrival or when
|
||||
/// the next non-MoveTo UpdateMotion replaces the locomotion source.
|
||||
/// Phase L.1c (2026-04-28).
|
||||
/// </summary>
|
||||
public bool HasMoveToDestination;
|
||||
|
||||
/// <summary>
|
||||
/// World-space destination from the most recent MoveTo packet's
|
||||
/// <c>Origin</c> field, converted via the same landblock-grid
|
||||
/// arithmetic <c>OnLivePositionUpdated</c> uses.
|
||||
/// </summary>
|
||||
public System.Numerics.Vector3 MoveToDestinationWorld;
|
||||
|
||||
/// <summary>
|
||||
/// <c>min_distance</c> from the MoveTo packet's MovementParameters.
|
||||
/// Used by <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> as
|
||||
/// the chase-arrival threshold per retail
|
||||
/// <c>MoveToManager::HandleMoveToPosition</c>.
|
||||
/// </summary>
|
||||
public float MoveToMinDistance;
|
||||
|
||||
/// <summary>
|
||||
/// <c>distance_to_object</c> from the MoveTo packet. Reserved for
|
||||
/// the flee branch (<c>move_away</c>); chase uses
|
||||
/// <see cref="MoveToMinDistance"/>.
|
||||
/// </summary>
|
||||
public float MoveToDistanceToObject;
|
||||
|
||||
/// <summary>
|
||||
/// True if MovementParameters bit 9 (<c>move_towards</c>, mask
|
||||
/// <c>0x200</c>) is set on the active packet — i.e. this is a
|
||||
/// chase. False = flee (<c>move_away</c>) or static target.
|
||||
/// </summary>
|
||||
public bool MoveToMoveTowards;
|
||||
/// <summary>
|
||||
/// Legacy field — no longer used for slerp (retail hard-snaps
|
||||
/// per FUN_00514b90 set_frame). Kept to avoid churn.
|
||||
|
|
@ -2454,6 +2493,37 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
remoteMot.ServerMoveToActive = update.MotionState.IsServerControlledMoveTo;
|
||||
|
||||
// Phase L.1c (2026-04-28): capture the full MoveTo path
|
||||
// payload so the per-tick remote driver can steer the
|
||||
// body toward Origin instead of holding velocity at zero
|
||||
// between sparse UpdatePosition snaps. Retail
|
||||
// MoveToManager::MoveToPosition stores the same fields
|
||||
// (acclient_2013_pseudo_c.txt:307521-307593).
|
||||
if (update.MotionState.IsServerControlledMoveTo
|
||||
&& update.MotionState.MoveToPath is { } path)
|
||||
{
|
||||
remoteMot.MoveToDestinationWorld = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.OriginToWorld(
|
||||
path.OriginCellId,
|
||||
path.OriginX,
|
||||
path.OriginY,
|
||||
path.OriginZ,
|
||||
_liveCenterX,
|
||||
_liveCenterY);
|
||||
remoteMot.MoveToMinDistance = path.MinDistance;
|
||||
remoteMot.MoveToDistanceToObject = path.DistanceToObject;
|
||||
remoteMot.MoveToMoveTowards = update.MotionState.MoveTowards;
|
||||
remoteMot.HasMoveToDestination = true;
|
||||
}
|
||||
else if (!update.MotionState.IsServerControlledMoveTo)
|
||||
{
|
||||
// Cycle changed off MoveTo — clear stale destination
|
||||
// so the per-tick driver doesn't keep steering after
|
||||
// the server has switched us back to interpreted
|
||||
// motion.
|
||||
remoteMot.HasMoveToDestination = false;
|
||||
}
|
||||
|
||||
// Forward axis (Ready / WalkForward / RunForward / WalkBackward).
|
||||
remoteMot.Motion.DoInterpretedMotion(
|
||||
fullMotion, speedMod, modifyInterpretedState: true);
|
||||
|
|
@ -5042,13 +5112,53 @@ public sealed class GameWindow : IDisposable
|
|||
rm.Body.Velocity = rm.ServerVelocity;
|
||||
}
|
||||
}
|
||||
else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive
|
||||
&& rm.HasMoveToDestination)
|
||||
{
|
||||
// Phase L.1c port of retail MoveToManager per-tick
|
||||
// steering (HandleMoveToPosition @ 0x00529d80).
|
||||
// Steer body orientation toward the latest
|
||||
// server-supplied destination, then let
|
||||
// apply_current_movement set Velocity from the
|
||||
// RunForward cycle through the now-correct heading.
|
||||
var driveResult = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.Drive(
|
||||
rm.Body.Position,
|
||||
rm.Body.Orientation,
|
||||
rm.MoveToDestinationWorld,
|
||||
rm.MoveToMinDistance,
|
||||
(float)dt,
|
||||
rm.MoveToMoveTowards,
|
||||
out var steeredOrientation);
|
||||
rm.Body.Orientation = steeredOrientation;
|
||||
|
||||
if (driveResult == AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.DriveResult.Arrived)
|
||||
{
|
||||
// Within arrival window — zero velocity until the
|
||||
// next MoveTo packet refreshes the destination
|
||||
// (or the server explicitly stops us with an
|
||||
// interpreted-motion UM cmd=Ready).
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Steering active — apply_current_movement reads
|
||||
// InterpretedState.ForwardCommand=RunForward (set
|
||||
// when the MoveTo packet arrived) and emits
|
||||
// velocity along +Y in body local space. Our
|
||||
// updated orientation rotates that into the right
|
||||
// world direction toward the target.
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
}
|
||||
else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive)
|
||||
{
|
||||
// We only parse enough of MoveTo to recover retail
|
||||
// animation speed. Do not let apply_current_movement
|
||||
// extrapolate position from an incomplete target
|
||||
// solver; hold until the next UpdatePosition-derived
|
||||
// velocity arrives.
|
||||
// MoveTo flag set but we haven't seen a path payload
|
||||
// yet (e.g. truncated packet, or a brand-new entity
|
||||
// whose first cycle UM is still in flight). Hold
|
||||
// velocity at zero — same conservative stance as the
|
||||
// 882a07c stabilizer for incomplete state.
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue