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:
parent
d247aef2e4
commit
f794832ebc
4 changed files with 165 additions and 54 deletions
|
|
@ -268,6 +268,41 @@ public class UpdateMotionTests
|
|||
Assert.Equal(90.0f, path.DesiredHeading);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesAttackHigh1_AsActionForwardCommand()
|
||||
{
|
||||
// Phase L.1c followup (2026-04-28): regression that verifies the
|
||||
// wire-format ACE uses for melee swings — mt=0 with
|
||||
// ForwardCommand=AttackHigh1 (0x0062 in low 16 bits) and
|
||||
// ForwardSpeed (typically the animSpeed). The receiver in
|
||||
// GameWindow.OnLiveMotionUpdated relies on this layout to bulk-copy
|
||||
// ForwardCommand into the body's InterpretedState so that
|
||||
// get_state_velocity returns 0 (gate is RunForward||WalkForward).
|
||||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2 + 4];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x800003B5u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6; // header padding
|
||||
|
||||
body[p++] = 0; // mt = Invalid (interpreted)
|
||||
body[p++] = 0; // motion_flags
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003C); p += 2; // stance: HandCombat
|
||||
|
||||
// InterpretedMotionState: flags = ForwardCommand (0x02) | ForwardSpeed (0x04)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x06u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0062); p += 2; // AttackHigh1 low bits
|
||||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.25f); p += 4; // animSpeed
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal((byte)0, result!.Value.MotionState.MovementType);
|
||||
Assert.False(result.Value.MotionState.IsServerControlledMoveTo);
|
||||
Assert.Equal((ushort)0x0062, result.Value.MotionState.ForwardCommand);
|
||||
Assert.Equal(1.25f, result.Value.MotionState.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesMoveToObjectTargetGuidAndOrigin()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue