# 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 Start with **Option C** for the minimum viable fix; promote to **Option A** if Option C produces choppy/janky motion. Skip Option B unless A and C both prove harder than they look. **Why C first.** - Smallest blast radius. Can be a single-commit hotfix. - Reveals exactly what ACE is sending in practice (the diagnostic layer below tells us cadence + format). - If Option C works smoothly, B.6 is done in a single session. - If Option C is too choppy, the diagnostic data informs Option A's design — what cadence does the per-tick driver need to fill in between server-position broadcasts? **Why not B.** Tweens aren't retail-faithful; we'd carry a non-retail visual band-aid into M2 and have to revisit when combat movement starts depending on real physics paths. --- ## 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 first 30 minutes of the next B.6 session should produce a clean trace of a single failed auto-walk attempt, from outbound packet through ACE's `ActionCancelled`. The trace decides between Option A and Option C. --- ## File-level scope sketch (Option C path) If the trace confirms ACE broadcasts adequate `UpdatePosition` during auto-walk: - **Modify:** [`src/AcDream.Core/Physics/PlayerMovementController.cs`](../../../src/AcDream.Core/Physics/PlayerMovementController.cs) — add an `IsServerAutoWalking` flag + setter. While true, suppress the user-input snap-back path in the reconciliation loop. - **Modify:** [`src/AcDream.App/Rendering/GameWindow.cs`](../../../src/AcDream.App/Rendering/GameWindow.cs) `OnLiveMotionUpdated` — when `update.Guid == _playerServerGuid` and `IsServerControlledMoveTo`, set the flag on the controller. When a non-MoveTo motion arrives for the player (Ready, RunForward initiated by user, etc.), clear it. - **Modify:** `GameWindow.OnLivePositionUpdated` (or equivalent) — while the auto-walk flag is set, trust server position without reconciliation. Total: estimated ~60 LOC + diagnostic. ## File-level scope sketch (Option A path, if needed) - All of Option C's changes, plus: - **New:** `src/AcDream.Core/Physics/LocalAutoWalkDriver.cs` — port the `RemoteMoveToDriver` logic adapted for the local player's body, including arrival detection + cycle selection. - **Modify:** `PlayerMovementController` — integrate the driver into the per-tick update loop when auto-walking; suppress input-derived velocity while active. - **Modify:** `GameWindow.OnLiveMotionUpdated` — wire the driver installation + teardown. Total: estimated ~150–250 LOC including tests. --- ## 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. Likely related: same self- echo filter at `OnLiveMotionUpdated:3289` that ignores MoveToObject also drops the inbound `Motion(Pickup)` that retail observers render correctly. May fix in the same B.6 work, or as a follow-up depending on how the auto-walk fix shakes out. --- ## State at design freeze - **Main HEAD:** `5053e40` (post-B.5 polish; M1 mechanically clean). - **No code changes in this commit** — design document only. - **Next session entry point:** add the `ACDREAM_PROBE_AUTOWALK` diagnostic, run a failed auto-walk reproduction, decide A vs C from the trace.