Captured a live ACDREAM_PROBE_AUTOWALK trace double-clicking +Je from ~3.5m. Findings folded into the spec's State at design freeze section: 1. Wire parser is correct (matches ACE MoveToObject.Write + MoveToParameters.Write byte-for-byte). 2. ACE sends mtRun=0.00. Not a parser bug — that's the wire value. Retail's apply_run_to_command (0x00527BE0) fell back to the player's own rate; our Slice 2 needs the same fallback chain. 3. Player position never changed during the entire trace — current behavior is pure no-op on the inbound MoveToObject (literally ignored, as our code at OnLiveMotionUpdated:3289 suggests). 4. ACE does NOT broadcast UpdatePosition for the local player during auto-walk. Definitively kills Option C — nothing to blend with. Local body must drive itself. The trace validates the spec's Option A path. Slice 2 implementation can proceed without further wire-format guessing.
422 lines
20 KiB
Markdown
422 lines
20 KiB
Markdown
# Phase B.6 — Local-player auto-walk for server-initiated MoveToObject — design
|
||
|
||
**Date:** 2026-05-14.
|
||
**Status:** DESIGN — implementation deferred to a dedicated session.
|
||
**Closes:** [issue #63](../../ISSUES.md) (server-initiated `MoveToObject` auto-walk not honored).
|
||
**Predecessors:** [B.5](../plans/2026-05-14-phase-b5-pickup.md) (ground-item pickup, close-range path) + [B.4b](../plans/2026-05-13-phase-b4b-plan.md) (outbound Use chain).
|
||
**References:**
|
||
- ACE server-side: [`Player_Move.cs:37–179`](../../../references/ACE/Source/ACE.Server/WorldObjects/Player_Move.cs) (`CreateMoveToChain`, `MoveToChain`, `MoveTo`).
|
||
- ACE pickup driver: [`Player_Inventory.cs:976–1106`](../../../references/ACE/Source/ACE.Server/WorldObjects/Player_Inventory.cs).
|
||
- Reference port (Rust): [holtburger `simulation.rs:33–41` + `178–191`](../../../references/holtburger/crates/holtburger-core/src/client/simulation.rs).
|
||
- Existing acdream infra: [`RemoteMoveToDriver.cs`](../../../src/AcDream.Core/Physics/RemoteMoveToDriver.cs), [`ServerControlledLocomotion.cs`](../../../src/AcDream.Core/Physics/ServerControlledLocomotion.cs).
|
||
|
||
---
|
||
|
||
## Problem statement
|
||
|
||
When the local player triggers a `Use (0x0036)` or `PutItemInContainer
|
||
(0x0019)` on a target outside ACE's `WithinUseRadius` (default 0.6 m),
|
||
ACE's `CreateMoveToChain` initiates a **server-side auto-walk** toward
|
||
the target. ACE also broadcasts `Motion(MovementType=MoveToObject,
|
||
target=X)` via `EnqueueBroadcastMotion`.
|
||
|
||
Our client currently does NOT honor this server-initiated motion for
|
||
the local player. The visible symptom: the character drifts a short
|
||
distance toward the target, then "snaps back" to the original
|
||
position. ACE's `MoveToChain` polls `WithinUseRadius` every 0.1 s; if
|
||
the player never enters the radius before `defaultMoveToTimeout` fires,
|
||
the chain calls `callback(false)` which broadcasts
|
||
`InventoryServerSaveFailed (ActionCancelled)` and the pickup / use
|
||
never completes.
|
||
|
||
**User-facing impact:**
|
||
- Double-click on a ground item from any distance → walk + snap, no pickup.
|
||
- F-press on a ground item from > 0.6 m → walk + snap, no pickup.
|
||
- Use on an out-of-range NPC → same.
|
||
|
||
Close-range (≤ 0.6 m) Use and PickUp work correctly via ACE's
|
||
early-return branch at `Player_Move.cs:66` (the `WithinUseRadius`
|
||
shortcut that skips the chain entirely).
|
||
|
||
---
|
||
|
||
## Current state — wire data we already have
|
||
|
||
`UpdateMotion (0xF74D)` is already parsed end-to-end. The parser
|
||
populates `EntityMotionUpdate.MotionState` with:
|
||
|
||
| Field | Source | Notes |
|
||
|---|---|---|
|
||
| `MovementType` | wire byte | `MoveToObject = 6`, `MoveToPosition = 7`. |
|
||
| `IsServerControlledMoveTo` | derived | True when `MovementType ∈ {6, 7}`. |
|
||
| `MoveToPath` | wire (when set) | `(OriginCellId, OriginX/Y/Z, MinDistance, DistanceToObject)` — the auto-walk destination, plus arrival predicates. |
|
||
| `MoveToSpeed`, `MoveToRunRate`, `MoveToCanRun`, `MoveTowards` | wire | speed scalars + chase-vs-flee bit. |
|
||
|
||
The remote-creature path at
|
||
[`GameWindow.cs:3346–3425`](../../../src/AcDream.App/Rendering/GameWindow.cs)
|
||
already consumes all of this — `_remoteDeadReckon` per-entity state
|
||
captures `MoveToDestinationWorld`, `MoveToMinDistance`,
|
||
`MoveToDistanceToObject`, `MoveToMoveTowards`, and
|
||
`HasMoveToDestination`. The per-tick driver
|
||
[`RemoteMoveToDriver`](../../../src/AcDream.Core/Physics/RemoteMoveToDriver.cs)
|
||
computes heading + arrival and is exercised on every NPC chase.
|
||
|
||
**Gap:** none of this fires for `update.Guid == _playerServerGuid`. The
|
||
local-player branch is gated out at:
|
||
|
||
1. `GameWindow.cs:3289` — `if (update.Guid == _playerServerGuid)` skips
|
||
`SetCycle` so `UpdatePlayerAnimation` stays authoritative.
|
||
2. `GameWindow.cs:3346` — `_remoteDeadReckon.TryGetValue(update.Guid,
|
||
…)` returns false because the local player isn't in
|
||
`_remoteDeadReckon`.
|
||
|
||
The wire is parsed, the destination is in `MotionState.MoveToPath`, and
|
||
nothing in our client reads it for the local player.
|
||
|
||
---
|
||
|
||
## Why the player visibly walks-then-snaps today
|
||
|
||
A. ACE broadcasts `MotionUpdated(MoveToObject)` → our client ignores it
|
||
for the local player (above).
|
||
B. ACE's server-side `PhysicsObj.MoveToObject(…)` runs its own
|
||
simulation. ACE updates the player's authoritative `Location` on its
|
||
side every physics tick.
|
||
C. ACE periodically broadcasts the player's updated position back to
|
||
everyone — including the player themselves — via `UpdatePosition`.
|
||
The local-player's `PositionUpdated` handler in `GameWindow` likely
|
||
applies these snapshots, producing the visible forward motion.
|
||
D. After `defaultMoveToTimeout` (server-side configurable, typically
|
||
on the order of seconds), ACE gives up. `MoveToChain` calls
|
||
`callback(false)`. Server-side, ACE may issue a position correction
|
||
that returns the player to where the chain started.
|
||
E. The local prediction (our `PlayerMovementController` running on
|
||
user input — which is "no movement keys held") reconciles toward
|
||
"stationary at original spot", producing the snap-back when ACE's
|
||
own corrections stop arriving.
|
||
|
||
The exact source of the visible motion (step C vs. some other path) is
|
||
unverified — flagged as **investigation work** below.
|
||
|
||
---
|
||
|
||
## Solution candidates
|
||
|
||
### Option A — Run the existing remote-driver for the local player
|
||
|
||
**Approach.** When `OnLiveMotionUpdated` sees `IsServerControlledMoveTo`
|
||
for `_playerServerGuid`, install the same per-tick steering machinery
|
||
that drives remote creatures: heading toward
|
||
`MoveToDestinationWorld`, run/walk cycle from
|
||
`ServerControlledLocomotion.PlanMoveToStart`, arrival detected via
|
||
`min_distance` (chase) / `distance_to_object` (flee).
|
||
|
||
While the auto-walk is active, suppress user-input driven movement so
|
||
the two control paths don't fight. On arrival or on a non-MoveTo motion
|
||
arriving, release auto-walk and restore user input.
|
||
|
||
**Pros.** Retail-faithful. Reuses well-tested infrastructure. Aligns
|
||
the local player's behavior with how every remote creature already
|
||
handles `MoveToObject`.
|
||
|
||
**Cons.** Largest implementation surface. Needs careful state-machine
|
||
design to prevent input/auto-walk fighting and ensure clean release on
|
||
ESC, arrival, packet cancel, target despawn, login transitions.
|
||
|
||
### Option B — Visual settle to arrival pose
|
||
|
||
**Approach.** When MoveToObject arrives, compute the approximate
|
||
arrival position via the holtburger
|
||
`approximate_move_to_object_projection_target` formula
|
||
(`target_pos - normalize(target_pos - source) × (DistanceToObject +
|
||
UseRadius)`). Smoothly tween the local-player camera/mesh from current
|
||
position to that arrival pose over the expected walk duration. Don't
|
||
send any extra packets; rely on ACE's own physics to walk the
|
||
authoritative position in parallel.
|
||
|
||
**Pros.** Smaller scope (~50 LOC). No state machine. No reconciliation
|
||
fight — the tween is purely visual; the underlying
|
||
`PlayerMovementController` is paused for the duration of the tween.
|
||
|
||
**Cons.** Not retail-faithful. Doesn't actually run the player's
|
||
physics integrator forward, so any side effects of normal movement
|
||
(footstep emit hooks, collision resolution mid-walk, environment
|
||
triggers) don't fire. Likely fine for an item pickup (no environment to
|
||
interact with mid-walk), but is a band-aid that doesn't generalize.
|
||
|
||
### Option C — Server-position-authoritative blend
|
||
|
||
**Approach.** Detect `IsServerControlledMoveTo` for the local player.
|
||
While active, treat inbound `UpdatePosition` as authoritative without
|
||
reconciliation — i.e. trust the server's interpolated positions and
|
||
suppress the local prediction's snap-back. Don't try to drive
|
||
local-side; let ACE's broadcasts drag us along.
|
||
|
||
**Pros.** Very small surface (~20 LOC). No new state machine; just a
|
||
flag that gates reconciliation.
|
||
|
||
**Cons.** Depends on ACE actually broadcasting per-tick UpdatePosition
|
||
during the auto-walk. The visible "walks then snaps" pattern suggests
|
||
it does broadcast for a while and then stops; need to confirm. If
|
||
ACE's broadcast cadence is too sparse, the motion is choppy.
|
||
|
||
---
|
||
|
||
## Recommendation
|
||
|
||
**Option A is the retail-faithful path; Options B and C are non-retail
|
||
shortcuts. Implement Option A.**
|
||
|
||
### Retail evidence (settles A vs C)
|
||
|
||
`MovementManager::PerformMovement` at retail address `0x00524440`
|
||
(decomp `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines
|
||
300628–300648) is the inbound-motion dispatcher. The switch on movement
|
||
type has explicit cases for `MoveToObject (6)` and `MoveToPosition (7)`
|
||
that:
|
||
|
||
1. Call `MovementManager::MakeMoveToManager(this)` to ensure the
|
||
per-physics-object manager exists.
|
||
2. Unpack the target guid + origin position + `MovementParameters` from
|
||
the wire.
|
||
3. Set `this->motion_interpreter->my_run_rate` from the packet.
|
||
4. Call **`CPhysicsObj::MoveToObject(this->physics_obj, target_guid,
|
||
¶ms)`** — which kicks off a **fully local** auto-walk on the
|
||
player's own physics body (`0x00512860`, decomp line 280598).
|
||
5. Fall through to `MoveToManager::MoveToPosition` only if the target
|
||
guid isn't found in the local physics world (rare; usually means the
|
||
client hasn't streamed in the target yet).
|
||
|
||
This is the **same code path** that runs for every remote creature
|
||
chasing the player — retail did not have a separate "remote-only" vs
|
||
"local-only" auto-walk pipeline. The local client's MoveToManager
|
||
actively drove the local player's body forward when the server sent
|
||
MoveToObject. ACE's server-side `PhysicsObj.MoveToObject` simulation
|
||
runs in parallel for authoritative-position tracking + arrival
|
||
detection (`MoveToChain.WithinUseRadius`), but the visible movement on
|
||
the local client comes from the local MoveToManager — not from
|
||
inbound UpdatePosition packets.
|
||
|
||
Option C would diverge from retail by relying on server position
|
||
broadcasts instead of local physics integration. That risks combat
|
||
movement, environment-trigger interactions, and animation hooks all
|
||
diverging from retail because they'd be driven by sparse server-side
|
||
position snapshots rather than smooth local integration.
|
||
|
||
### Existing acdream infrastructure that's already retail-shaped
|
||
|
||
We have most of the building blocks already:
|
||
|
||
- [`RemoteMoveToDriver`](../../../src/AcDream.Core/Physics/RemoteMoveToDriver.cs)
|
||
is the per-tick steering loop — heading correction, arrival via
|
||
`min_distance` / `distance_to_object`, ±20° aux-turn tolerance —
|
||
ported from retail's `MoveToManager::HandleMoveToPosition`
|
||
(`0x00529d80`). It's already exercised on every NPC chase. The
|
||
retail-faithful fix for the local player **reuses this driver**,
|
||
installed against the local player's body instead of a remote's
|
||
dead-reckoned body.
|
||
- [`ServerControlledLocomotion.PlanMoveToStart`](../../../src/AcDream.Core/Physics/ServerControlledLocomotion.cs)
|
||
already does what retail's `MovementParameters::get_command`
|
||
(`0x0052AA00`) does: seed `WalkForward` / `RunForward` depending on
|
||
`CanRun` + `MoveToSpeed` + `MoveToRunRate`.
|
||
- `MotionState.MoveToPath` is fully parsed on the wire. Remote chase
|
||
reads it at `GameWindow.cs:3401–3417`.
|
||
|
||
The B.6 work is essentially **"do for `_playerServerGuid` what we
|
||
already do for remotes,"** with one extra concern: the local player has
|
||
a user-input motion source (`PlayerMovementController`) that has to
|
||
yield to the auto-walk while it's active.
|
||
|
||
### Why not B
|
||
|
||
Tweens aren't retail-faithful and would diverge worse than C.
|
||
Eliminated.
|
||
|
||
---
|
||
|
||
## Required investigation before any code
|
||
|
||
The "walks then snaps back" symptom is observed but not yet
|
||
characterized in detail. We need a live trace of:
|
||
|
||
1. Outbound: timestamp + opcode + payload of the user's Use / PickUp
|
||
packet that triggers the auto-walk.
|
||
2. Inbound during the auto-walk: every `UpdateMotion`, `UpdatePosition`,
|
||
`VectorUpdate`, `WeenieError`, `InventoryServerSaveFailed` for the
|
||
local player, timestamped.
|
||
3. Visible client position at each inbound event (so we can correlate
|
||
"walks forward at frame N" with the inbound that caused it).
|
||
|
||
**Implementation step:** add a runtime-gated diagnostic
|
||
`ACDREAM_PROBE_AUTOWALK=1` that logs one line per relevant event during
|
||
local-player auto-walk attempts. Roughly 30 LOC; mirrors L.2a slice 1's
|
||
`ACDREAM_PROBE_RESOLVE` / `ACDREAM_PROBE_CELL` pattern.
|
||
|
||
The trace is no longer needed to *decide between options* (retail
|
||
decomp settles that — Option A wins), but it remains valuable as a
|
||
**baseline measurement** for the Option A implementation: knowing what
|
||
ACE sends today lets us verify the local driver behaves equivalently
|
||
on the wire (no extra packets needed, position broadcasts arrive at
|
||
expected cadence, the auto-walk completes inside
|
||
`defaultMoveToTimeout` instead of timing out).
|
||
|
||
---
|
||
|
||
## File-level scope sketch (Option A — retail-faithful)
|
||
|
||
Mirror retail's `MovementManager::PerformMovement` case 6 against
|
||
acdream's existing `PlayerMovementController` + `RemoteMoveToDriver`.
|
||
|
||
**Slice 1 — diagnostic baseline (~30 LOC).**
|
||
- **Modify:** `src/AcDream.Core/Physics/PhysicsDiagnostics.cs` — add
|
||
`ProbeAutoWalkEnabled` flag gated on `ACDREAM_PROBE_AUTOWALK=1`.
|
||
- **Modify:** `GameWindow.OnLiveMotionUpdated`, `OnLivePositionUpdated`,
|
||
`OnVectorUpdated`, `SendUse`, `SendPickUp` — when probe is on and the
|
||
guid is `_playerServerGuid`, log one line per event with timestamp +
|
||
payload. Mirror the `[resolve]` / `[cell-transit]` line format from
|
||
L.2a.
|
||
|
||
**Slice 2 — install auto-walk on inbound MoveToObject (~100 LOC).**
|
||
- **Modify:** `PlayerMovementController` — add `BeginServerAutoWalk(
|
||
Vector3 destinationWorld, float minDistance, float distanceToObject,
|
||
bool moveTowards, float moveToSpeed, float moveToRunRate, bool
|
||
canRun)` + `EndServerAutoWalk(reason)` methods. The controller owns
|
||
the "input vs auto-walk" state. While auto-walking, the per-tick
|
||
update calls `RemoteMoveToDriver.Step(...)` against its own body, and
|
||
the user-input motion path is suppressed.
|
||
- **Modify:** `GameWindow.OnLiveMotionUpdated` — when `update.Guid ==
|
||
_playerServerGuid && IsServerControlledMoveTo && MoveToPath is
|
||
not null`, translate the path's `OriginCellId + OriginXYZ` to world
|
||
space (same `RemoteMoveToDriver.OriginToWorld` helper the remote
|
||
path uses), call `_playerController.BeginServerAutoWalk(...)`.
|
||
Otherwise (a non-MoveTo motion arrives for the player), call
|
||
`EndServerAutoWalk(reason="motion-changed")`.
|
||
- **Modify:** `PlayerMovementController.Tick` — if auto-walking,
|
||
consume input only for cancellation (W/A/S/D pressed → cancel
|
||
auto-walk → restore input); skip the input-driven velocity solve;
|
||
let `RemoteMoveToDriver.Step` set the body's velocity + heading;
|
||
apply arrival check via `min_distance` / `distance_to_object`; on
|
||
arrival, call `EndServerAutoWalk(reason="arrived")`.
|
||
|
||
**Slice 3 — animation cycle selection (~20 LOC).**
|
||
- **Modify:** `GameWindow` `UpdatePlayerAnimation` (the path that
|
||
drives the local player's animation cycle from user input) — when
|
||
the controller is in `IsServerAutoWalking` state, source the cycle
|
||
from `ServerControlledLocomotion.PlanMoveToStart(...)` instead of the
|
||
user-input MotionInterpreter. This is what makes the local player
|
||
visibly walk + animate during the auto-walk.
|
||
|
||
**Slice 4 — local pickup animation (probably closes #64, ~10 LOC).**
|
||
- **Modify:** `OnLiveMotionUpdated` — for `_playerServerGuid`, allow
|
||
`Motion(Pickup)` / `Motion(Pickup5/10/15/20)` to drive `SetCycle`
|
||
bypassing the existing self-echo filter at `GameWindow.cs:3289`.
|
||
This is the bend-down animation retail observers see when we pick up
|
||
an item. Same one-shot mechanism retail used; `UpdatePlayerAnimation`
|
||
doesn't predict it because it's server-initiated, so admitting the
|
||
echo is correct.
|
||
|
||
Total: estimated ~160 LOC + unit tests for the controller state
|
||
machine. No new files; all changes land in existing physics + render
|
||
infrastructure.
|
||
|
||
---
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] Double-click a ground item from 2–5 m away. Character walks to
|
||
the item, picks it up, despawn fires, inventory updates.
|
||
- [ ] F-press on a selected item from 2–5 m away. Same behavior as
|
||
double-click.
|
||
- [ ] Use a Holtburg NPC from 2–5 m away. Character walks to the NPC,
|
||
chat dialogue appears (NPC interaction completes, like the
|
||
close-range case from M1 target 3).
|
||
- [ ] No regression on close-range pickup (B.5 path remains 1-tap
|
||
works).
|
||
- [ ] User can interrupt the auto-walk by pressing a movement key (W /
|
||
A / S / D). Auto-walk releases; input takes over; ACE's
|
||
`MoveToChain` reads "not arrived" + "user cancelled" and stops.
|
||
- [ ] Pressing ESC during auto-walk releases the auto-walk and returns
|
||
to normal player mode.
|
||
- [ ] No "walks then snaps back" visual artifact under any of the
|
||
scenarios above.
|
||
|
||
---
|
||
|
||
## Out of scope (deferred to later phases)
|
||
|
||
- `MoveToPosition` (movement type 7) for the local player — typically
|
||
used by portal traversal and admin-teleport flows. M4 territory.
|
||
- `Stick` / sticky-target tracking — used by combat chase. M2/M3
|
||
territory.
|
||
- Sphere-cylinder distance variant — relevant for vendor / corpse
|
||
interactions where the target has non-trivial extent. Can be added
|
||
in B.6's follow-up commit if needed.
|
||
- Local player's `MoveToObject` initiation via wire (we currently rely
|
||
on ACE to start the auto-walk on the player's behalf — that's the
|
||
retail behavior and we don't need a client-initiated path).
|
||
|
||
---
|
||
|
||
## Carry-overs from B.5
|
||
|
||
- **#64** — local-player pickup animation. Same self-echo filter at
|
||
`OnLiveMotionUpdated:3289` that ignores MoveToObject also drops the
|
||
inbound `Motion(Pickup)` retail observers render correctly. Slice 4
|
||
above admits server-initiated one-shot motions through the filter
|
||
for the local player, which should close #64. Verify in the same
|
||
visual-test pass.
|
||
|
||
---
|
||
|
||
## State at design freeze
|
||
|
||
- **Main HEAD:** `281d125` (initial B.6 design spec committed).
|
||
- **No code changes in this spec commit** — design document only.
|
||
- **Spec update 2026-05-14 (this commit):** retail decomp at
|
||
`MovementManager::PerformMovement` (`0x00524440` case 6, decomp lines
|
||
300628–300648) decisively settles A vs C in favor of **Option A**.
|
||
Retail's local client ran its own `MoveToManager` and called
|
||
`CPhysicsObj::MoveToObject` on the local player's body. Option C
|
||
(server-position-blend) is not retail-faithful and is no longer
|
||
considered.
|
||
- **Slice 1 shipped 2026-05-14** (`eda8278` + `1b4f3ba`):
|
||
`ACDREAM_PROBE_AUTOWALK` diagnostic + DebugPanel checkbox.
|
||
|
||
### Trace-captured findings (post-Slice-1)
|
||
|
||
A live trace captured at Holtburg with the probe enabled —
|
||
double-clicking `+Je` (a remote player at `(111.34, 5.96, 94.01)` in
|
||
cell `0xA9B40021`) from ~3.5 m away, 4 successive Use sends. Findings:
|
||
|
||
1. **Parser confirmed correct.** Each `[autowalk-mt]` line reads
|
||
`mt=0x06 isMoveTo=True moveTowards=True
|
||
path=cell=0xA9B40021,xyz=(111.34,5.96,94.01),minDist=0.00,objDist=0.50
|
||
mtSpd=1.00 mtRun=0.00`. Matches ACE's
|
||
`MoveToObject.Write` + `MoveToParameters.Write` byte-for-byte.
|
||
2. **ACE sends `mtRun=0.00`** — not a parser bug. ACE's
|
||
`Player_Move.MoveTo` default for unspecified `runRate` is `0.0f`,
|
||
and the call into MoveToObject uses that wire value directly. Retail
|
||
decomp at `0x005245e9` copies the wire value into
|
||
`motion_interpreter->my_run_rate`; if 0, the local MoveToManager
|
||
falls back to the player's own run-rate via
|
||
`CMotionInterp::apply_run_to_command` (`0x00527BE0`).
|
||
3. **Player position never changed** during the entire trace. All
|
||
`[autowalk-up]` lines after the 4 Use sends report
|
||
`pos=(112.32, 9.36, 94.00)` verbatim — current behavior is pure
|
||
no-op on the inbound MoveToObject.
|
||
4. **ACE does NOT broadcast `UpdatePosition` for the local player
|
||
during the auto-walk.** This kills Option C even more firmly than
|
||
the retail decomp did. The local body has to drive itself.
|
||
5. **Slice 2 must handle `mtRun == 0.0`** — fall back to the player's
|
||
own current run rate. The trace also captured `fwdSpd=2.86` for
|
||
the user's normal running before the auto-walk attempt — that's
|
||
the server-echoed `ForwardSpeed`, available as a recent-history
|
||
source for the fallback. Default `1.0` if no echo yet.
|
||
|
||
### Next session entry point
|
||
|
||
Slice 2: add `PlayerMovementController.BeginServerAutoWalk` /
|
||
`EndServerAutoWalk` + per-tick steering via `RemoteMoveToDriver`-style
|
||
heading + arrival. Wire in `GameWindow.OnLiveMotionUpdated` with the
|
||
`mtRun=0` fallback chain. Suppress user-input motion while
|
||
auto-walking; cancel on movement-key press.
|