docs(B.6): retail decomp settles Option A; revise spec with 4-slice plan

Grounded the design in named-retail evidence. MovementManager::Perform
Movement at 0x00524440 case 6 (decomp lines 300628-300648) shows the
retail client's local-side dispatcher for inbound MoveToObject:
unpacks the wire, sets motion_interpreter->my_run_rate, calls
CPhysicsObj::MoveToObject on the LOCAL player's physics body. Same
code path retail used for every creature chasing the player.

Conclusion: Option A (run a local driver against the player's body)
is retail-faithful. Option C (server-position-blend) is a non-retail
shortcut and is now eliminated from consideration.

Re-scoped the spec into 4 slices:
  1. ACDREAM_PROBE_AUTOWALK diagnostic baseline (~30 LOC)
  2. PlayerMovementController.BeginServerAutoWalk + reuse of
     RemoteMoveToDriver against the local player's body (~100 LOC)
  3. Animation cycle selection during auto-walk (~20 LOC)
  4. Local pickup-animation echo (closes #64, ~10 LOC)

Total ~160 LOC, no new files. All existing acdream infrastructure
(RemoteMoveToDriver, ServerControlledLocomotion, MotionState.MoveTo
Path parsing) is reused; the work is wiring it for _playerServerGuid
in addition to remote guids.
This commit is contained in:
Erik 2026-05-14 17:56:35 +02:00
parent 281d125e9b
commit 9e1d33a5f7

View file

@ -164,22 +164,73 @@ ACE's broadcast cadence is too sparse, the motion is choppy.
## Recommendation ## Recommendation
Start with **Option C** for the minimum viable fix; promote to **Option A is the retail-faithful path; Options B and C are non-retail
**Option A** if Option C produces choppy/janky motion. Skip Option B shortcuts. Implement Option A.**
unless A and C both prove harder than they look.
**Why C first.** ### Retail evidence (settles A vs C)
- 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 `MovementManager::PerformMovement` at retail address `0x00524440`
non-retail visual band-aid into M2 and have to revisit when combat (decomp `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines
movement starts depending on real physics paths. 300628300648) 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,
&params)`** — 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:34013417`.
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.
--- ---
@ -201,45 +252,72 @@ characterized in detail. We need a live trace of:
local-player auto-walk attempts. Roughly 30 LOC; mirrors L.2a slice 1's local-player auto-walk attempts. Roughly 30 LOC; mirrors L.2a slice 1's
`ACDREAM_PROBE_RESOLVE` / `ACDREAM_PROBE_CELL` pattern. `ACDREAM_PROBE_RESOLVE` / `ACDREAM_PROBE_CELL` pattern.
The first 30 minutes of the next B.6 session should produce a clean The trace is no longer needed to *decide between options* (retail
trace of a single failed auto-walk attempt, from outbound packet decomp settles that — Option A wins), but it remains valuable as a
through ACE's `ActionCancelled`. The trace decides between Option A **baseline measurement** for the Option A implementation: knowing what
and Option C. 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 C path) ## File-level scope sketch (Option A — retail-faithful)
If the trace confirms ACE broadcasts adequate `UpdatePosition` during Mirror retail's `MovementManager::PerformMovement` case 6 against
auto-walk: acdream's existing `PlayerMovementController` + `RemoteMoveToDriver`.
- **Modify:** [`src/AcDream.Core/Physics/PlayerMovementController.cs`](../../../src/AcDream.Core/Physics/PlayerMovementController.cs) **Slice 1 — diagnostic baseline (~30 LOC).**
— add an `IsServerAutoWalking` flag + setter. While true, suppress - **Modify:** `src/AcDream.Core/Physics/PhysicsDiagnostics.cs` — add
the user-input snap-back path in the reconciliation loop. `ProbeAutoWalkEnabled` flag gated on `ACDREAM_PROBE_AUTOWALK=1`.
- **Modify:** [`src/AcDream.App/Rendering/GameWindow.cs`](../../../src/AcDream.App/Rendering/GameWindow.cs) - **Modify:** `GameWindow.OnLiveMotionUpdated`, `OnLivePositionUpdated`,
`OnLiveMotionUpdated` — when `update.Guid == _playerServerGuid` and `OnVectorUpdated`, `SendUse`, `SendPickUp` — when probe is on and the
`IsServerControlledMoveTo`, set the flag on the controller. When a guid is `_playerServerGuid`, log one line per event with timestamp +
non-MoveTo motion arrives for the player (Ready, RunForward initiated payload. Mirror the `[resolve]` / `[cell-transit]` line format from
by user, etc.), clear it. L.2a.
- **Modify:** `GameWindow.OnLivePositionUpdated` (or equivalent) — while
the auto-walk flag is set, trust server position without
reconciliation.
Total: estimated ~60 LOC + diagnostic. **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")`.
## File-level scope sketch (Option A path, if needed) **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.
- All of Option C's changes, plus: **Slice 4 — local pickup animation (probably closes #64, ~10 LOC).**
- **New:** `src/AcDream.Core/Physics/LocalAutoWalkDriver.cs` — port the - **Modify:** `OnLiveMotionUpdated` — for `_playerServerGuid`, allow
`RemoteMoveToDriver` logic adapted for the local player's body, including `Motion(Pickup)` / `Motion(Pickup5/10/15/20)` to drive `SetCycle`
arrival detection + cycle selection. bypassing the existing self-echo filter at `GameWindow.cs:3289`.
- **Modify:** `PlayerMovementController` — integrate the driver into the This is the bend-down animation retail observers see when we pick up
per-tick update loop when auto-walking; suppress input-derived an item. Same one-shot mechanism retail used; `UpdatePlayerAnimation`
velocity while active. doesn't predict it because it's server-initiated, so admitting the
- **Modify:** `GameWindow.OnLiveMotionUpdated` — wire the driver echo is correct.
installation + teardown.
Total: estimated ~150250 LOC including tests. Total: estimated ~160 LOC + unit tests for the controller state
machine. No new files; all changes land in existing physics + render
infrastructure.
--- ---
@ -281,18 +359,28 @@ Total: estimated ~150250 LOC including tests.
## Carry-overs from B.5 ## Carry-overs from B.5
- **#64** — local-player pickup animation. Likely related: same self- - **#64** — local-player pickup animation. Same self-echo filter at
echo filter at `OnLiveMotionUpdated:3289` that ignores MoveToObject `OnLiveMotionUpdated:3289` that ignores MoveToObject also drops the
also drops the inbound `Motion(Pickup)` that retail observers render inbound `Motion(Pickup)` retail observers render correctly. Slice 4
correctly. May fix in the same B.6 work, or as a follow-up depending above admits server-initiated one-shot motions through the filter
on how the auto-walk fix shakes out. for the local player, which should close #64. Verify in the same
visual-test pass.
--- ---
## State at design freeze ## State at design freeze
- **Main HEAD:** `5053e40` (post-B.5 polish; M1 mechanically clean). - **Main HEAD:** `281d125` (initial B.6 design spec committed).
- **No code changes in this commit** — design document only. - **No code changes in this spec commit** — design document only.
- **Next session entry point:** add the `ACDREAM_PROBE_AUTOWALK` - **Spec update 2026-05-14 (this commit):** retail decomp at
diagnostic, run a failed auto-walk reproduction, decide A vs C from `MovementManager::PerformMovement` (`0x00524440` case 6, decomp lines
the trace. 300628300648) 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.
- **Next session entry point:** Slice 1 — add the
`ACDREAM_PROBE_AUTOWALK` diagnostic as the baseline, run a failed
auto-walk reproduction for a clean trace, then proceed to Slice 2
(`PlayerMovementController.BeginServerAutoWalk` + `RemoteMoveToDriver`
reuse for the local player).