acdream/docs/research/2026-07-03-r4-moveto/V0-pins.md
Erik b3decdfac6 feat(R4-V5): LOCAL PLAYER cutover - B.6 auto-walk DELETED; P1 autonomous gate ported; TS-36 bound (closes M1-local, M9, M10, M17; retires TS-36, AD-26; re-anchors AD-27, AP-23)
The local player now runs server MoveTos through the same verbatim
MoveToManager remotes got in V4. One commit, GameWindow + controller,
per the no-fan-out rule for coupled slices.

P1 gate (V0-pins.md P1, ported verbatim): CPhysics::SetObjectMovement
(0x00509690 @0050972e) drops any movement event whose wire autonomous
byte is set when the addressed object IsThePlayer - ACE's self-addressed
MoveToState reflection (MovementData.cs:162 IsAutonomous=1) never
reaches unpack_movement, which is what makes the retail unconditional
unpack-head interrupt safe. Gate placed AFTER the sequence gates
(retail order). The stale "ACE follows every mt=0x06 with an mt=0x00"
comment block dies with the code it excused (its causal story was
pre-#75; refuted in V0-pins P1).

Run-rate re-anchor (P1 contingency NOT needed - no AD row): the echo
tap ApplyServerRunRate is deleted outright. Both retail feeds already
exist: PlayerDescription skills via SetCharacterSkills (K-fix7) into
InqRunRate (preferred by apply_run_to_command/get_state_velocity), and
the mt-6/7 my_run_rate wire write (M13) now performed for the player by
the shared RouteServerMoveTo. The tap's InterpretedState.ForwardSpeed
overwrite was a pre-R3 mechanism that fought the ported machinery.

B.6 auto-walk deleted wholesale (~330 lines): fields, Begin/End/
DriveServerAutoWalk, IsServerAutoWalking, AutoWalkArrived, the
autoWalkConsumedMotion gates, the #69 turn-dir edge synthesizer, and
the relocated AutoWalkArrivalEpsilon/AutoWalkTurnRateFor constants
(AD-26 retired - the invented 5/30-degree bands are gone; arrival is
retail's distance predicate; turn-first is the TurnToHeading node).

TS-36 retired: Motion.InterruptCurrentMovement binds to
MoveTo.CancelMoveTo(ActionCancelled). Movement-key edges (ctor-default
params carry the 0x8000 CancelMoveTo bit), Shift (set_hold_run
interrupt:true), jump(), StopCompletely, and teleport all cancel a
running moveto through the retail chain - verified by controller-level
tests, not assumed.

MoveToComplete seam WIDENED to natural completion (Core): retail's
BeginNextNode empty-queue completion is inline CleanUp+StopCompletely
(raw @00529d47) and notifies nothing - the client-addition seam had to
fire there (both sticky and non-sticky exits) or AD-27's deferred
close-range Use/PickUp re-send never fires. Never fires on CancelMoveTo.
AD-27 re-anchored from the deleted AutoWalkArrived event.

InstallSpeculativeTurnToTarget rewired through the player's manager
(retail 9a/9b client-initiated shape): close-range -> TurnToObject,
far -> MoveToObject; AP-23 radius buckets + the #77 CanCharge
prediction survive as the params source (row re-anchored).

Per-tick: MoveTo.UseTime() at the old DriveServerAutoWalk slot
(provisional until R6, per the plan's placement decision); the P4
player-side TargetTracker twin (fields + pre-Update feed) mirrors the
remote adapter (AP-79 row widened). HitGround dual-call added on the
player landing edge AND the remote landing site - the latter closes a
V4 wiring-contract gap the adversarial review caught (retail 2d order:
minterp then moveto; without it a landing NPC's moveto never re-arms).

Also from the adversarial review: the mt-8 unresolvable-target degrade
now performs retail's params.desired_heading = wire_heading
substitution (decomp 2f case 8; invisible against ACE per P6, required
for the verbatim degrade); the V4 remote MoveToManager binding gets a
real curTime clock (the ctor stub advanced 1/30s per READ, skewing the
progress/fail-distance windows - note: the pending V4 NPC smoke ran on
the skewed clock); TS-33 row extended with the orientation-diff gap
(ApproxPositionEqual vs retail Frame::is_equal full-frame compare - a
stationary heading snap does not reach the wire; masked against ACE,
R7 outbound scope owns the fix).

MoveToMath gains HeadingFromYaw/YawFromHeading (the P5 scalar bridge
for yaw-authoritative bodies - the player's heading snap must write
Yaw, not the quaternion the controller re-derives every frame).

Tests: 3,956 green (+8). New: MoveToManagerCompletionSeamTests (arrival
fires once with None, no refire, cancel never fires, sticky handoff
order) + PlayerMoveToCutoverTests (EnterPlayerModeNow-shape rig: walks
to arrival with zero user input and zero MotionStateChanged frames -
the #75 invariant by construction; TurnToHeading rotates Yaw and snaps
exact; W-edge and jump cancel without firing complete). W6 edge suite
retargeted from the deleted echo tap to a direct apply pass (the
regression lives in ApplyInterpretedMovement, not the wire trigger).

Spec: docs/research/2026-07-03-r4-moveto/r4-port-plan.md section 3 V5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:24:22 +02:00

330 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# R4-V0 — ambiguity pins and ACE-oddity adjudications
The verbatim extraction is `r4-moveto-decomp.md` (line-anchored); the port plan
with the full ambiguity table is `r4-port-plan.md` §0. This note records the
PINNED resolutions the V1V6 ports code against. Every pin below was produced by
an independent raw re-read of `docs/research/named-retail/acclient_2013_pseudo_c.txt`
(line numbers cited are that file's), cross-read against ACE source
(`references/ACE/Source/ACE.Server/`, MoveToManager under `Physics/Managers/`),
and P1/P3 additionally survived an adversarial refutation pass (P3 by direct
byte-level disassembly of the PDB-matched retail binary — evidence one tier
stronger than a Ghidra decompile). **No pin was refuted; none blocks on cdb or
Ghidra.** Ghidra MCP was down for the whole pass; the deferred decompile checks
are listed per-pin and in the tail section — all confirmatory, none load-bearing.
## Pinned
- **P1 — ACE's companion mt-0 echo vs retail's unpack-head cancel: PINNED — no
adaptation needed; port the missing TAIL of retail's own gate.**
`CPhysics::SetObjectMovement` (0x00509690) drops any movement event whose wire
autonomous byte is set when the addressed object `IsThePlayer`, BEFORE
`unpack_movement` is ever called — so retail's unconditional head-interrupt
never sees an autonomous echo. Plan option (c) (suppress-when-command-matches)
is DISCARDED. Confidence: **high** (adversarially verified; refutation failed
on every angle).
Port shape: in the local-player 0xF74C path, after the S1 `MotionSequenceGate`
passes, drop the event entirely — no interrupt, no state application — when
`update.IsAutonomous && guid == local player guid`. Verbatim (raw 271370271431):
`ebx = weenie_obj->vtable->IsThePlayer()` @005096a8; two wraparound sequence
gates; then @0050972e `if ((arg7 == 0 || ebx == 0)) {
arg2->last_move_was_autonomous = arg7; CPhysicsObj::unpack_movement(arg2,
&arg3, arg4); if (ebx != 0) return 1; }` else fall to `return 0`. Note
`last_move_was_autonomous` is stored ONLY on the unpack path — do not store it
for dropped events. The `||`-vs-`&&` BN-inversion worry is refuted
behaviorally: with `&&` the local player would never unpack ANY movement event,
yet server MoveTos demonstrably drive retail clients against ACE. The 4-arg
overload (0x00509790, raw 271458271485) pins arg7 = the wire byte at offset +4
after two u16 sequences with align-to-4 — byte-identical to ACE's writer
(MovementData.cs:189-200: `GetNextSequence(ObjectMovement)`; autonomous ?
`GetCurrentSequence` : `GetNextSequence(ObjectServerControl)`;
`Convert.ToByte(motion.IsAutonomous)`; `writer.Align()`).
With the gate in place, the verbatim `unpack_movement` head-cancel ports
cleanly: `CPhysicsObj::interrupt_current_movement` + `unstick_from_object` on
EVERY UM type 0/6/7/8/9 before the type switch (0x00524440, raw 300566300573;
jump table @300707300719 — types 15 fall to return 0, head already ran).
`interrupt_current_movement` = `if (movement_manager)
MovementManager::CancelMoveTo(0x36)` (raw 278189278200), a no-op when
`moveto_manager == 0` (raw 300277300288 @0x005241b0). mt 6/7/8/9 keep the
retail unconditional cancel as planned (PerformMovement re-cancels anyway, §3a).
Why the echo was never a retail problem: the "companion mt-0 echo" is NOT
unconditional ACE behavior — it is ACE reflecting the client's OWN outbound
MoveToState back to the sender. `BroadcastMovement` has exactly ONE caller
(GameActionMoveToState.cs:36); `MovementData(Creature, MoveToState)` hardcodes
`IsAutonomous = true` (MovementData.cs:162) and Player_Networking.cs:365 sends
it to self (`EnqueueBroadcast(true, movementEvent)`; ACE's own comment
"shouldn't need to go to originating player?"). The 2026-05-14 trace behind
GameWindow.cs:4534-4546 was self-inflicted: the pre-#75 auto-walk synthesized
Forward+Run input that leaked outbound as a MoveToState
(PlayerMovementController.cs:546-557 preserves the finding); post-#75
(:298-308) no MoveToState is built during auto-walk, so no echo exists at
moveto start — **the GameWindow.cs:4534-4546 causal story ("ACE follows every
mt=0x06 with an mt=0x00") is stale**. ACE's mt-6/7/8/9 senders are
`IsAutonomous = false` (Motion.cs:18 default, copied at MovementData.cs:44;
only two `IsAutonomous = true` sites in ACE.Server: MovementData.cs:162 and
the Player.cs:944-951 jump hack), so they pass the gate and the auto-walk
trigger survives. The gate also absorbs ACE's other self-addressed autonomous
mt-0 class (the charged-jump hack) — and it is precisely why retail WASD
against ACE (every keypress echoed back IsAutonomous=1) doesn't stutter:
standing behavioral corroboration.
**Register adjudication (the exact wording V5/V6 execute):** NO new AD row.
The informal "do NOT cancel local moveto on a non-MoveTo UM" adaptation at
GameWindow.cs:4534-4546 has no register row (verified — a row-less deviation)
and is RETIRED by the ported retail mechanism; delete the comment block with
the code in V5. ONE CONTINGENCY: the gate severs acdream's only live runSkill
sync feed — GameWindow.cs:4364-4372 wires
`ApplyServerRunRate(update.MotionState.ForwardSpeed)` and its ONLY live source
is the autonomous mt-0 echo. V5 must re-anchor run-rate sync to retail's own
mechanism (mt-6/7 `my_run_rate` wire read = plan item M13, plus
PlayerDescription). If V5 instead taps the echo's ForwardSpeed BEFORE the drop,
that tap IS an ACE-compat adaptation requiring this row in the same commit:
> **AD-new | Local-player autonomous-echo ForwardSpeed tap** — acdream reads
> `InterpretedMotionState.ForwardSpeed` from the self-addressed autonomous
> mt-0 UM (ACE's MoveToState reflection, MovementData.cs:162
> IsAutonomous=true hardcoded) before the ported retail
> SetObjectMovement autonomous/IsThePlayer drop (0x00509690 @0050972e)
> discards it, feeding `PlayerMovementController.ApplyServerRunRate`. Retail
> discards the event entirely. | Risk: local/observer run-speed desync if the
> echo changes; masks the missing my_run_rate wire feed. | Retire when M13's
> mt-6/7 my_run_rate + PlayerDescription feed lands.
Adjacent seam (R5 note): retail's 0xF74C dispatch calls
`cmdinterp->vtable->LoseControlToServer()` whenever SetObjectMovement returns
nonzero — i.e. whenever a NON-autonomous movement event is applied to the
local player (raw 357214357235: instance-seq gate then `if
(CPhysics::SetObjectMovement(...) != 0) this->cmdinterp->vtable->
LoseControlToServer();`). This is the retail autonomy-handoff hook that
`BeginServerAutoWalk` currently approximates — R5/MovementManager scope.
Ghidra deferred (non-blocking): decompile `0x00509690` (re-verify the
`(arg7 == 0 || ebx == 0)` condition + which sequence stamps are written on the
reject paths — the function has BN self-subtraction flag-test garbles at
@005096db/@00509712, but the decisive gate is a plain integer test with the
adjacent `last_move_was_autonomous` store); decompile the 0xF74C dispatch case
at ~`0x00559850` (confirm LoseControlToServer fires iff SetObjectMovement != 0).
- **P2 — `get_desired_heading` (0x0052aad0) 180/0 constants: PINNED to ACE's
shape.** `RunForward|WalkForward → movingAway ? 180 : 0`; `WalkBackwards →
movingAway ? 0 : 180`; default 0. Confidence: **high**. The BN raw
(308016308033) confirms only the command GROUPING — {RunForward 0x44000007,
WalkForward 0x45000005} vs {WalkBackwards 0x45000006} vs default — while both
constant branches render identically (`result = arg3`) and the default renders
as x87 flag-synthesis nonsense (`return (arg2 - 0x45000006)`), the known
setcc-garble class. Constants + per-branch polarity are 100% ACE-sourced
(MovementParameters.cs:186-198) but doubly corroborated: (a) caller math (raw
307227307233, HandleMoveToPosition @0x00529e25) adds the return to
`Position::heading(to target)` then wraps [0,360) — a degree OFFSET; (b)
physics: §5c pure-away picks WalkForward+movingAway=1 ("heading flips via
get_desired_heading +180", r4-moveto-decomp.md:619-620) → away-walk faces
away = +180; §5d towards_and_away picks WalkBackwards+movingAway=1 inside the
min band ("backs up with WalkBackwards (no turn)", :656) → backstep faces the
target = 0. Ghidra deferred (non-blocking): decompile `0x0052aad0`; mark the
pseudocode doc UNVERIFIED-BY-GHIDRA until then.
- **P3 — `heading_diff` (0x00528fb0) third-arg mirror: PINNED as MIRROR
PRESENT — port ACE's body verbatim.** `result = h1 h2; if |result| < ε
result = 0; if result < −ε result += 360; if (result > ε && motion !=
TurnRight) result = 360 result;` with ε = 0.000199999995f and TurnRight =
0x6500000D. r4-moveto-decomp §5g's "arg3 UNUSED in the body" is a BN x87-setcc
artifact (the W6b class); r4-ace-moveto §1's suspicion was correct.
Confidence: **high** — sealed by direct disassembly of the PDB-matched binary
(`C:\Turbine\Asheron's Call\acclient.exe`, CodeView GUID
9e847e2f-777c-4bd9-886c-22256bb87f32 = MATCH), which supersedes the deferred
Ghidra check entirely. The tail BN garbled into a fake "return flag word"
(raw 306344306346) disassembles as exactly the mirror guard:
```
00528fec fcom dword [0x007c8a00] ; result vs ε (0.00019999...)
00528ff2 fnstsw ax
00528ff4 test ah, 0x41 ; C0|C3 → result <= ε
00528ff7 jnz 0x00529009 ; skip mirror unless result > ε STRICT
00528ff9 cmp dword [esp+0xC], 0x6500000D ; arg3 vs TurnRight — arg3 IS read
00529001 je 0x00529009 ; TurnRight → skip mirror
00529003 fsubr dword [0x0079bc60] ; ST0 = 360.0f result
00529009 ret ; float return in ST0
```
The 29 bytes BN collapsed (0x00528fec→0x00529008) fit the mirror encoding to
the byte; `test ah,0x41 / jnz` encodes strict `>` (skip on less OR equal), so
ACE's `>` comparison (Managers/MoveToManager.cs:825-826) is the correct port.
Call sites: `BeginTurnToHeading` @0x00529be0 passes CONSTANT 0x6500000d
TurnRight (mirror explicitly disabled — direction pick stays the ≤180 test);
`HandleTurnToHeading` @0x0052a1bc (raw :307495) passes the LIVE
`current_command`. Two refinements over the plan's P3 row: (1) the pin's
"previous_heading update path differs" clause is FALSE — both branches of the
progress test write `previous_heading` identically (raw @0x0052a1ef /
@0x0052a206; ACE :611-621); (2) the mirror is behaviorally DEAD in this build —
its only divergent effect is `fail_progress_count` reset-vs-increment, and
that counter is write-only (6 refs in the whole raw, all writes: =0
@0x005292b1/@0x00529f3e/@0x0052a13a/@0x0052a1e5, +=1 @0x0052a014/@0x0052a224;
zero reads) — port it for verbatim fidelity + conformance tests, not because
retail-visible behavior hangs on it. No deferred check remains (Ghidra + cdb
fallback both MOOT).
- **P4 — TargetManager scope: DECISION CONFIRMED — R4 ships the minimal
App-side `TargetTracker` adapter, not a TargetManager port.** Confidence:
**high** (as a recorded decision; the retail feed constants and delivery
contract are pinned from the extraction). On `set_target(0, tlid, 0.5, 0)`
(verbatim call sites: r4-moveto-decomp.md:359 MoveToObject §3b, :423
TurnToObject §3d; constants table :1168 — radius 0.5f, initial quantum 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; `clear_target` unsubscribes; target despawn/teleport
delivers `status = ExitWorld/Teleported` so `CancelMoveTo 0x37/0x38` falls out
of the verbatim manager (§6d @307802307867: `!= Ok` on first callback →
0x38 NoObject :857-858; on retarget → 0x37 ObjectGone :866-867; retarget sets
`sought_position = interpolated`, `current_target_position = target`
:869-870 + FLT_MAX progress reseeds :871-874). `set_target_quantum` is
accepted and recorded but does NOT throttle (over-delivery is
convergence-safe). TargetStatus values from ACE (r4-ace-moveto.md:654-656):
Undefined=0, OK=1, ExitWorld=2, Teleported=3, Contained=4, Parented=5,
TimedOut=6; ACE's HandleTargetting cadence is 0.5 s with a 10 s
Undefined→TimedOut timeout. Wire inputs available today: mt-6 TargetGuid
(UpdateMotion.cs:310-315, parsed-but-unused = gap M8 — the subscription key);
wire Origin (:319-326) doubles as the first-delivery fallback when the guid
is unresolvable, mirroring retail's own case-6 degrade to
MoveToPosition(wire origin) (r4-moveto-decomp.md:290-292). NOT on the wire:
target radius/height for `cylinder_distance` (from the local entity table)
and ongoing target updates (the adapter's per-tick tracking; ACE's ~1 Hz
MoveTo re-emit is the backstop). **Register row (AP class) lands in V4,
replacing AP-8's "no target re-tracking" clause; full TargetManager port is
R5 scope.** Fallback if the adapter proves too loose in V4: grep
`TargetManager::` in the raw (~200 lines) and port verbatim.
- **P5 — Position::heading convention: PINNED — compass degrees, 0 = North
(+Y), 90 = East (+X), CLOCKWISE, [0,360); identity quaternion faces heading
0.** Formula: `heading(from, to) = (450 atan2Deg(dy, dx)) % 360`. Wire
`DesiredHeading` is in THIS convention (retail @0052a71d feeds it straight
into `Frame::set_heading` on sought_position, §3e). Golden cardinals:
N(0,+1)→0, E(+1,0)→90, S(0,1)→180, W(1,0)→270. Confidence: **high**.
Evidence: `Position::heading` 0x005a9520 (raw 438288438290): `450.0
fpatan(...) × 57.295779513082323`, fmod tail; ACE Position.cs:200-209 pins the
atan2 arg order (`Atan2(dir.Y, dir.X)`); `Frame::get_heading` 0x00535760 (raw
319781) same formula on the forward basis (ACE AFrame.cs:147-152);
`Frame::set_heading` 0x00535e40 (raw 320055320066) round-trips — h=0 →
forward (sin 0, cos 0) = +Y. An in-tree twin of the formula already exists at
SceneryHelpers.cs:75.
**⚠ CORRECTION to the plan's own pin method — the packer-reuse trap.** The
plan says "the outbound AP packer already encodes body yaw→AC heading; REUSE
that exact conversion". Reuse it at the **QUATERNION level ONLY**
(`YawToAcQuaternion`/`AcQuaternionToYaw` are wire-correct end-to-end — its
two internal errors cancel). Its internal SCALAR intermediate
(GameWindow.cs:8083-8103: `headingDeg = 180f yawDeg`) is **holtburger's
convention = retail + 90°**, and its comment "AC heading: 0=West, 90=North,
180=East, 270=South" (from holtburger math.rs:56, whose identity-quat note
:146-148 says "Heading 90 (North)") is WRONG as a statement of the retail wire
convention. Literal scalar reuse would face `use_final_heading` and every
`get_desired_heading` offset 90° wrong. The correct scalar bridge from acdream
yaw (yaw=0 faces +X per PlayerMovementController.cs:1022-1025) is
`heading = (90 yawDeg) mod 360`. Quaternion-level proof:
YawToAcQuaternion(yaw=0) → (z=0.7071, w=+0.7071), from which ACE's
AFrame.get_heading extracts 90 = East — correct for a +X-facing body.
- **P6 — TurnToObject (mt 8) wire layout: PINNED byte-for-byte from ACE's
writer, matching the decomp read order exactly.** After the common UM header
(u8 movementType, u8 motionFlags, u16 currentStyle — MovementData.cs:202-205):
`[u32 object guid][f32 standalone heading][u32 params bitfield][f32 speed]
[f32 params desired_heading]`. mt 9 (TurnToHeading) = the 3-dword
TurnToParameters only. Confidence: **high**. The standalone second field is
ACE `TurnToObject.DesiredHeading` = retail's `wire_heading`, consumed ONLY in
the unresolvable-object fallback: substituted into `params.desired_heading`,
then degrade to TurnToHeading (decomp §2f case 8, r4-moveto-decomp.md:293-298:
`object_id = read_dword(); wire_heading = read_dword(); UnPackNet(...); if
(GetObjectA(object_id) == 0) { var_9c.desired_heading = wire_heading; goto
label_524725; }`; ACE TurnToObject.cs:25-31 + field comment :10-11 "used
instead of the DesiredHeading in the TurnToParameters"). UnPackNet 0xc-byte
form per §2g (:311): bitfield, speed, desired_heading. Gap M7 confirmed:
UpdateMotion.cs:252 `else if (movementType is 6 or 7)` — mt 8/9 payloads
silently discarded today. **V3 fixture caveat:** ACE populates BOTH heading
fields from the same source (TurnToObject.cs:17 and TurnToParameters.cs:18
both copy `motion.DesiredHeading`), so ACE-golden fixtures always have
field2 == field5 — distinguish the two fields by OFFSET (hand-vary one in
synthetic fixtures), never by value.
- **P7 — remote Contact for the UseTime gate: PINNED — Contact IS live for
grounded remotes; no state-writer fix needed.** Confidence: **high**. Remote
bodies are BORN with `Contact | OnWalkable | Active` (GameWindow.cs:580-591)
and BOTH remote per-tick paths re-assert `TransientState |= Contact |
OnWalkable | Active` unconditionally every tick while `!rm.Airborne`
(:9366-9370 player-remote pipeline; :9570-9574 NPC/legacy — the path
RemoteMoveToDriver runs on today). The bits are cleared ONLY for the airborne
arc (:4833-4841, `Velocity.Z > 0.5f` launch) and restored on landing by two
independent paths (:5161-5167 UP grounded snap; :9808-9818 resolver landing —
which already calls `rm.Motion.HitGround()`, the exact retail re-arm seam
`MoveToManager::HitGround` §6e binds to: `if (movement_type == Invalid)
return; BeginNextNode(this);`, r4-moveto-decomp.md:884-889). Retail UseTime
gates on `transient_state & 1` (Contact only, @307781) — a strict SUBSET of
the funnel's `Contact && OnWalkable` that demonstrably works for remote
Falling dispatch today (MotionInterpreter.cs:2089-2092). A chasing grounded
NPC passes the gate every tick; an airborne remote stalls the moveto until
landing — retail behavior by design. The pin-method's live
`ACDREAM_DUMP_MOTION` smoke was NOT run (read-only pass; the write is
unconditional so the smoke is confirmatory only) — **folded into V4's
acceptance run** (already listed there: "Live smoke: NPC chase +
ACDREAM_DUMP_MOTION").
## Pending
None. No pin was refuted or left uncorrectable.
## Deferred non-blocking checks (Ghidra MCP down during V0)
All three are confirmatory — the textual adjudications above are decisive.
| Address | Function | What to confirm |
|---|---|---|
| `0x00509690` | `CPhysics::SetObjectMovement` (7-arg) | The `(arg7 == 0 \|\| ebx == 0)` gate condition; which sequence stamps are written on the reject paths (BN garbles @005096db/@00509712 are self-subtraction flag-test artifacts) |
| ~`0x00559850` | 0xF74C dispatch case | `LoseControlToServer()` fires iff `SetObjectMovement != 0` |
| `0x0052aad0` | `MovementParameters::get_desired_heading` | The 180/0 constants + per-branch polarity (currently ACE-sourced; grouping BN-verified) |
P3's deferred check (`0x00528fb0`) is MOOT — resolved by direct disassembly of
the PDB-matched binary (see the pin).
## Adjacent findings (load-bearing for V-commits, discovered while pinning)
- **runSkill-sync severance (V5 trap, from P1):** GameWindow.cs:4364-4372's
`ApplyServerRunRate(ForwardSpeed)` is fed EXCLUSIVELY by the autonomous mt-0
echo the P1 gate will drop. V5 re-anchors run-rate sync to M13 (mt-6/7
`my_run_rate` wire read → `Motion.MyRunRate`) + PlayerDescription — or ships
the contingent AD row quoted in P1.
**V5 EXECUTED (2026-07-03): re-anchor taken, NO AD row needed.** Both
retail-mechanism feeds already existed: PlayerDescription run/jump skills
flow via `SetCharacterSkills` (K-fix7, wired since before V5 — the
"we don't parse PD yet" comment in the controller ctor was stale) into
`PlayerWeenie.InqRunRate`, which `apply_run_to_command`/`get_state_velocity`
PREFER over `MyRunRate`; and V5's shared `RouteServerMoveTo` performs the
M13 `Motion.MyRunRate = MoveToRunRate` write for the local player on
mt-6/7. `ApplyServerRunRate` was deleted outright — its
`InterpretedState.ForwardSpeed` overwrite was a pre-R3 mechanism that
fought the ported machinery (any DoMotion edge recomputes ForwardSpeed
through adjust_motion/apply_run_to_command).
- **LoseControlToServer seam (R5):** retail hands autonomy to the server at the
0xF74C dispatch whenever a non-autonomous movement event is applied to the
local player (raw 357214357235). Note for R5/MovementManager — the retail
hook `BeginServerAutoWalk` approximates today.
- **GameWindow.cs:4534-4546 comment is stale** (from P1): the "ACE follows every
mt=0x06 with an mt=0x00" causal story described the pre-#75 build's
self-inflicted echo; post-#75 no echo exists at moveto start. The comment
block dies with the code in V5 — do not port against it.
- **RemoteMoveToDriver.cs:53-57 stale claim FIXED in this commit** (plan §0
mandate): the class doc claimed "ACE swaps the chase/flee arrival predicates"
— refuted by r4-ace-moveto §1 and by the file's own :186-199 comment; retail
and ACE agree (chase arrives `dist <= distance_to_object`, flee arrives
`dist >= min_distance`).
## V0 cdb capture (pending, non-blocking)
No pin above depends on it — P1/P3 were adversarially sealed textually (P3 down
to the instruction bytes). One live retail session feeds V2/V4 goldens: bp
`MoveToManager::PerformMovement` / `BeginMoveForward` / `BeginTurnToHeading` /
`HandleMoveToPosition` / `HandleUpdateTarget` /
`MovementParameters::get_command` (args+ret) while the user runs the plan §0
protocol (door-use at range, use-while-facing-away, monster chase, outrun). A
`heading_diff` arg3+ST0 log during a TurnLeft-direction TurnToHeading would
spot-check P3's mirror live — nice-to-have only.