fix(anim): Phase L.1c clear MoveTo state + bulk-copy ForwardCommand on overlay UMs

User-observed regression on commit d247aef: creature reaches melee
range and "just runs" instead of stopping to attack. Two independent
research subagents converged on the same root cause.

When ACE broadcasts a melee swing, it sends an mt=0 UpdateMotion with
ForwardCommand=AttackHigh1 (Action class, 0x10000062), motion_flags
=StickToObject, and a trailing 4-byte sticky-target guid — there is
NO preceding cmd=Ready. The swing UM IS the stop signal.

Retail's CMotionInterp::move_to_interpreted_state
(acclient_2013_pseudo_c.txt:305936-305992) bulk-copies forward_command
from the wire into InterpretedState UNCONDITIONALLY, regardless of
motion class. With forward_command=AttackHigh1, get_state_velocity
(:305172-305180) returns velocity.Y=0 because its gate is
RunForward||WalkForward — body stops moving forward. The animation
overlay (the swing) is appended on top of whatever cyclic tail is
active.

Acdream's overlay branch in GameWindow.OnLiveMotionUpdated routed
Action-class commands through PlayAction (animation overlay only) and
SKIPPED:
  - ServerMoveToActive flag update — stale RunForward MoveTo state
    persisted, the per-tick driver kept steering toward the prior
    Origin and calling apply_current_movement.
  - InterpretedState.ForwardCommand bulk-copy — even if the flag had
    been cleared, the body's InterpretedState.ForwardCommand stayed
    at RunForward from the prior MoveTo cycle, so
    apply_current_movement kept producing forward velocity.
  - MoveToPath capture — staleness-timeout band-aid masked this.

Fix: lift the _remoteDeadReckon state-update block out of the
substate-only `else` branch so it runs for both overlay and substate
paths. For non-MoveTo packets, write fullMotion + speedMod directly to
InterpretedState.ForwardCommand/ForwardSpeed (bypassing
ApplyMotionToInterpretedState, which is a heuristic helper that
silently no-ops for Action class — see MotionInterpreter.cs:941-970).
This matches retail's copy_movement_from
(acclient_2013_pseudo_c.txt:293301-293311) bulk-copy semantics.

Also corrected RemoteMoveToDriver arrival predicate to retail-faithful:
chase = dist <= DistanceToObject; flee = dist >= MinDistance. The
prior max(MinDistance, DistanceToObject) defensive port happened to
compute the right value for ACE's wire defaults but had wrong
semantics (would have failed for any retail config with MinDistance >
DistanceToObject).

Tests: 1414 → 1416. New parser test for the AttackHigh1 wire layout;
new driver tests for retail-faithful chase/flee arrival.

Defers: target-guid live resolution for type 6 packets (chase-lag
mitigation, symptom #3), StickToObject sticky-target guid trailing
field, full MoveToManager port (CheckProgressMade, pending_actions
queue, Sticky/StickTo, use_final_heading).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 10:02:53 +02:00
parent d247aef2e4
commit f794832ebc
4 changed files with 165 additions and 54 deletions

View file

@ -60,12 +60,30 @@ public class RemoteMoveToDriverTests
}
[Fact]
public void Drive_RetailMinDistanceWins_WhenLargerThanDistanceToObject()
public void Drive_FleeArrival_UsesMinDistance()
{
// Hypothetical retail packet: MinDistance=2.0 (set explicitly),
// DistanceToObject=0.6 (default). Arrival should fire at 2 m
// because retail's algorithm uses MinDistance and it's the larger
// of the two.
// Flee branch (moveTowards=false): arrival when dist >= MinDistance.
// Retail / ACE both use MinDistance for the flee-arrival threshold.
var bodyPos = new Vector3(0f, 0f, 0f);
var bodyRot = Quaternion.Identity;
var dest = new Vector3(0f, 6f, 0f);
var result = RemoteMoveToDriver.Drive(
bodyPos, bodyRot, dest,
minDistance: 5.0f, distanceToObject: 0.6f,
dt: 0.016f, moveTowards: false,
out _);
Assert.Equal(RemoteMoveToDriver.DriveResult.Arrived, result);
}
[Fact]
public void Drive_ChaseDoesNotArriveAtMinDistanceFloor()
{
// Regression: my earlier max(MinDistance, DistanceToObject) port
// would have arrived here because dist (1.5) <= MinDistance (2.0).
// Retail uses DistanceToObject for chase arrival, so a chase at
// dist=1.5 with DistanceToObject=0.6 should still STEER, not arrive.
var bodyPos = new Vector3(0f, 0f, 0f);
var bodyRot = Quaternion.Identity;
var dest = new Vector3(0f, 1.5f, 0f);
@ -76,7 +94,7 @@ public class RemoteMoveToDriverTests
dt: 0.016f, moveTowards: true,
out _);
Assert.Equal(RemoteMoveToDriver.DriveResult.Arrived, result);
Assert.Equal(RemoteMoveToDriver.DriveResult.Steering, result);
}
[Fact]