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>
330 lines
22 KiB
Markdown
330 lines
22 KiB
Markdown
# 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 V1–V6 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 271370–271431):
|
||
`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 271458–271485) 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 300566–300573;
|
||
jump table @300707–300719 — types 1–5 fall to return 0, head already ran).
|
||
`interrupt_current_movement` = `if (movement_manager)
|
||
MovementManager::CancelMoveTo(0x36)` (raw 278189–278200), a no-op when
|
||
`moveto_manager == 0` (raw 300277–300288 @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 357214–357235: 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
|
||
(308016–308033) 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
|
||
307227–307233, 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 306344–306346) 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 @307802–307867: `!= 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 438288–438290): `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 320055–320066) 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 357214–357235). 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.
|