diff --git a/docs/superpowers/specs/2026-05-14-phase-b6-design.md b/docs/superpowers/specs/2026-05-14-phase-b6-design.md new file mode 100644 index 0000000..214a683 --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-phase-b6-design.md @@ -0,0 +1,298 @@ +# 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.