fix(physics): close #77 — auto-walk honors ACE CanCharge bit; zero velocity in turn-in-place

Two related close-range bugs reported in #77 share a root in
PlayerMovementController.DriveServerAutoWalk + BeginServerAutoWalk:

1. **Walk-vs-run misclassification.** BeginServerAutoWalk decided
   `_autoWalkInitiallyRunning = (initialDist - distanceToObject) >= 1.0f`,
   forcing run at any chase past ~1.6 m. ACE's wire-level walk-vs-run
   answer is the MovementParameters CanCharge bit (0x10), which
   Creature.SetWalkRunThreshold sets when server-side player→target
   distance >= WalkRunThreshold/2 (= 7.5 m default). Retail's
   MovementParameters::get_command (decomp 0x0052aa00) gates the run
   path on CanCharge first; the inner walk_run_threshold check
   practically always walks given ACE's 15 m default. The hardcoded
   1.0 m threshold pushed run into the 3-5 m walk-range the user
   reported should walk.

2. **Velocity leak in turn-in-place phase.** When the auto-walked body
   crossed the destination, desiredYaw flipped ~180°, walkAligned
   dropped to false, and the `if (!moveForward) return true;` branch
   returned without zeroing body velocity. The body kept the prior
   frame's running velocity (RunAnimSpeed × runRate ≈ 11 m/s) and
   slid 4-5 m past the target before the turn-around rotation
   completed — the "runs and slides away, runs back, picks up"
   symptom in #77 bug B.

Changes:

- `CreateObject.ServerMotionState.CanCharge`: new bool prop reading
  bit 0x10 of MoveToParameters. Cross-ref ACE
  MovementParams.CanCharge = 0x10.
- `PlayerMovementController.BeginServerAutoWalk`: replaces the unused
  `walkRunThreshold` parameter with `bool canCharge`; sets
  `_autoWalkInitiallyRunning = canCharge`.
- `PlayerMovementController.DriveServerAutoWalk` turn-in-place branch:
  calls `_motion.DoMotion(Ready, 1.0)` and zeros body horizontal
  velocity (preserving Z for gravity). No-op for case (a) initial-turn
  with stationary body; fixes (b) overshoot recovery and (c) settling
  cases.
- `GameWindow.OnLiveMotionUpdated`: passes
  `update.MotionState.CanCharge` through; [autowalk-begin] trace
  shows `canCharge=` instead of `walkRunThresh=`.
- `GameWindow.InstallSpeculativeTurnToTarget`: predicts ACE's
  CanCharge from local distance using ACE's exact 7.5 m rule, so the
  speculative install agrees with the wire-triggered overwrite that
  arrives moments later.

Visual-verified at Holtburg 2026-05-18: walk-range NPC click walks +
fires Use, walk-range F-key pickup walks + no overshoot, far-range
(8-10 m) pickup still runs. Test baseline unchanged (8 Core pre-existing
failures, 0 net-new failures across Core/Net/UI/App suites).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-18 09:33:33 +02:00
parent 677266d477
commit 3be700020b
3 changed files with 93 additions and 47 deletions

View file

@ -3396,16 +3396,17 @@ public sealed class GameWindow : IDisposable
pathData.OriginZ,
_liveCenterX,
_liveCenterY);
bool canCharge = update.MotionState.CanCharge;
_playerController.BeginServerAutoWalk(
destWorld,
pathData.MinDistance,
pathData.DistanceToObject,
update.MotionState.MoveTowards,
pathData.WalkRunThreshold);
canCharge);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-begin] dest=({destWorld.X:F2},{destWorld.Y:F2},{destWorld.Z:F2}) minDist={pathData.MinDistance:F2} objDist={pathData.DistanceToObject:F2} walkRunThresh={pathData.WalkRunThreshold:F2} towards={update.MotionState.MoveTowards}"));
$"[autowalk-begin] dest=({destWorld.X:F2},{destWorld.Y:F2},{destWorld.Z:F2}) minDist={pathData.MinDistance:F2} objDist={pathData.DistanceToObject:F2} canCharge={canCharge} towards={update.MotionState.MoveTowards}"));
}
}
// Note: do NOT cancel auto-walk on a non-MoveTo motion
@ -9412,21 +9413,27 @@ public sealed class GameWindow : IDisposable
if ((odf & LargeFlatMask) != 0) useRadius = 2.0f;
}
// Issue #77 fix (2026-05-18) — predict ACE's CanCharge bit
// from local distance so the speculative auto-walk uses the
// same walk/run as the wire-triggered overwrite that arrives
// moments later. ACE's Creature.SetWalkRunThreshold sets
// CanCharge when player→target distance >= WalkRunThreshold /
// 2 = 7.5 m (the 15 m wire default halved). Match exactly so
// the speculative install doesn't flip walk↔run when ACE's
// MoveToObject broadcast overwrites it.
const float AceCanChargeDistance = 7.5f;
var bodyPos = _playerController.Position;
float ddx = entity.Position.X - bodyPos.X;
float ddy = entity.Position.Y - bodyPos.Y;
float distToTarget = MathF.Sqrt(ddx * ddx + ddy * ddy);
bool speculativeCanCharge = distToTarget >= AceCanChargeDistance;
_playerController.BeginServerAutoWalk(
destinationWorld: entity.Position,
minDistance: 0f,
distanceToObject: useRadius,
moveTowards: true,
// 15 m matches ACE's MoveToParameters.SetDefaults
// WalkRunThreshold for non-combat Use/PickUp paths.
// Using 9999 here forced walk-mode for the brief window
// between this speculative install and ACE's MovementType=6
// overwrite — far targets briefly walked before switching
// to run, which the user observed as "we only walk, not
// running from the correct threshold". 15.0 lines up with
// what ACE will send anyway, so the initial decision and
// the overwrite agree.
walkRunThreshold: 15.0f);
canCharge: speculativeCanCharge);
}
private uint? SelectClosestCombatTarget(bool showToast)