From 386b1ce550a933ad16a25e7ff3097906a9092c7e Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 3 Jul 2026 11:02:13 +0200 Subject: [PATCH] =?UTF-8?q?docs(R4-V0):=20pins=20P1-P7=20resolved=20?= =?UTF-8?q?=E2=80=94=20the=20autonomous-byte=20gate=20IS=20retail's=20echo?= =?UTF-8?q?=20discriminator;=20heading=5Fdiff=20mirror=20Ghidra-confirmed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 (BLOCKER -> RESOLVED, no adaptation needed): retail's CPhysics::SetObjectMovement (0x00509690) drops any movement event whose wire AUTONOMOUS byte is set when the addressed object IsThePlayer - BEFORE unpack_movement's unconditional head-interrupt ever runs. ACE's mt-0 MoveToState echo is IsAutonomous=1 HARDCODED (MovementData.cs:162) and only exists when the client itself sends a MoveToState (the 2026-05-14 'echo kills auto-walk' trace was the pre-#75 build whose overlay leaked outbound packets - post-#75 there is no echo at moveto start). acdream parses isAutonomous (UpdateMotion.cs:129) and consumes it NOWHERE; MotionSequenceGate cites 0x00509690 in its own doc but omitted this final branch. V-commits port the missing gate tail and the verbatim head-cancel lands cleanly; the informal GameWindow no-cancel-on-non-MoveTo note retires. P3 (Ghidra-confirmed live, textual verdict independently identical): heading_diff 0x00528fb0 HAS the TurnLeft mirror - 360-diff whenever the turn command != TurnRight; the extraction's 'arg unused' was a BN x87-setcc artifact (the W6b class). P2 Ghidra-confirmed: get_desired_heading = fwd-towards 0 / fwd-away 180 / back-towards 180 / back-away 0. ghidra-confirmations.md holds both raw decompiles. P4-P7 pinned per V0-pins.md. RemoteMoveToDriver's stale chase/flee class-doc claim corrected (retail and ACE AGREE on the predicates). Co-Authored-By: Claude Fable 5 --- docs/research/2026-07-03-r4-moveto/V0-pins.md | 319 ++++++++++++++++++ .../ghidra-confirmations.md | 58 ++++ .../Physics/RemoteMoveToDriver.cs | 10 +- 3 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 docs/research/2026-07-03-r4-moveto/V0-pins.md create mode 100644 docs/research/2026-07-03-r4-moveto/ghidra-confirmations.md diff --git a/docs/research/2026-07-03-r4-moveto/V0-pins.md b/docs/research/2026-07-03-r4-moveto/V0-pins.md new file mode 100644 index 00000000..d738159b --- /dev/null +++ b/docs/research/2026-07-03-r4-moveto/V0-pins.md @@ -0,0 +1,319 @@ +# 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. +- **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. diff --git a/docs/research/2026-07-03-r4-moveto/ghidra-confirmations.md b/docs/research/2026-07-03-r4-moveto/ghidra-confirmations.md new file mode 100644 index 00000000..00766b69 --- /dev/null +++ b/docs/research/2026-07-03-r4-moveto/ghidra-confirmations.md @@ -0,0 +1,58 @@ +# R4-V0 — Ghidra ground-truth decompiles (patchmem.gpr, fetched live 2026-07-03) + +Authoritative resolutions for the two pins the BN pseudo-C garbled. Fetched +via the Ghidra MCP while the V0 pin workflow ran; these OVERRIDE any textual +adjudication that disagrees. + +## P3 — `heading_diff` 0x00528fb0: the TurnLeft mirror EXISTS + +```c +float __cdecl heading_diff(float param_1, float param_2, ulong param_3) +{ + float fVar1; + + fVar1 = param_1 - param_2; + if (ABS(param_1 - param_2) < F_EPSILON) { + fVar1 = 0.0f; + } + if (fVar1 < -F_EPSILON) { + fVar1 = fVar1 + 360.0f; // ___real_43b40000 + } + if ((F_EPSILON < fVar1) && (param_3 != 0x6500000d)) { + fVar1 = 360.0f - fVar1; // the mirror — NOT-TurnRight only + } + return fVar1; +} +``` + +Adjudication of the research contradiction: +- `r4-moveto-decomp.md` §5g ("turn-command arg UNUSED") is **WRONG** — the + arg gates the mirror. +- `r4-ace-moveto.md`'s suspicion (an x87-garbled `360−diff` TurnLeft branch) + is **CONFIRMED**: the mirror applies whenever the turn command is not + TurnRight (0x6500000d), i.e. the TurnLeft direction measures the + complementary angle. Port verbatim with the epsilon literal + 0.000199999995f (F_EPSILON). + +## P2 — `MovementParameters::get_desired_heading` 0x0052aad0: ACE-shaped constants CONFIRMED + +```c +float __thiscall MovementParameters::get_desired_heading( + MovementParameters* this, ulong command, int movingAway) +{ + if ((command == 0x44000007) || (command == 0x45000005)) { // Run/WalkForward + if (movingAway == 0) + return 0.0f; + } + else { + if (command != 0x45000006) // not WalkBackward + return 0.0f; + if (movingAway != 0) + return 0.0f; + } + return 180.0f; // ___real_43340000 +} +``` + +Truth table: forward+towards → 0°; forward+away → 180°; +backward+towards → 180°; backward+away → 0°; any other command → 0°. diff --git a/src/AcDream.Core/Physics/RemoteMoveToDriver.cs b/src/AcDream.Core/Physics/RemoteMoveToDriver.cs index bc1e6d51..68f337ae 100644 --- a/src/AcDream.Core/Physics/RemoteMoveToDriver.cs +++ b/src/AcDream.Core/Physics/RemoteMoveToDriver.cs @@ -51,9 +51,13 @@ namespace AcDream.Core.Physics; /// /// /// -/// ACE divergence: ACE swaps the chase/flee arrival predicates -/// (dist <= DistanceToObject vs retail's dist <= MinDistance). -/// We follow retail. +/// Arrival predicates: retail and ACE AGREE — chase arrives at +/// dist <= distance_to_object, flee arrives at +/// dist >= min_distance (adjudicated in +/// docs/research/2026-07-03-r4-moveto/r4-ace-moveto.md §1; see the +/// arrival comment in ). An earlier revision of this doc +/// claimed ACE swapped the two predicates — that was wrong; do not port +/// against it. /// /// public static class RemoteMoveToDriver