docs(B.6): design spec for local-player MoveToObject auto-walk (issue #63)
Captures the wire-format facts that are already parsed (MotionState. IsServerControlledMoveTo + MoveToPath fields), the two gating sites that drop the inbound MoveToObject for the local player today, and a three-option solution space (run remote-driver locally, visual tween, server-position-authoritative blend). Recommendation: Option C first (smallest blast radius, single-commit hotfix if ACE's UpdatePosition broadcast cadence is adequate); promote to Option A only if the trace shows server broadcasts are too sparse to render smoothly. Explicitly does NOT implement yet. The 'walks then snaps back' visible symptom is observed but the mechanism isn't characterized in detail — the spec calls for a diagnostic-trace session first (ACDREAM_PROBE_ AUTOWALK env var, ~30 LOC) to capture exactly what ACE sends during a failed auto-walk. The trace decides between Option C (sufficient position broadcasts) and Option A (need to fill in per-tick locally). #64 (local pickup animation) is flagged as likely-related — same OnLiveMotionUpdated:3289 self-echo filter drops both. May fix in the same B.6 work.
This commit is contained in:
parent
5053e40e6f
commit
281d125e9b
1 changed files with 298 additions and 0 deletions
298
docs/superpowers/specs/2026-05-14-phase-b6-design.md
Normal file
298
docs/superpowers/specs/2026-05-14-phase-b6-design.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue