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
|
|
@ -143,7 +143,8 @@ public static class CreateObject
|
|||
byte MovementType = 0,
|
||||
uint? MoveToParameters = null,
|
||||
float? MoveToSpeed = null,
|
||||
float? MoveToRunRate = null)
|
||||
float? MoveToRunRate = null,
|
||||
MoveToPathData? MoveToPath = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE/retail movement types 6 and 7 are server-controlled
|
||||
|
|
@ -155,8 +156,43 @@ public static class CreateObject
|
|||
|
||||
public bool MoveToCanRun => !MoveToParameters.HasValue
|
||||
|| (MoveToParameters.Value & 0x2u) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// MovementParameters bit 9 (mask 0x200) — set when the creature is
|
||||
/// chasing its target. Cross-checked against acclient.h:31423-31443
|
||||
/// (named retail) + ACE <c>MovementParamFlags.MoveTowards</c>.
|
||||
/// </summary>
|
||||
public bool MoveTowards => MoveToParameters.HasValue
|
||||
&& (MoveToParameters.Value & 0x200u) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path-control payload of a server-controlled MoveTo packet (movementType 6 or 7).
|
||||
/// Wire layout per <c>MovementParameters::UnPackNet</c> @ <c>0x0052ac50</c>
|
||||
/// + the leading <c>Origin</c> + optional target guid for type 6:
|
||||
/// <list type="bullet">
|
||||
/// <item>type 6 (MoveToObject) only: u32 <c>TargetGuid</c></item>
|
||||
/// <item>Origin: u32 <c>cellId</c>, then 3 floats (local x/y/z within the landblock)</item>
|
||||
/// <item>MovementParameters (28 bytes, exact retail order):
|
||||
/// u32 flags, f32 <c>distance_to_object</c>, f32 <c>min_distance</c>,
|
||||
/// f32 <c>fail_distance</c>, f32 <c>speed</c>, f32 <c>walk_run_threshhold</c>,
|
||||
/// f32 <c>desired_heading</c></item>
|
||||
/// </list>
|
||||
/// (The trailing <c>runRate</c> float is captured separately on
|
||||
/// <see cref="ServerMotionState.MoveToRunRate"/>.)
|
||||
/// </summary>
|
||||
public readonly record struct MoveToPathData(
|
||||
uint? TargetGuid,
|
||||
uint OriginCellId,
|
||||
float OriginX,
|
||||
float OriginY,
|
||||
float OriginZ,
|
||||
float DistanceToObject,
|
||||
float MinDistance,
|
||||
float FailDistance,
|
||||
float WalkRunThreshold,
|
||||
float DesiredHeading);
|
||||
|
||||
/// <summary>
|
||||
/// One entry in the InterpretedMotionState's Commands list (MotionItem).
|
||||
/// The server packs 0..many of these per broadcast: emotes, attacks,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ public static class UpdateMotion
|
|||
uint? moveToParameters = null;
|
||||
float? moveToSpeed = null;
|
||||
float? moveToRunRate = null;
|
||||
CreateObject.MoveToPathData? moveToPath = null;
|
||||
List<CreateObject.MotionItem>? commands = null;
|
||||
|
||||
if (movementType == 0)
|
||||
|
|
@ -232,7 +233,8 @@ public static class UpdateMotion
|
|||
movementType,
|
||||
out moveToParameters,
|
||||
out moveToSpeed,
|
||||
out moveToRunRate);
|
||||
out moveToRunRate,
|
||||
out moveToPath);
|
||||
}
|
||||
|
||||
return new Parsed(guid, new CreateObject.ServerMotionState(
|
||||
|
|
@ -241,7 +243,8 @@ public static class UpdateMotion
|
|||
movementType,
|
||||
moveToParameters,
|
||||
moveToSpeed,
|
||||
moveToRunRate));
|
||||
moveToRunRate,
|
||||
moveToPath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -255,11 +258,13 @@ public static class UpdateMotion
|
|||
byte movementType,
|
||||
out uint? movementParameters,
|
||||
out float? speed,
|
||||
out float? runRate)
|
||||
out float? runRate,
|
||||
out CreateObject.MoveToPathData? path)
|
||||
{
|
||||
movementParameters = null;
|
||||
speed = null;
|
||||
runRate = null;
|
||||
path = null;
|
||||
|
||||
// Retail MovementManager::PerformMovement (0x00524440) consumes
|
||||
// MoveToObject/MoveToPosition as:
|
||||
|
|
@ -268,25 +273,60 @@ public static class UpdateMotion
|
|||
// MovementParameters::UnPackNet (0x0052AC50): flags, distance,
|
||||
// min, fail, speed, walk/run threshold, desired heading
|
||||
// f32 runRate copied into CMotionInterp::my_run_rate.
|
||||
//
|
||||
// Phase L.1c (2026-04-28): the full path payload is now retained on
|
||||
// <see cref="CreateObject.MoveToPathData"/> so the per-tick remote
|
||||
// body driver can steer toward Origin instead of holding velocity at
|
||||
// zero between sparse UpdatePosition snaps. The 882a07c stabilizer
|
||||
// was deliberately conservative because we only had speed+runRate;
|
||||
// with the rest of the packet captured, the body solver has full
|
||||
// path data and can run faithfully.
|
||||
uint? targetGuid = null;
|
||||
if (movementType == 6)
|
||||
{
|
||||
if (body.Length - pos < 4) return false;
|
||||
pos += 4; // target guid
|
||||
targetGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
if (body.Length - pos < 16 + 28 + 4) return false;
|
||||
pos += 16; // Origin
|
||||
|
||||
uint originCellId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float originX = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float originY = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float originZ = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
|
||||
movementParameters = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
pos += 4; // distanceToObject
|
||||
pos += 4; // minDistance
|
||||
pos += 4; // failDistance
|
||||
float distanceToObject = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float minDistance = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float failDistance = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
speed = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
pos += 4; // walkRunThreshold
|
||||
pos += 4; // desiredHeading
|
||||
float walkRunThreshold = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
float desiredHeading = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
runRate = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
|
||||
|
||||
path = new CreateObject.MoveToPathData(
|
||||
targetGuid,
|
||||
originCellId,
|
||||
originX,
|
||||
originY,
|
||||
originZ,
|
||||
distanceToObject,
|
||||
minDistance,
|
||||
failDistance,
|
||||
walkRunThreshold,
|
||||
desiredHeading);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue