Workflow-produced R4 research (3 docs, 2,541 lines): - r4-moveto-decomp.md: all 33 MoveToManager members verbatim (0x00529010-0x0052a987) + the MovementManager type-6/7/8/9 relay + MovementParameters::get_command. HandleUpdateTarget CONFIRMED on MoveToManager (0x0052a7d0 — closes the R3 negative result; object moves are DEFERRED until the first target-update callback). Walk-vs- run = the CanCharge rule exactly (can_charge OR can_run && dist-gap > walk_run_threshhold, riding hold_key_to_apply into DoInterpretedMotion — MoveToManager never calls set_hold_run). Sticky handoff located (empty queue + 0x80 → StickTo). fail_progress_count is WRITE-ONLY in retail (no give-up threshold — do not invent one). 8-class BN artifact ledger. - r4-ace-moveto.md: 16 flagged ACE-isms, all retail-verified — incl. a stale-member read (MoveToPosition checks the PREVIOUS move's UseFinalHeading), a field transposition (InitializeLocalVars zeroes DistanceToObject where retail zeroes the flags word), a dropped BeginNextNode (ACE turns start one tick late), a UseTime gate inversion, and the canceling CanCharge default+fast-path pair. Blast radius: acdream has THREE independent approximations of this one mechanism (DriveServerAutoWalk / RemoteMoveToDriver / ServerControlledLocomotion) + an outdated chase/flee claim in RemoteMoveToDriver's class doc. - r4-port-plan.md: 7 pins (P1 blocker: retail interrupts current movement on EVERY UM but ACE sends a companion mt-0 echo after each MoveTo — needs a discriminator pin before V5; P3: heading_diff's TurnLeft mirror needs one Ghidra decompile of 0x00528fb0; P4: type-6/8 moves need a minimal TargetTracker adapter until R5), 17 gaps M1-M17, commits V0-V6. Retires AD-8/AD-9/AP-8/AP-9 + TS-36. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
452 lines
46 KiB
Markdown
452 lines
46 KiB
Markdown
# R4 port work-list — MoveToManager verbatim + the three-approximation cutover
|
||
|
||
Inputs: `r4-moveto-decomp.md` (verbatim retail extraction, same dir),
|
||
`r4-ace-moveto.md` (ACE cross-ref + blast radius, same dir), plan of record
|
||
`docs/plans/2026-07-02-retail-motion-animation-rewrite.md` (stage R4), R3 pins
|
||
`docs/research/2026-07-02-r3-motioninterp/W0-pins.md` (A4 MovementParameters masks —
|
||
used as-is), current code `src/AcDream.Core/Physics/RemoteMoveToDriver.cs` (340
|
||
lines, static), `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (87 lines),
|
||
`src/AcDream.App/Input/PlayerMovementController.cs` (1604 lines, post-R3-W6; B.6
|
||
auto-walk block :275-:766 + :896), `src/AcDream.App/Rendering/GameWindow.cs`
|
||
(local MoveTo routing :4401-4547, remote seeding :4549-4620, remote per-tick
|
||
:9594-9682, speculative install :11915/:12004/:12122),
|
||
`src/AcDream.Core.Net/Messages/UpdateMotion.cs` (mt 6/7 parse :252-356),
|
||
`src/AcDream.Core/Physics/Motion/MovementParameters.cs` (R3-W1, flags+scalars only),
|
||
`src/AcDream.Core/Physics/MotionInterpreter.cs` (R3-complete: DoInterpretedMotion
|
||
:2861, StopInterpretedMotion :3016, adjust_motion :1241, MotionsPending :2118,
|
||
InterruptCurrentMovement seam :645, UnstickFromObject seam :633, MyRunRate :585).
|
||
|
||
**Precondition / state at R4 start:** R1/R2/R3 SHIPPED (R3 through W6 `fb7beb70`;
|
||
R2+R3 share one pending visual pass — R4 code work does not block on it). The
|
||
R3 seams R4 consumes all exist: `DoInterpretedMotion(uint, MovementParameters)` /
|
||
`StopInterpretedMotion(uint, MovementParameters)` / `adjust_motion(ref,ref,HoldKey)`
|
||
(the entire retail `_DoMotion` shape), `MotionsPending()`, `TransientStateFlags.Contact`
|
||
on `PhysicsObj`, `HitGround`/`LeaveGround` (W4), the `InterruptCurrentMovement`
|
||
no-op `Action?` seam (register TS-36 — "R4 is where cancel_moveto lands"), and
|
||
`MovementParameters` with retail ctor defaults (0x1EE0F, threshhold 15, CanCharge
|
||
false). What does NOT exist: any MoveToManager, the command-selection family
|
||
(`get_command`/`towards_and_away`/`get_desired_heading`), MovementType values
|
||
0/6/7/8/9, WeenieError 0xB/0x36/0x37/0x38/0x3D, mt 8/9 wire parsing, and any
|
||
heading/cylinder-distance helper in Core. MovementManager itself is **R5** —
|
||
R4 binds MoveToManager to the interp directly and keeps type-dispatch at the
|
||
existing call sites (same "null-guarded relay inserted later without behavior
|
||
change" pattern R3 §4 used for MotionDone).
|
||
|
||
---
|
||
|
||
## 0. DECOMP AMBIGUITIES TO PIN before porting (the V0 pins commit)
|
||
|
||
| # | Ambiguity | Evidence each way | Pin method |
|
||
|---|---|---|---|
|
||
| P1 | **ACE's companion mt-0 echo vs retail's unpack-head cancel — the V5 landmine.** Retail `MovementManager::unpack_movement` (0x00524440 @300566-300598) calls `CPhysicsObj::interrupt_current_movement` (→ CancelMoveTo 0x36) at the HEAD of EVERY movement event, including case 0. But ACE follows every mt=0x06 MoveToObject with an mt=0x00 InterpretedMotionState echo (cmd=RunForward, fwdSpd≈2.86) one packet later — trace-verified 2026-05-14 (`launch-slice2.log`; the finding is preserved verbatim in GameWindow.cs:4534-4546: "Cancelling on the InterpretedMotionState killed the auto-walk on frame 1"). A verbatim head-interrupt port re-breaks auto-walk on frame 1 against ACE. Retail servers evidently never sent an interpreted UM mid-moveto to the mover (any interpreted UM IS the cancel+replace signal). | decomp §2f head vs GameWindow:4534-4546 trace note; ACE `Player.OnMoveComplete`/MoveToChain broadcast code | (a) Re-capture the echo with `ACDREAM_PROBE_AUTOWALK=1` + `ACDREAM_DUMP_MOTION=1` and record the echo's movement/serverControl sequence stamps + isAutonomous flag vs the mt-6 packet's; (b) read ACE's broadcast path (does the echo bump ServerControlSequence? is it flagged autonomous?) for a principled discriminator; (c) if no wire discriminator exists, the adaptation is: **suppress the head-interrupt for a local-player mt-0 UM whose interpreted ForwardCommand equals the moveto's `current_command` (post-adjust) while `is_moving_to()`** — an ACE-compat register row (AD class, sibling of AD-32), NOT a silent divergence. Whatever the pin, mt 6/7/8/9 arrivals keep the retail unconditional cancel (MoveToManager::PerformMovement re-cancels anyway, §3a). |
|
||
| P2 | **`get_desired_heading` (0x0052aad0) 180/0 constants.** BN body is an x87 setcc garble (decomp §5e); ACE gives `WalkForward\|RunForward → movingAway ? 180 : 0`, `WalkBackwards → movingAway ? 0 : 180`, default 0. The command GROUPING is visible in the BN text; the constants are not. | decomp §5e vs ACE MovementParameters.cs:186-198 | One Ghidra MCP decompile of `0x0052aad0` (`/decompile_function?address=0x0052aad0`) when a CodeBrowser is up (was down during extraction — decomp §10). Confidence already high; port ACE's shape if Ghidra stays down, with the pin noted UNVERIFIED-BY-GHIDRA in the pseudocode doc. |
|
||
| P3 | **`heading_diff` (0x00528fb0) third-arg mirror — the two research docs CONTRADICT.** r4-moveto-decomp §5g: "arg3 (the turn command) is UNUSED in the body — keep the parameter for signature parity". r4-ace-moveto §1: ACE has `if (result > EPSILON && motion != TurnRight) result = 360 - result` and says the mirror is "invisible in the pseudo-C — verify once in Ghidra". Behavior-bearing: `HandleTurnToHeading` @0052a1bc passes the LIVE `current_command` (can be TurnLeft) into heading_diff for its progress test — with the mirror, a TurnLeft turn measures progress as 360−raw; without it, the progress window `(ε, 180)` reads the wrong side and TurnLeft turns would increment fail_progress_count every tick (harmless in retail — the counter is dead — but the `previous_heading` update path differs). | decomp §5g vs ACE MoveToManager.cs:817-828 | Ghidra decompile of `0x00528fb0` — decisive (the fn is 40 lines). If Ghidra stays down: cdb bp on `acclient!heading_diff` logging arg3+ST0 during a TurnLeft-direction TurnToHeading. Do not port either reading unpinned. |
|
||
| P4 | **TargetManager scope — what feeds `HandleUpdateTarget` for type 6/8.** Retail: `set_target` registers the mover as a voyeur on the target (radius 0.5, quantum 0); the target's movement fans back via `receive_target_update` → `HandleUpdateTarget` (decomp §9f; ACE `TargetManager.cs` with a 0.5 s `HandleTargetting` cadence + 10 s timeout). The r4 extraction covers only the CALL SHAPES — TargetManager bodies were NOT extracted. Without SOME feed, `initialized` never flips and type-6/8 moves never start (UseTime gate §6a). | decomp §9f (shapes only) vs ACE TargetManager.cs (full impl, server-side) | DECISION (made here, confirm in V0): R4 ships a **minimal App-side `TargetTracker` adapter**, not a TargetManager port: on `set_target(0, tlid, 0.5, 0)` GameWindow's entity table delivers one immediate `TargetInfo{ObjectId, Ok, target_position=entity pos, interpolated_position=same}`, then re-delivers per tick when the target has moved > 0.5 units since last delivery (the voyeur radius from the call site); `clear_target` unsubscribes; despawn/teleport of the target delivers `status=ExitWorld/Teleported` (→ CancelMoveTo 0x37/0x38 falls out of the verbatim manager). `set_target_quantum` is accepted and recorded but does NOT throttle the tracker (retail uses it to slow updates; over-delivery is convergence-safe). Register row (AP class) replacing AP-8's "no target re-tracking" clause; the full TargetManager port is R5 scope alongside PositionManager. If V0 finds this too loose, the fallback pin is extracting `TargetManager::SetTarget/ReceiveUpdate/HandleTargetting` from the raw (grep `TargetManager::` — est. ~200 lines) and porting verbatim in V4 instead. |
|
||
| P5 | **Position::heading convention + coordinate space.** MoveToManager consumes `Position::heading(from, to)` (degrees), `Frame::get_heading/set_heading`, `Position::distance`, `Position::cylinder_distance`. acdream has NO Core heading helper (only `SceneryHelpers.SetHeading`); RemoteMoveToDriver derives yaw from quaternions ad hoc. Wire `DesiredHeading` is degrees in AC convention. The manager will run in acdream's streaming-world space (Vector3 + cell id) via `OriginToWorld`, not retail's block-local Position — distances are equivalent (Euclidean after rebase); headings must match the wire convention exactly or `use_final_heading` faces the wrong way. | acclient Position::heading (not in this extraction); `RemoteMoveToDriver.OriginToWorld` :263-277; L.2b outbound heading packers (already convert yaw→AC heading) | Grep the named raw for `Position::heading` + `Frame::get_heading` and pin the formula (expected: heading = fmod(450 − atan2-degrees, 360) or the L.2b-established equivalent — the outbound AP packer already encodes body yaw→AC heading; REUSE that exact conversion, don't derive a second one). Golden test: heading(a,b) for the four cardinal offsets == {0, 90, 180, 270} per the wire convention ACE round-trips. |
|
||
| P6 | **TurnToObject (mt 8) wire layout.** Decomp §2f case 8 reads `object_id` dword, then a `wire_heading` dword, THEN UnPackNet's 3 dwords (bitfield, speed, desired_heading); on unresolvable object it substitutes `desired_heading = wire_heading` and degrades to TurnToHeading. Two heading fields exist on the wire; ACE's server-side writer is the cheap cross-check for which is which. | decomp §2f @300653-300676; ACE MovementEvents TurnToObject writer | Read ACE's `MoveToObject/TurnToObject` GameMessage writers (Network/GameMessages) and byte-diff against the decomp read order; build the V3 parse fixtures from ACE's writer directly (golden bytes). |
|
||
| P7 | **Remote contact for the UseTime gate.** `MoveToManager::UseTime` gates on `transient_state & 1` (CONTACT). Remotes' `PhysicsObj.TransientState` is maintained well enough that R3's funnel `contact_allows_move` works for remote Falling dispatch — but confirm the Contact bit specifically is live for grounded NPCs (not just OnWalkable), else remote movetos stall forever. | MotionInterpreter.cs:1979/2089 (funnel reads Contact+OnWalkable for remotes today) | Code read of the remote PhysicsBody state writers + one `ACDREAM_DUMP_MOTION` smoke: a chasing NPC must pass the gate every grounded tick. If remotes lack the bit, fixing the STATE WRITER is in scope (root cause), not softening the gate. |
|
||
|
||
**V0 also commits the research docs into the repo**
|
||
(`docs/research/2026-07-03-r4-movetomanager/`: the decomp extraction, the ACE
|
||
cross-ref, this plan, and a `V0-pins.md` in the W0-pins format), and fixes the
|
||
two known stale comments so nobody ports against them: RemoteMoveToDriver.cs:53-57
|
||
("ACE swaps the chase/flee arrival predicates" — WRONG, refuted in r4-ace-moveto §1;
|
||
the file dies in V4 but V0-readers must not absorb the claim) — a doc-only touch.
|
||
|
||
**V0 cdb capture (optional, non-blocking):** all seven pins are textually
|
||
resolvable (P1 needs an ACE trace, not retail cdb). If a retail session happens
|
||
anyway: bp `MoveToManager::PerformMovement` / `BeginMoveForward` /
|
||
`BeginTurnToHeading` / `HandleMoveToPosition` / `HandleUpdateTarget` /
|
||
`MovementParameters::get_command` (args+ret) while the user runs: use a door at
|
||
range (MoveToObject + arrival), use while facing away (TurnToHeading node first),
|
||
let a monster chase (aux-turn band), outrun it (fail-distance NOT firing — dead
|
||
counter), /follow-style sticky if reachable. Feeds V2/V4 goldens; synthetic +
|
||
ACE-writer fixtures suffice without it.
|
||
|
||
---
|
||
|
||
## 1. ITEMIZED GAPS — current vs retail (R4 scope)
|
||
|
||
Severity: **BLOCKER** = stage deliverable impossible without it; **HIGH** =
|
||
visible behavior wrongness / blocks R5-R6; **MED** = edge-visible; **LOW** = textual.
|
||
|
||
| # | Retail behavior acdream lacks/diverges on | Retail anchor | Current-code anchor | Severity |
|
||
|---|---|---|---|---|
|
||
| M1 | **No MoveToManager exists** — no state block (movement_type, sought/current_target/starting positions, params copy, progress clocks, current/aux command, moving_away, initialized), no `pending_actions` node queue, none of the 33 members. THREE independent approximations stand in for the one retail mechanism: `DriveServerAutoWalk` (local player), `RemoteMoveToDriver.Drive` (remotes), `ServerControlledLocomotion.PlanMoveToStart` (animation seed). | struct acclient.h:31473; whole extraction §§1-7 | RemoteMoveToDriver.cs; PlayerMovementController.cs:567-766; ServerControlledLocomotion.cs:28-53; GameWindow.cs:4412/4942 | **BLOCKER** |
|
||
| M2 | **Command-selection family absent**: `get_command` (0x0052aa00 — move/flee band pick + THE walk-vs-run rule: CanCharge 0x10 fast-path → Run, else CanRun && (!CanWalk \|\| dist−dto > threshhold 15) → Run else walk), `towards_and_away` (0x0052a9a0 — WalkBackwards inside the min band), `get_desired_heading` (0x0052aad0, P2). B.6's one-shot wire-CanCharge decision (#77) approximates the fast-path only — no per-tick re-evaluation, so a running auto-walk never demotes to walk-pace near the target (the R3 visual-pass expected-diff "auto-walk-at-run walk-pace legs (R4)"). `PlanMoveToStart` has NO distance logic at all. | decomp §5c/§5d/§5e | PlayerMovementController.cs:452-487 (`_autoWalkInitiallyRunning` one-shot), :701-706; ServerControlledLocomotion.cs:35-53 | **BLOCKER** |
|
||
| M3 | **No node plan** (`[TurnToHeading(face)] → [MoveToPosition] → [TurnToHeading(final)]?` with `BeginNextNode` stepping): auto-walk's turn-first phase + 30° walk-while-turning band are invented substitutes (AD-26's 5°/30°); the `use_final_heading` (0x40) node is entirely missing — wire `DesiredHeading` is parsed and thrown away. | §3c/§4a/§4b/§6f | PlayerMovementController.cs:606-706; CreateObject.MoveToPathData.DesiredHeading unused (r4-ace §5 wire layer) | **BLOCKER** |
|
||
| M4 | **No aux-command steering**: retail corrects heading DURING the move by issuing TurnRight/TurnLeft as interpreted motions through `_DoMotion` when \|diff\| leaves the ±20° deadband, and STOPS them re-entering it — observers see actual turn cycles. acdream rotates remotes by quaternion math (`RemoteMoveToDriver` π/2 rad/s + the borrowed ACE snap) and the local player via yaw writes — no turn motions dispatched, wrong legs. | HandleMoveToPosition Phase 1 @307225-307280 (20/340 deadband) | RemoteMoveToDriver.cs:68 (snap tolerance), :236 (ACE set_heading fudge); PlayerMovementController.cs:658-672 | **HIGH** |
|
||
| M5 | **Arrival predicate approximated + wrong distance metric**: retail `GetCurrentDistance` uses `cylinder_distance(own r/h, target r/h)` when `use_spheres` (0x400 — set in every ACE MoveTo packet) — edge-to-edge; acdream measures center-distance and compensates with AD-8's `max(minDistance, distanceToObject)` hack + an invented `ArrivalEpsilon 0.05` + the AD-26 facing gate. Retail: `moving_away ? dist ≥ min_distance : dist ≤ distance_to_object`, distance-only. | GetCurrentDistance §5a; arrival @307306-307331 (adjudicated r4-ace §1) | RemoteMoveToDriver.cs:161 (AD-8), :170-254; PlayerMovementController.cs:606-620 (AD-26 gate at :618) | **HIGH** |
|
||
| M6 | **No progress detector / fail distance**: `CheckProgressMade` (1 s window, 0.25 units/s incremental AND overall), `fail_distance` → CancelMoveTo(0x3D YouChargedTooFar), the FLT_MAX progress-clock reseeds on retarget. acdream substitutes the AD-9 1.5 s staleness timer (a liveness guard retail doesn't have) and parses FailDistance into `MoveToPathData` without a consumer. | CheckProgressMade §5b; @307300-307331; HandleUpdateTarget retarget @307843-307861 | RemoteMoveToDriver.cs:136 (AD-9); CreateObject.cs MoveToPathData.FailDistance unused | **HIGH** |
|
||
| M7 | **Types 8/9 (TurnToObject/TurnToHeading) dropped end-to-end** — the wire parser handles mt 6/7 only (mt 8/9 fall through to an empty Parsed: heading + params silently discarded); no consumer exists. These are the plan-of-record's "dropped D9/DEV-5 commands". | unpack §2f cases 8/9; UnPackNet 0xc-byte form §2g | UpdateMotion.cs:252 (`movementType is 6 or 7` — no 8/9 branch) | **HIGH** |
|
||
| M8 | **Target re-tracking absent** (type 6/8 initialization + retarget): no set_target/HandleUpdateTarget chain; the wire target guid (mt 6) is parsed into `MoveToPathData.TargetGuid` and never used — acdream chases the packet-time ORIGIN and relies on ACE's ~1 Hz MoveTo re-emit for freshness. Also no quantum retune (Phase 3, dist/speed ETA). | HandleUpdateTarget §6d; MoveToObject §3b; Phase 3 @307378-307436; §9f seams | RemoteMoveToDriver.cs:44-51 (doc'd skip = AP-8); UpdateMotion.cs:313 (guid parsed, unused) | **HIGH** (P4 pins the R4 answer) |
|
||
| M9 | **TS-36 dangling**: `InterruptCurrentMovement` is a no-op Action — `jump()` (:921/:1833 sites), `StopCompletely` (:1062), and DoMotion/StopMotion's cancel_moveto bit interrupt NOTHING. Once a real moveto exists, a jump or user input mid-moveto would leave the moveto running alongside — the exact "silent double-motion" the TS-36 row predicts. | interrupt_current_movement §9e → MovementManager::CancelMoveTo §2c | MotionInterpreter.cs:645/:921/:998/:1062/:1833; register TS-36 | **BLOCKER** for V5 |
|
||
| M10 | **PerformMovement discipline absent**: every new moveto must CancelMoveTo(0x36) + `unstick_from_object` FIRST (§3a); MoveToObject/MoveToPosition StopCompletely unconditionally, TurnTo\* only on the stop_completely bit (0x10000); self-target → CleanUp+Stop degenerate. acdream's `BeginServerAutoWalk` just overwrites fields. | PerformMovement §3a; §3b-§3e | PlayerMovementController.cs:452-487 | **HIGH** |
|
||
| M11 | **MovementType/MovementStruct too narrow**: enum lacks `Invalid=0, MoveToObject=6, MoveToPosition=7, TurnToObject=8, TurnToHeading=9`; MovementStruct lacks `ObjectId/TopLevelId/Pos/Radius/Height/Params`. | MovementTypes acclient.h:2856; MovementStruct acclient.h:38069 | MotionInterpreter.cs:101-113 (1-5 only), :408-422 | MED (mechanical) |
|
||
| M12 | **WeenieError lacks the moveto codes**: 0x0B NoMotionInterpreter, 0x36 ActionCancelled, 0x37 ObjectGone, 0x38 NoObject, 0x3D YouChargedTooFar (0x47 exists). Local-only, safe add. | codes §7c/§7a; A10 table | MotionInterpreter.cs:136-205 | LOW |
|
||
| M13 | **`my_run_rate` not fed from the MoveTo wire**: retail unpack cases 6/7 write `minterp->my_run_rate = read_float()`; acdream parses `MoveToRunRate` but only feeds the PlanMoveToStart seed — the interp's `MyRunRate` (:585) never sees it, so `apply_run_to_command`'s speed scale during a moveto uses stale rate. | unpack @300603/@300660 | UpdateMotion.cs:342; GameWindow.cs:4415 | MED |
|
||
| M14 | **Sticky discipline absent**: MoveToPosition/TurnToHeading must CLEAR sticky (0x80) on the stored params (`&= 0xffffff7f`); BeginNextNode's empty-queue sticky branch hands off to `PositionManager::StickTo(tlid, radius, height)` — reads the three fields BEFORE CleanUp zeroes them. R4 ships the seam (Action, no-op → R5 StickyManager) + the verbatim ordering. Wire side: the 0xF74C header sticky bit (motionFlags & 0x1 → trailing sticky guid dword) is parsed by nobody (`_motionFlags` dead at UpdateMotion.cs:135). | §3c @0052a3e5; §3e @0052a70c; BeginNextNode §4b; unpack case 0 @0052455d | nothing; UpdateMotion.cs:135 | MED (seam now, body R5) |
|
||
| M15 | **Heading/geometry helpers absent in Core**: `Position::heading`, `heading_diff` (P3), `heading_greater`, `Frame::get/set_heading`, `cylinder_distance`. Local yaw↔AC-heading conversion exists only in the outbound packers and ad-hoc quaternion math. | §5f/§5g/§5a; P5 | SceneryHelpers.cs:80 (render-side only); RemoteMoveToDriver quaternion yaw | **BLOCKER** (mechanical) |
|
||
| M16 | **`PlanFromVelocity` invented heuristics carry NO register row** (StopSpeed 0.20, RunThreshold 1.25 — acdream constants for the UP-dead-reckoning cycle pick, GameWindow.cs:4942/:5350 "until S6"). Found during this audit: a divergence without a row is a register-rule violation regardless of R4. Its CONSUMER (UP-DR cycle selection) is R6 scope, so the code survives R4 — the row must land now. | none (that's the problem) | ServerControlledLocomotion.cs:54-87; GameWindow.cs:4937-4943, :5350 | MED (bookkeeping, fix in V4) |
|
||
| M17 | **Speculative use-turn bypasses the (future) manager**: `InstallSpeculativeTurnToTarget` (GameWindow.cs:12122) locally predicts ACE's MoveTo before the wire packet via the auto-walk machinery + the 7.5 m CanCharge guess + AP-23 radius buckets. Retail's client-side use flow issues local `CPhysicsObj::TurnToObject/MoveToObject` through the SAME manager (§9a/§9b callers). | §9a/§9b | GameWindow.cs:11915/:12004/:12122-12164 | MED (rewire in V5) |
|
||
|
||
---
|
||
|
||
## 2. KEEP LIST — already matching retail (do not re-port)
|
||
|
||
| Behavior | Retail anchor | acdream anchor |
|
||
|---|---|---|
|
||
| `MovementParameters` flags+scalars class: A4 masks, ctor defaults 0x1EE0F / dto 0.6 / threshhold 15 / FLT_MAX / speed 1 / HoldKey.Invalid, CanCharge-false + threshold-15 ACE-trap notes | ctor 0x00524380; W0-pins A4 | `Motion/MovementParameters.cs` — V1 EXTENDS with get_command family; no field changes |
|
||
| The entire `_DoMotion`/`_StopMotion` seam target: `adjust_motion(ref,ref,HoldKey)` + `DoInterpretedMotion`/`StopInterpretedMotion(uint, MovementParameters)` incl. the retail double-adjust (MoveToManager adjusts, DoInterpretedMotion adjusts again — r4-ace §1 "do NOT fix") | §7a/§7b; 0x00529010/0x00529080 | MotionInterpreter.cs:1241/:2861/:3016 — untouched |
|
||
| WeenieError numerics 0x8/0x24/0x3f-0x49/0x47 (R3-W1 renumber) — V1 only ADDS values | A10 table | MotionInterpreter.cs:136-205 |
|
||
| mt 6/7 wire parse: UnPackNet field order (bitfield, dto, min, fail, speed, threshhold, desiredHeading) + runRate tail + mt-6 guid head — verified against §2g this session | UnPackNet 0x0052ac50 §2g | UpdateMotion.cs:280-356 + `CreateObject.MoveToPathData` — V3 adds mt 8/9 beside it |
|
||
| `OriginToWorld` (landblock-local origin → streaming world) | Position rebase equivalence (P5) | RemoteMoveToDriver.cs:263-277 — MOVES to the manager's home in V4 (the one survivor of the file) |
|
||
| MotionSequenceGate (S1) in front of ALL 0xF74C routing — R4 changes nothing upstream of the gate | 0xF74C dispatch pc:357214 | GameWindow OnLiveMotionUpdated head |
|
||
| R2 pipeline (MotionTableManager/GetObjectSequence/funnel sink) — MoveToManager's dispatched motions flow through it like any other interpreted motion; the animation side needs ZERO new work | R2/R3 shipped | Motion/ classes + MotionTableDispatchSink |
|
||
| Contact/OnWalkable transient-state plumbing (local + remote) — UseTime's `transient_state & 1` gate reads the same source `contact_allows_move` uses (P7 confirms) | @307781 | MotionInterpreter.cs:1979/2089 pattern; PhysicsBody TransientState |
|
||
| HitGround/LeaveGround verbatim bodies (R3-W4) — V4/V5 add the moveto HitGround call BESIDE interp.HitGround (retail order: minterp first, then moveto, §2d); LeaveGround has NO moveto side (COMDAT no-op, §2e) | MovementManager::HitGround 0x00524300 | MotionInterpreter.cs HitGround/LeaveGround + their App call sites |
|
||
| Jump charge UI (AP-24), run/jump skill defaults (TS-21), AP diff cadence (AP-30), outbound packers — untouched by R4 | — | PlayerMovementController.cs sections 4-8 |
|
||
| AD-27 Use/PickUp re-send on arrival (ACE MoveToChain-timeout workaround) — SURVIVES, re-anchored to the new `MoveToComplete` seam (§4); retail notifies nothing on arrival (CleanUpAndCallWeenie is CleanUp+Stop only, §7e) | §7e | GameWindow.cs:11915 subscription; register AD-27 |
|
||
| AP-23 per-type use-radius heuristic — survives (ACE's close-branch broadcasts nothing actionable; unchanged by R4), re-anchored | ACE Player_Move.cs:66 | GameWindow.cs:12102-12164; register AP-23 |
|
||
| `PlanFromVelocity` (UP-DR cycle heuristic) — survives to R6 with its NEW register row (M16); only `PlanMoveToStart` dies in R4 | retire R6 (per-tick order) | ServerControlledLocomotion.cs:54-87 |
|
||
|
||
---
|
||
|
||
## 3. COMMIT SEQUENCE — dependency-sorted, each ONE commit, tests-first
|
||
|
||
New code target: `src/AcDream.Core/Physics/Motion/MoveToManager.cs` (+
|
||
`MoveToMath.cs` for the free functions) per plan rule 4 — pure logic, GL-free,
|
||
seams only. NAME WATCH: retail's `MoveToManager::MovementNode {type, heading}`
|
||
must NOT collide with R2's `Motion/MotionNode.cs` (the pending_motions node) —
|
||
name it `MoveToNode`, register-note the rename. Tests:
|
||
`tests/AcDream.Core.Tests/Physics/Motion/`. Every commit: build+test green,
|
||
register rows added/retired in-commit.
|
||
|
||
**V0 — pins + research docs (docs only).**
|
||
Commit `docs/research/2026-07-03-r4-movetomanager/` (decomp extraction, ACE
|
||
cross-ref, this plan, `V0-pins.md` resolving P1-P7 in the W0-pins format —
|
||
P1's ACE trace + source read and P3's Ghidra decompile are load-bearing; P2/P5/P6
|
||
are cheap; P4 is a recorded decision). Fix the RemoteMoveToDriver.cs:53-57 stale
|
||
claim in the same commit. Deps: none.
|
||
|
||
**V1 — command-selection family + state widening (Core, no consumers).** (closes M2-mechanics, M11, M12, M15)
|
||
`MovementParameters` gains `GetCommand(dist, heading, out motion, out holdKey,
|
||
out movingAway)` (verbatim §5c INCLUDING the CanCharge 0x10 fast-path ACE dropped
|
||
— A13+A15 trap: retail's version of BOTH), `TowardsAndAway` (§5d),
|
||
`GetDesiredHeading` (P2 pin), plus a `FromWire(uint bitfield, dto, min, fail,
|
||
speed, threshhold, desiredHeading)` factory (UnPackNet semantics — bit masks per
|
||
A4) and the field-by-field copy the entry points do. `MoveToMath.cs`: `HeadingDiff`
|
||
(P3 pin), `HeadingGreater` (§5f), `PositionHeading` + `GetHeading/SetHeading`
|
||
(P5 pin — REUSING the outbound packer's yaw↔heading conversion, one convention
|
||
in the codebase), `CylinderDistance` (§5a signature). `MovementType` gains
|
||
Invalid/6/7/8/9; `MovementStruct` gains `ObjectId/TopLevelId/Pos(world+cell)/
|
||
Radius/Height/Params`. `WeenieError` += 0x0B/0x36/0x37/0x38/0x3D.
|
||
Tests first: get_command truth table (all four flag quadrants × distance bands ×
|
||
the hold-key cascade incl. CanCharge fast-path, threshold-edge ≤ vs <,
|
||
walk-incapable), towards_and_away three bands, heading helpers wrap/epsilon
|
||
(0.000199999995f literal) tables, cardinal-heading goldens (P5).
|
||
Fixture source: **synthetic + A4/A10 pins**. Deps: V0.
|
||
|
||
**V2 — MoveToManager verbatim (Core class + conformance harness; no App wiring).** (closes M1/M3/M4/M5/M6/M10/M14-core)
|
||
All 33 members per the extraction: ctor/Create/InitializeLocalVariables (flags
|
||
word + context_id zeroed, floats stale, FLT_MAX distance resets — NOT ACE's A2/A3)
|
||
/Destroy; PerformMovement (cancel 0x36 + unstick first, 4-way); MoveToObject/
|
||
MoveToPosition/TurnToObject (desired-heading clobber quirk VERBATIM)/TurnToHeading
|
||
(immediate BeginNextNode — ACE's A4 gap not copied); MoveToObject_Internal/
|
||
TurnToObject_Internal (fmod, sought-heading read); node factories + `MoveToNode`
|
||
(managed List<> — AD-34 wording); BeginNextNode (sticky handoff reads
|
||
radius/height/tlid BEFORE CleanUp, StickTo seam no-op); BeginMoveForward
|
||
(localParams: clear 0x8000, holdKey from get_command, write-back to stored
|
||
params, progress-clock seed); BeginTurnToHeading (empty-head → CancelMoveTo(8)
|
||
per A10 — not ACE's throw; PreviousHeading := DIFF quirk verbatim);
|
||
UseTime (retail gate polarity: `tlid == 0 || type == Invalid || initialized` —
|
||
NOT ACE's A12 negation); HandleMoveToPosition (Phase 1 aux 20°/340° — NO ACE A6
|
||
set_heading snap, NO A8 inRange block; Phase 2 arrival `moving_away ? dist ≥ min
|
||
: dist ≤ dto`, fail-distance 0x3D; Phase 3 quantum ETA retune via seam);
|
||
HandleTurnToHeading (heading_greater → snap set_heading(node.Heading, send:true)
|
||
+ pop + stop + next); HandleUpdateTarget (init/retarget split, 0x37/0x38 codes,
|
||
FLT_MAX reseeds); HitGround; CheckProgressMade (1 s / 0.25 both-rates);
|
||
GetCurrentDistance (use_spheres → cylinder); CleanUp (stop current+aux,
|
||
clear_target gate, InitializeLocalVariables — does NOT drain nodes);
|
||
CleanUpAndCallWeenie (CleanUp THEN StopCompletely — reentrancy-safe ordering
|
||
+ the `MoveToComplete` seam, §4); CancelMoveTo (drain + CleanUp + Stop; the
|
||
WeenieError arg kept-but-unread per §7c); _DoMotion/_StopMotion (§7a — adjust
|
||
then dispatch); is_moving_to; fail_progress_count as a write-only field (§8).
|
||
Seams (ctor-injected, §4): interp, StopCompletely, position/heading accessors,
|
||
own+target radius/height, IsInterpolating, Contact, set_target/clear_target/
|
||
quantum, unstick, StickTo, MoveToComplete.
|
||
Tests first: per-function conformance tables from the extraction's constants
|
||
inventory (§12); the reentrancy test (CancelMoveTo → CleanUpAndCallWeenie →
|
||
interp.StopCompletely → InterruptCurrentMovement → CancelMoveTo no-ops on
|
||
Invalid); quirk goldens (TurnToObject heading clobber ⇒ final = face-object;
|
||
BeginTurnToHeading stores diff; UseTime gate matrix incl. uninitialized type-6
|
||
stall); a scripted end-to-end table drive (positions fed per tick → expected
|
||
node pops + dispatched motion ids/hold keys, incl. run→walk demote inside
|
||
threshold 15).
|
||
Fixture source: **synthetic + §12 constants (+ V0 cdb goldens if captured)**.
|
||
Deps: V1.
|
||
|
||
**V3 — wire completion: mt 8/9 + my_run_rate + params exposure.** (closes M7, M13, M14-wire-note)
|
||
UpdateMotion parses mt 8 (guid + wire_heading + 3-dword UnPackNet, P6 order) and
|
||
mt 9 (3-dword UnPackNet) into a widened `MoveToPathData` (or a sibling
|
||
`TurnToPathData`) carrying the DECODED bitfield; mt 6/7 exposure widened so ALL
|
||
UnPackNet fields reach the consumer as a `MovementParameters` via `FromWire`
|
||
(today only ad-hoc bit properties). Parse the 0xF74C motionFlags sticky-guid
|
||
trailer (bit 0x1 → read dword; carried, unconsumed until R5) so the buffer
|
||
cursor is honest; standing_longjump bit (0x2) NOTED as an R5 unpack_movement
|
||
item, not consumed here. `MoveToRunRate` documented as the `MyRunRate` write
|
||
the V4/V5 consumers perform (unpack @300603).
|
||
Tests first: golden-byte fixtures generated from ACE's event writers (P6) for
|
||
all four types + flag permutations; existing mt 6/7 fixtures green unchanged.
|
||
Fixture source: **ACE-writer golden bytes**. Deps: V1 (types), parallel with V2.
|
||
|
||
**V4 — REMOTE cutover: per-remote MoveToManager; RemoteMoveToDriver + PlanMoveToStart DELETED.** (closes M1-remote, M4/M5/M6/M8-remote; retires AD-8, AD-9, AP-8, AP-9; adds the M16 row)
|
||
Each `RemoteMotion` gains a `MoveTo` manager bound to its existing
|
||
`Motion` interp + body (construction beside the R3 sink bind, §4). GameWindow
|
||
remote UM routing (:4549-4620): mt 6 → resolve guid against the entity table →
|
||
`MovementStruct{MoveToObject, radius/height from the entity's setup}` →
|
||
`MoveTo.PerformMovement`; unresolvable → degrade to MoveToPosition(wire origin)
|
||
per §2f; mt 7/8/9 likewise; `Motion.MyRunRate = MoveToRunRate`. The P4
|
||
`TargetTracker` adapter feeds `HandleUpdateTarget` (register row). Per-tick: the
|
||
:9594-9682 block becomes `rm.MoveTo.UseTime()` (same slot the legacy driver
|
||
occupied — see the placement decision below); remote HitGround sites add
|
||
`rm.MoveTo.HitGround()` after `rm.Motion.HitGround()` (§2d order). DELETE:
|
||
`RemoteMoveToDriver.cs` (OriginToWorld moves to `MoveToMath`; TurnRateFor's
|
||
surviving consumers, if any outside the deleted paths, get the constant from the
|
||
interp's own apply_run_to_command home), `PlanMoveToStart` + its :4412 seeding
|
||
branch (the manager's own `_DoMotion` → funnel sink now produces the cycle,
|
||
identical mechanism to every other interpreted motion), the
|
||
`HasMoveToDestination/MoveToDestinationWorld/LastMoveToPacketTime/
|
||
ServerMoveToActive` RemoteMotion fields and their :4917/:5329 reads.
|
||
Registers in-commit: AD-8/AD-9/AP-8/AP-9 DELETED; NEW AP row "TargetTracker
|
||
minimal re-tracking adapter (P4) — full TargetManager port R5"; NEW AP row for
|
||
PlanFromVelocity's constants (M16, survives to R6); note that arrival now uses
|
||
retail cylinder distance (the AD-8 max() class is GONE — watch melee-range stop
|
||
distance in the visual pass).
|
||
Tests first: scripted chase/flee/retarget/fail-distance scenario harness driving
|
||
a manager against a mocked tracker; dispatched-motion trace conformance (NPC
|
||
chase emits WalkForward+HoldKey_Run → aux turns → stop, per V2's table);
|
||
existing remote funnel suites green. Live smoke: NPC chase + ACDREAM_DUMP_MOTION.
|
||
Fixture source: **V2 harness + live smoke**. Deps: V2+V3.
|
||
|
||
**V5 — LOCAL PLAYER cutover: B.6 auto-walk DELETED; TS-36 bound (ONE commit, GameWindow + controller — do NOT fan out, feedback_dont_parallelize_coupled_plan_slices).** (closes M1-local, M9, M10, M17; retires TS-36, AD-26; re-anchors AD-27, AP-23; adds the P1 row if pinned as an adaptation)
|
||
`PlayerMovementController` gains a `MoveTo` manager bound to its `Motion` interp
|
||
(exposed like `Motion` was for R3-W2's bind). GameWindow local routing
|
||
(:4507-4547): the `BeginServerAutoWalk` call becomes MovementStruct →
|
||
`MoveTo.PerformMovement` (OriginToWorld unchanged); the P1 pin governs the mt-0
|
||
companion echo (adaptation row if that's the pin). `Motion.InterruptCurrentMovement`
|
||
binds to `MoveTo.CancelMoveTo(ActionCancelled)` — TS-36 RETIRED; user input now
|
||
cancels the moveto exactly the retail way (input edge → DoMotion with
|
||
CancelMoveTo bit → interrupt seam → CancelMoveTo), so the explicit
|
||
"user-input cancel" check dies with the block. DELETE the whole B.6 block:
|
||
fields :275-:340 (incl. `IsServerAutoWalking`, `_autoWalkTurnDirectionThisFrame`
|
||
+ its :1528-1540 consumer), `BeginServerAutoWalk` :452, `EndServerAutoWalk` :495,
|
||
`DriveServerAutoWalk` :567-766, the :896 call + `autoWalkConsumedMotion` skip
|
||
plumbing (:1028). `AutoWalkArrived` is replaced by the `MoveToComplete(None)`
|
||
seam subscription (AD-27 re-anchored, same Use-resend behavior).
|
||
`InstallSpeculativeTurnToTarget` (:12122) rewires to a LOCAL
|
||
`MovementStruct{TurnToObject/MoveToObject}` through the player's manager
|
||
(retail §9a/§9b client-initiated shape; AP-23 buckets survive as the radius
|
||
source, row re-anchored). Per-tick: `MoveTo.UseTime()` at the :896 slot
|
||
(placement decision below). `[autowalk-*]` probes retarget to the manager
|
||
(PhysicsDiagnostics name kept).
|
||
Tests first: full suite green; controller edge-driven suites unchanged;
|
||
manager-driven use-walk scenario (turn node → walk → demote-to-walk near target
|
||
→ arrival → MoveToComplete fires once); reentrancy live test (jump mid-moveto
|
||
cancels it — TS-36's predicted failure); outbound golden-byte parity for a
|
||
scripted approach (MTS/AP bytes vs pre-cutover capture — the moveto issues
|
||
non-autonomous motions, so outbound autonomous traffic must NOT change).
|
||
**ONE user visual pass** (with R2/R3's if still pending): use a door at range
|
||
(walk-up + arrival + door opens once), use while facing away (visible turn
|
||
first), NPC chase legs (turn cycles during corrections, walk-pace close-in),
|
||
TurnTo emote-target if reachable.
|
||
Fixture source: **pre-cutover traces + golden-byte + V2 harness**. Deps: V4.
|
||
|
||
**V6 — register sweep + roadmap + digest (docs/cleanup only).**
|
||
Verify retired: AD-8/AD-9/AP-8/AP-9 (V4), TS-36/AD-26 (V5). Verify added:
|
||
TargetTracker AP row, PlanFromVelocity AP row (M16), P1 echo AD row (if pinned),
|
||
MoveToNode-rename note under AD-34's wording, StickTo/quantum no-op seam rows
|
||
(→R5). Re-anchor: AD-25 (:1212 shifts after the controller deletion), AD-27,
|
||
AP-23, AP-24, TS-21, AP-30 line numbers. Roadmap stage table (R4 shipped);
|
||
milestones check; memory digest note (animation deep-dive: MoveToManager gap
|
||
CLOSED; the "three approximations" pattern retired).
|
||
Deps: V5.
|
||
|
||
Parallelization: V0∥nothing; V1→V2 sequential; V3∥V2 (after V1). V4→V5
|
||
sequential (remotes prove the manager before the local cutover). V4 and V5 both
|
||
touch GameWindow — single-agent each.
|
||
|
||
**DECISION — B.6 auto-walk dies in R4 (V5), not R5.** Grounds: the plan of
|
||
record's R3 keep-list marked it "REPLACED in R4 by MoveToManager"; the mandate
|
||
is delete-in-stage; everything DriveServerAutoWalk does is a subset of the
|
||
verbatim manager (turn-first = TurnToHeading node; 30° walk-while-turning band =
|
||
retail's 20° aux band; arrival = the distance predicate; user cancel = the
|
||
cancel_moveto interrupt chain); keeping it alongside a real manager would mean
|
||
two writers on the same motion channels — the exact double-motion hazard TS-36
|
||
warns about. What survives R5-ward is only the two seams the manager can't fill
|
||
yet: StickTo (R5 StickyManager) and the full TargetManager (P4 row).
|
||
|
||
**DECISION — per-tick driver placement pre-R6: the legacy drivers' own slots.**
|
||
`MoveTo.UseTime()` runs (a) for the player: in `PlayerMovementController.Update`
|
||
at the exact point `DriveServerAutoWalk` runs today (:896, before the
|
||
input-driven motion block); (b) for remotes: in the GameWindow per-remote tick
|
||
block where `RemoteMoveToDriver.Drive` runs today (:9594). Justification:
|
||
least-invasive — both slots already sit at the "after inbound wire, before/with
|
||
physics integrate" altitude the legacy approximations were tuned for, so the
|
||
cutover changes WHAT steers, not WHEN, and every behavior diff in the visual
|
||
pass is attributable to the manager itself rather than to a reordering. Retail's
|
||
true slot (`UpdateObjectInternal`: … transition sweep → MovementManager.UseTime →
|
||
PositionManager.UseTime, plan module map) is R6's deliverable for ALL managers
|
||
at once; R3 set the precedent (r3-port-plan §4 rule 2: "tick placement
|
||
provisional until R6"). The provisional placement gets a one-line note in both
|
||
call sites, not a register row (ordering-within-tick is R6's audited scope).
|
||
|
||
---
|
||
|
||
## 4. WIRING CONTRACT — MoveToManager ↔ the R3 MotionInterpreter (+ GameWindow routing)
|
||
|
||
Retail chain being stood in for (R5 inserts MovementManager as a null-guarded
|
||
relay WITHOUT behavior change — same rule as R3 §4's MotionDone note):
|
||
|
||
```
|
||
retail: unpack_movement / CPhysicsObj entry points
|
||
└─ MovementManager::PerformMovement (1-5 → CMotionInterp, 6-9 → MoveToManager)
|
||
└─ MoveToManager ── _DoMotion/_StopMotion ──► CMotionInterp
|
||
(adjust_motion → DoInterpretedMotion/StopInterpretedMotion)
|
||
R4: GameWindow OnLiveMotionUpdated (mt switch — the pre-existing call-site dispatch)
|
||
├─ mt 0 → funnel (unchanged; P1 pin governs the local-player echo)
|
||
├─ mt 1-5 → interp paths (unchanged)
|
||
└─ mt 6/7/8/9 → entity.MoveTo.PerformMovement(MovementStruct) [+ MyRunRate write]
|
||
```
|
||
|
||
**Construction & binding (per entity, at the same site as the R3 sink bind):**
|
||
- Remote: where `RemoteMotion.Motion` + the MotionTableManager sink are wired
|
||
(GameWindow entity creation), construct `MoveTo = new MoveToManager(...)` with:
|
||
- `interp` = `remoteMot.Motion` (the `_DoMotion` target — adjust_motion +
|
||
DoInterpretedMotion/StopInterpretedMotion, NOTHING else; MoveToManager never
|
||
calls DoMotion/set_hold_run/raw-state APIs — decomp §7b),
|
||
- `stopCompletely` = `() => Motion.StopCompletely()` (retail routes
|
||
CPhysicsObj::StopCompletely → MovementStruct type 5 → interp; the direct
|
||
call is the same body pre-R5),
|
||
- body accessors: position+cell, `GetHeading`/`SetHeading(deg, send)` (P5
|
||
convention; `send` flags the outbound heading snap — remotes: no-op send),
|
||
own radius/height (setup shape), Contact (`PhysicsObj.TransientState`),
|
||
`IsInterpolating` = InterpolationManager queue-non-empty,
|
||
- target seams: `set_target/clear_target` → the P4 TargetTracker;
|
||
`get/set_target_quantum` → tracker-recorded value (accepted, non-throttling);
|
||
- `unstick` → `Motion.UnstickFromObject` (existing no-op seam, R5);
|
||
`stickTo(tlid, radius, height)` → NEW no-op Action seam (R5 StickyManager;
|
||
register row);
|
||
- `MoveToComplete(WeenieError)` → remote: nothing (retail-faithful);
|
||
local player: the AD-27 Use-resend subscription (fires ONLY on `None`).
|
||
- Local player: `PlayerMovementController` constructs/exposes `MoveTo` bound to
|
||
its own `Motion`; GameWindow routes local mt 6-9 to it.
|
||
|
||
**The cancel chain (TS-36 retires here):**
|
||
```
|
||
user input edge / jump() / StopCompletely
|
||
└─ MotionInterpreter — params.CancelMoveTo bit / hardcoded sites (:921/:998/:1062/:1833)
|
||
└─ InterruptCurrentMovement?.Invoke() [R3 seam, was no-op]
|
||
└─ R4 bind: entity.MoveTo.CancelMoveTo(WeenieError.ActionCancelled)
|
||
└─ drain nodes → CleanUp (stops current+aux via _StopMotion,
|
||
clear_target, InitializeLocalVariables → movement_type=Invalid)
|
||
→ StopCompletely
|
||
```
|
||
Reentrancy invariant (MUST be tested, V2): the tail `StopCompletely` re-enters
|
||
`InterruptCurrentMovement` → `CancelMoveTo`, which NO-OPS because CleanUp already
|
||
reset `movement_type` to Invalid BEFORE the stop (retail ordering §7e; ACE note
|
||
r4-ace §3). Do not reorder CleanUp/StopCompletely.
|
||
|
||
**HandleUpdateTarget feed (P4):** TargetTracker (App) watches the entity table;
|
||
delivery → `entity.MoveTo.HandleUpdateTarget(TargetInfo)`; context 0 only
|
||
(retail CPhysicsObj::HandleUpdateTarget @280794 gates on context_id == 0).
|
||
Target despawn → `status = ExitWorld` (manager cancels 0x37/0x38 itself).
|
||
|
||
**Per-tick + ground events:** `MoveTo.UseTime()` at the two legacy slots
|
||
(decision above). Every existing `Motion.HitGround()` call site adds
|
||
`MoveTo.HitGround()` AFTER it (retail §2d order: minterp then moveto).
|
||
LeaveGround/ReportExhaustion: NO moveto call — COMDAT no-ops (§2e), do not
|
||
invent.
|
||
|
||
**GameWindow mt 6-9 routing detail (V4/V5):**
|
||
1. MotionSequenceGate first (unchanged).
|
||
2. `Motion.MyRunRate = MoveToRunRate` (unpack @300603/@300660).
|
||
3. mt 6: resolve `TargetGuid` in the entity table → top-level parent id (retail
|
||
resolves `parent ?: target`, §9a — acdream: container/parent link if the
|
||
entity model has one, else the guid itself) + setup radius/height →
|
||
`MovementStruct{MoveToObject, ObjectId, TopLevelId, Radius, Height,
|
||
Params=FromWire(...)}`. Unresolvable → `MovementStruct{MoveToPosition,
|
||
Pos=OriginToWorld(wire origin)}` (the §2f degrade — NOT an error).
|
||
4. mt 7: MoveToPosition(OriginToWorld(origin), FromWire params).
|
||
5. mt 8: resolve → TurnToObject; unresolvable → params.DesiredHeading =
|
||
wire_heading, TurnToHeading (§2f fallback).
|
||
6. mt 9: TurnToHeading(FromWire 3-field params).
|
||
7. mt 0 for the local player: per the P1 pin (echo suppression rule or wire
|
||
discriminator); for remotes: unchanged funnel + (retail head shape) an
|
||
interrupt — remotes' UM streams from ACE already interleave moveto and
|
||
interpreted UMs, and V4 must apply the SAME P1 answer to remotes chasing
|
||
under ACE's re-emit (a fresh mt-6 re-emit cancels+restarts via
|
||
PerformMovement regardless, so remotes are insensitive to the pin either way).
|
||
|
||
**What the manager dispatches (animation side — zero new work):**
|
||
`_DoMotion(0x45000005 WalkForward, localParams{holdKey})` → adjust_motion
|
||
promotes to RunForward×runRate under HoldKey_Run → DoInterpretedMotion → the
|
||
R2 sink → MotionTableManager → GetObjectSequence — the identical path a wire
|
||
RunForward takes today. `PlanMoveToStart`'s seeding job ceases to exist rather
|
||
than being replaced.
|
||
|
||
---
|
||
|
||
## 5. NEGATIVE RESULTS / DO-NOT-INVENT (binding on subagents)
|
||
|
||
- **`MoveToManager::LeaveGround` / `ReportExhaustion` — COMDAT-folded no-ops**
|
||
in this build (decomp §2e); `CMotionInterp::HandleEnterWorld` likewise. No
|
||
members, no seams, no behavior.
|
||
- **`fail_progress_count` is write-only in RETAIL too** (§8: 6 sites, zero
|
||
reads). NO give-up-after-N mechanism exists — the only aborts are
|
||
fail_distance (0x3D) and target status (0x37/0x38). Do not invent a stall
|
||
timeout to replace AD-9's timer; deleting the timer is the point.
|
||
- **`CancelMoveTo`'s WeenieError arg is NEVER READ in the body** (§7c) — every
|
||
call site's code (8/0x36/0x37/0x38/0x3D) is dead in this build. Keep the
|
||
parameter (R5 parity + logging), wire no behavior to its value.
|
||
- **`CleanUpAndCallWeenie` contains NO weenie call in this build** (§7e — the
|
||
name is vestigial; body ≡ CleanUp + StopCompletely). acdream's
|
||
`MoveToComplete` seam is a documented CLIENT addition standing in for ACE's
|
||
server-side `OnMoveComplete` — do not present it as retail.
|
||
- **No listener/observer machinery on MoveToManager or MovementManager**
|
||
(grep-negative, §10). `MoveToComplete` is the one seam, added consciously.
|
||
- **`CPhysicsObj::cancel_moveto` does not exist** — the cancel entry is
|
||
`interrupt_current_movement` (§9e). **`CPhysicsObj::MoveToPosition` does not
|
||
exist** — position moves enter via unpack case 7 / direct manager call only.
|
||
- **Do NOT copy the ACE-isms** (all RETAIL-VERIFIED in r4-ace §4): A1
|
||
stale-member UseFinalHeading read (read the ARGUMENT in MoveToPosition), A2
|
||
InitializeLocalVars zeroing DistanceToObject (retail zeroes the FLAGS word),
|
||
A3 missing FLT_MAX resets, A4 missing BeginNextNode in TurnToHeading, A6
|
||
set_heading snap in the aligned branch ("custom: sync for server ticrate"),
|
||
A7 `AlwaysTurn`, A8 `inRange` arrival block ("custom for low monster update
|
||
rate"), A11 WeenieObj NPE, A12 UseTime gate negation, A13 CanCharge-true
|
||
default, A14 threshold 1.0, A15 dropped CanCharge fast-path.
|
||
- **Do NOT "fix" the retail quirks**: TurnToObject's desired_heading write is
|
||
clobbered before any read (final heading = face-the-object; ACE matches);
|
||
BeginTurnToHeading stores the remaining DIFF into previous_heading; the
|
||
double adjust_motion (_DoMotion + DoInterpretedMotion-internal); PerformMovement
|
||
returning 0 unconditionally (errors surface via CancelMoveTo, not the return).
|
||
- **HandleTurnToHeading's `set_heading(node.Heading, true)` snap IS retail**
|
||
(0052a146) — the only heading snap in the whole family. Everything else
|
||
rotates via dispatched turn motions.
|
||
- **BN artifact ledger applies** (decomp §11): the chase-arrival and turn-pick
|
||
branch senses in the raw are `test ah,0x41` inversions — the adjudicated
|
||
readings (chase arrives at `dist ≤ distance_to_object`; diff ≥ 180 →
|
||
TurnLeft) are pinned; do not re-derive from the literal pseudo-C. The
|
||
`RemoteMoveToDriver.cs:53-57` "ACE swaps the predicates" doc claim is WRONG
|
||
(superseded in-file at :186-199) — fixed in V0.
|
||
- **TargetManager / StickyManager / ConstraintManager / PositionManager::StickTo
|
||
bodies were NOT extracted** — call shapes only (§9f/§9g). Do not invent
|
||
internals beyond the P4 minimal adapter; R5 owns the ports.
|
||
- **The older Ghidra chunk `chunk_00520000.c` is from a DIFFERENT build** for
|
||
this address region (§10) — function boundaries don't align; unusable for
|
||
adjudication. Use the named raw + Ghidra MCP on `patchmem.gpr` only.
|
||
- **MovementManager itself is R5** — R4 must not grow a premature facade; the
|
||
type dispatch stays at the existing GameWindow/controller call sites, and the
|
||
two direct binds (interrupt→CancelMoveTo, HitGround dual-call) are exactly
|
||
the relays R5 will absorb.
|