feat(B.6 slice 2): local-player auto-walk on inbound MoveToObject

Retail-faithful per MovementManager::PerformMovement (0x00524440 case 6,
decomp 300628-300648): when ACE broadcasts MoveToObject for the local
player, the local client runs its OWN auto-walk on its OWN body —
heading correction toward the target, run-forward velocity, arrival
detected via the wire's min_distance / distance_to_object predicates.

Implementation:

  PlayerMovementController:
    + IsServerAutoWalking property (read-only)
    + BeginServerAutoWalk(destWorld, minDist, objDist, moveTowards)
    + EndServerAutoWalk(reason)  // idempotent, logs to [autowalk-end]
                                  // when ACDREAM_PROBE_AUTOWALK is on
    + ApplyAutoWalkOverlay(dt, input) — called at the top of Update.
        - User movement key (Forward/Backward/Strafe/Turn) cancels.
        - Arrival predicate matches RemoteMoveToDriver / retail.
        - Heading steered toward destination at ±20° snap-on-aligned
          tolerance / π/2 rad/s rotation rate (same constants the
          remote-creature path uses).
        - Synthesizes input as Forward+Run; the rest of Update's
          MotionInterpreter + body-velocity pipeline runs unchanged.

  GameWindow.OnLiveMotionUpdated (local-player branch):
    + when update.MotionState.IsServerControlledMoveTo and MoveToPath
      is populated: translate origin to world via RemoteMoveToDriver
      .OriginToWorld, call _playerController.BeginServerAutoWalk.
    + when a non-MoveTo motion arrives and auto-walk is active:
      EndServerAutoWalk(reason="motion-non-moveto").
    + [autowalk-begin] trace line under ACDREAM_PROBE_AUTOWALK.

The mtRun=0 case from the spec trace is handled implicitly: this
slice doesn't read MoveToRunRate at all — it relies on the existing
input.Run path which uses the player's local InqRunRate (env-var
defaulted to 200). Future slice can layer in mtRun!=0 honor if needed.

Slices 3 (animation cycle source while auto-walking) and 4 (local
pickup animation echo for #64) deferred to follow-up commits.
This commit is contained in:
Erik 2026-05-14 18:50:59 +02:00
parent d82b0648b5
commit b936ef8b0b
2 changed files with 222 additions and 0 deletions

View file

@ -3313,6 +3313,46 @@ public sealed class GameWindow : IDisposable
Console.WriteLine(System.FormattableString.Invariant(
$"[autowalk-mt] stance=0x{stance:X4} cmd={cmdHex} mt=0x{update.MotionState.MovementType:X2} isMoveTo={update.MotionState.IsServerControlledMoveTo} moveTowards={update.MotionState.MoveTowards} {pathStr} {spd} {mtsSpd} {mtsRun}"));
}
// B.6 slice 2 (2026-05-14): drive the local player's body
// through a server-initiated auto-walk when ACE sends
// MoveToObject (movement type 6) — retail-faithful per
// MovementManager::PerformMovement 0x00524440 case 6. When
// the inbound motion is NOT a MoveTo, cancel any active
// auto-walk (server intent changed).
if (_playerController is not null)
{
if (update.MotionState.IsServerControlledMoveTo
&& update.MotionState.MoveToPath is { } pathData)
{
// Translate landblock-local origin → world space.
var destWorld = AcDream.Core.Physics.RemoteMoveToDriver
.OriginToWorld(
pathData.OriginCellId,
pathData.OriginX,
pathData.OriginY,
pathData.OriginZ,
_liveCenterX,
_liveCenterY);
_playerController.BeginServerAutoWalk(
destWorld,
pathData.MinDistance,
pathData.DistanceToObject,
update.MotionState.MoveTowards);
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} towards={update.MotionState.MoveTowards}"));
}
}
else if (_playerController.IsServerAutoWalking)
{
// A non-MoveTo motion arrived (e.g., Ready, or a
// user-input echo) — server's auto-walk intent is
// gone; release the local driver.
_playerController.EndServerAutoWalk("motion-non-moveto");
}
}
}
else
{