acdream/docs/research/2026-07-02-r3-motioninterp/W0-pins.md
Erik cd0289bea2 docs(R3-W0): ambiguity pins A1-A10 — all textually resolved, adversarially verified
Workflow-produced pin pass over the R3 decomp extraction (3 independent
raw re-readers + adversarial refuters on the two load-bearing pins +
synthesis). No pin was refuted; none blocks on cdb.

Headlines:
- A1: motion_allows_jump 0x005279e0 is a BLOCKLIST (0 = pass, 0x48 =
  blocked) — the BN extraction's whitelist annotation was inverted
  (corrected in-place in §3a + §10). Retail blocks FALLEN 0x40000008
  and PASSES Falling 0x40000015; ACE mis-transcribed the exact-id term
  as Falling (one-off slip — ACE's own charge gates use Fallen).
  Definitive literal-uint blocklist table recorded.
- A3: the raw-vs-interpreted dual-dispatch gate is IsThePlayer (vtable
  slot +0x14, bound via the ACCWeenieObject vftable dump @0x007e3ea0),
  NOT ACE's IsCreature — in all four functions. Anti-artifact proof:
  HitGround/LeaveGround nearby call the +0x2c IsCreature slot, so BN
  distinguishes the slots locally. Copying ACE's gate would send
  remotes down apply_raw_movement against an empty raw state.
- A4: MovementParameters absolute-mask table pinned from acclient.h's
  own bitfield struct; retail ctor default 0x1EE0F has can_charge
  CLEAR (ACE sets it true) and walk_run_threshold 15.0 (ACE 1.0).
- A5/A6: both jump-velocity epsilons are 0.000199999995f (acdream's
  0.001 must change); get_leave_ground_velocity's fallback matrix is
  GLOBAL→LOCAL (index-pattern match against Frame::globaltolocalvec).
- A10: definitive error-code table from an exhaustive return-site
  sweep, incl. a second 0x24 site (DoInterpretedMotion action-class
  contact block) absent from the plan row.
- Adjacent: move_to_interpreted_state's apply_current_movement arg2 is
  a garbled allowJump = (motion_allows_jump(old fwd) == 0) — polarity
  trap for W-commits.

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

277 lines
19 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.

# R3-W0 — ambiguity pins and ACE-oddity adjudications
The verbatim extraction is `r3-motioninterp-decomp.md` (line-anchored); the port
plan with the full ambiguity table is `r3-port-plan.md` §0. This note records the
PINNED resolutions the W1W6 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) and A1/A3 additionally survived an adversarial
refutation pass (independent Ghidra MCP decompiles + vtable-dump slot resolution).
**No pin was refuted; none blocks on cdb.**
## Pinned
- **A1 — `motion_allows_jump` (0x005279e0) polarity: PINNED as a BLOCKLIST.
`0` = jump allowed (pass), `0x48` = jump BLOCKED** (a
"YouCantJumpFromThisPosition"-class error code). The extraction doc's original
§3a "whitelist / 0x48 = allowed" note was inverted (now corrected in-place, same
commit). Confidence: **high** (adversarially verified; refutation failed).
Definitive blocklist (port as **literal uint ranges**, NOT enum-ordinal ranges):
| Motion id (literal uint) | MotionCommand names | Returns |
|---|---|---|
| `[0x1000006f, 0x10000078]` | MagicPowerUp01..MagicPowerUp10 | `0x48` BLOCK |
| `[0x10000128, 0x10000131]` | TripleThrustLow..MagicPowerUp07Purple | `0x48` BLOCK |
| `0x40000008` (exact) | **Fallen** | `0x48` BLOCK |
| `[0x40000016, 0x40000018]` | Reload, Unload, Pickup | `0x48` BLOCK |
| `[0x4000001e, 0x40000039]` | AimLevel..MagicPray | `0x48` BLOCK |
| `[0x41000012, 0x41000014]` | Crouch, Sitting, Sleeping | `0x48` BLOCK |
| everything else — incl. **Falling `0x40000015`**, Ready `0x41000003`, Dead `0x40000011`, all turn/sidestep ids (`0x6500000d` TurnRight etc.) | — | `0` PASS |
Evidence (all boundaries re-derived from the verbatim body, raw 304908304931):
`005279e9 if (arg2 > 0x40000018) { 00527a24 if (arg2 > 0x41000014) return 0;
00527a39 if ((arg2 < 0x41000012 && (arg2 < 0x4000001e || arg2 > 0x40000039)))
return 0; } 005279e9 else if (arg2 < 0x40000016) { 005279f7 if (arg2 > 0x10000131)
{ 00527a18 if (arg2 != 0x40000008) 00527a1c return 0; } 005279f7 else if ((arg2 <
0x10000128 && (arg2 < 0x1000006f || arg2 > 0x10000078))) 00527a10 return 0; }
00527a40 return 0x48;` — the middle band `[0x40000016..0x40000018]` satisfies
neither outer branch and falls straight to `return 0x48`. Polarity sealed by four
independent callers plus the top of the chain:
1. `jump_is_allowed` 0x005282b0 (raw 305539305542): `eax_7 =
motion_allows_jump(this, this->interpreted_state.forward_command); if (eax_7
!= 0) return eax_7;` — nonzero returned verbatim as the failure code; all
success paths return `eax_7 == 0`.
2. `DoInterpretedMotion` 0x00528360 (raw 305597305607): params bit `0x20000`
(disable_jump_during_link) FORCES `eax_5 = 0x48` — a jump-*disable* bit
forcing the value only makes sense if `0x48` = block; `eax_5` is stashed as
the queue node's `jump_error_code` (add_to_queue arg4 → node +0xc, raw
305044), which `jump_is_allowed`'s head peek returns as an error when nonzero.
3. `CMotionInterp::jump` 0x00528780 (raw 305801305812): `result =
jump_is_allowed(...); if (result != 0) { standing_longjump = 0; return
result; }` — the jump only executes on the zero path.
4. Sibling convention: `jump_charge_is_allowed` (raw 304940304948) and
`charge_jump` (raw 305453305466) both `return 0` on pass / `0x48`/`0x49` on
block; `charge_jump`'s success path arms `standing_longjump` then `return 0`.
**Falling vs Fallen: retail blocks Fallen (0x40000008) and PASSES Falling
(0x40000015). ACE mis-transcribed** — ACE MotionInterp.cs:770-779's exact-id
term is `substate == (uint)MotionCommand.Falling` and Fallen appears nowhere in
its motion_allows_jump: the exact inverse of retail on those two ids. ACE's own
jump_charge_is_allowed (MotionInterp.cs:736) uses Fallen, matching retail's
0x40000008 in both charge gates — the Falling term is a one-off ACE transposition
slip, not a deliberate change. Falling still can't yield mid-air jumps in retail:
`jump_is_allowed`'s airborne gate (raw 305559305570) independently returns
`0x24` unless transient_state has Contact(1)+OnWalkable(2). **Port: block Fallen,
pass Falling.**
- **A2 — `jump_is_allowed` pending-head peek: PINNED — the peek fires whenever the
queue is non-empty; there is NO `Count > 1` gate.** Raw 305524305556
(0x005282b0): `if (CPhysicsObj::IsFullyConstrained(this->physics_obj) != 0)
return 0x47;` THEN `class LListData* head_ = this->pending_motions.head_; ...
if (head_ != 0) eax_6 = *(int32_t*)((char*)head_ + 0xc); if ((head_ == 0 ||
eax_6 == 0)) { eax_6 = jump_charge_is_allowed(this); ... } return eax_6;` — if
the head exists AND its `jump_error_code` (+0xc) is nonzero, the whole
charge/motion/stamina chain is skipped and the peeked code returned. Ordering
note (both decompilers agree): **IsFullyConstrained (→0x47) is checked BEFORE
the peek** — port in that order. ACE L752-753's `PendingMotions.Count > 1 &&`
has no retail counterpart; do not copy. ACE L746's `WeenieObj == null &&
!WeenieObj.IsCreature()` NPE-typo also confirmed wrong — retail (raw
305517305520) is `if (weenie_obj != 0) eax_2 = weenie_obj->vtable->IsCreature();
if ((weenie_obj != 0 && eax_2 == 0))` → non-creature-with-weenie skips the ground
check. Confidence: **high**.
- **A3 — dual-dispatch gate: PINNED to `IsThePlayer` (vtable slot +0x14), NOT
`IsCreature` (+0x2c), in all four functions.** Confidence: **high**
(adversarially verified: independent Ghidra decompile shows raw offset +0x14 at
all four sites vs +0x2c in HitGround; the ACCWeenieObject vftable dump at
0x007e3ea0, raw 10351011035135, binds +0x14 → `ACCWeenieObject::IsThePlayer`
0x0058C3D0 and +0x2c → `IsCreature` 0x0058C3F0).
| Function | Address @ raw line | Weenie tested | Predicate → raw path | Dispatch args | Entry gate |
|---|---|---|---|---|---|
| `apply_current_movement` | 0x00528870 @305838 | `this->weenie_obj` | `(weenie==0 \|\| IsThePlayer()) && movement_is_autonomous()` | `(arg2, arg3)` passthrough | `physics_obj != 0 && initted != 0` |
| `ReportExhaustion` | 0x005288d0 @305861 | `this->weenie_obj` | same | `(0, 0)` | same |
| `SetWeenieObject` | 0x00528920 @305884 | **incoming `arg2`** (stored to `this->weenie_obj` first) | same | `(1, 0)` | same |
| `SetPhysicsObject` | 0x00528970 @305911 | `this->weenie_obj` | same | `(1, 0)` | `arg2 != 0 && initted != 0` (after `this->physics_obj = arg2`) |
Predicate true → `apply_raw_movement(a, b)`; false → `apply_interpreted_movement(a, b)`.
Verbatim (apply_current_movement, raw 305846305855): `if (weenie_obj != 0)
eax_2 = weenie_obj->vtable->IsThePlayer(); if (((weenie_obj == 0 || eax_2 != 0)
&& CPhysicsObj::movement_is_autonomous(this->physics_obj) != 0)) {
apply_raw_movement(this, arg2, arg3); return; } apply_interpreted_movement(this,
arg2, arg3);`. Semantics: `IsThePlayer` (raw 406494406500) = `this->id ==
SmartBox::smartbox->player_id` (neg/sbb idiom); `movement_is_autonomous` (raw
276443276447) = `return this->last_move_was_autonomous;`. Anti-artifact proof:
`HitGround` (raw 306005) and `LeaveGround` (raw 306031), ~0x250 bytes away in
the same decomp region, call `IsCreature()` — BN distinguishes the two slots
locally, ruling out a slot-shift mislabel. **ACE MotionInterp.cs:430-438's
`IsCreature` predicate is a genuine server-side divergence — do NOT copy** (a
remote player is a creature but not the player; the wrong gate sends remotes
down `apply_raw_movement` against an empty raw state).
Watchouts: (a) a FIFTH IsThePlayer site exists in `move_to_interpreted_state`
(raw 305977): per queued action node, `if ((weenie_obj->vtable->IsThePlayer()
== 0 || node->autonomous == 0))` → execute — i.e. server-echoed autonomous
actions are SKIPPED for the local player (local-echo suppression). (b) the BASE
`CWeenieObject::vftable` data dump at 0x007e3df4 (raw 10350611035094) is
misaligned (BN data-label overlap artifact) — only the derived ACC table at
0x007e3ea0 is trustworthy. (c) the PDB also has a distinct
`ACCWeenieObject::IsPlayer` (slot +0x10, 0x0058C890) — never conflate the three
`Is*Player*` virtuals.
- **A4 — MovementParameters bit numbering: PINNED via the retail PDB's own
bitfield struct, acclient.h:31423-31443** — bit-for-bit identical to ACE's
`MovementParamFlags`. Confidence: **high**. Absolute masks:
| Mask | Flag | Mask | Flag |
|---|---|---|---|
| `0x1` | can_walk | `0x400` | use_spheres |
| `0x2` | can_run | `0x800` | set_hold_key |
| `0x4` | can_sidestep | `0x1000` | autonomous |
| `0x8` | can_walk_backwards | `0x2000` | modify_raw_state |
| `0x10` | can_charge | `0x4000` | modify_interpreted_state |
| `0x20` | fail_walk | `0x8000` | cancel_moveto |
| `0x40` | use_final_heading | `0x10000` | stop_completely |
| `0x80` | sticky | `0x20000` | disable_jump_during_link |
| `0x100` | move_away | | |
| `0x200` | move_towards | | |
Ctor default (raw 300510300534, 0x00524380): `(bitfield & 0xfffdee0f) |
0x1ee0f` → **0x1EE0F** sets {can_walk, can_run, can_sidestep,
can_walk_backwards, move_towards, use_spheres, set_hold_key, modify_raw_state,
modify_interpreted_state, cancel_moveto, stop_completely}; clears {can_charge,
fail_walk, use_final_heading, sticky, move_away, autonomous,
disable_jump_during_link}. Runtime reads all consistent: DoMotion @306183
byte1-sign = `0x8000` cancel_moveto → interrupt; @306188 byte1&8 = `0x800`
set_hold_key → `SetHoldKey(hold_key_to_apply, ((__inner0 >> 0xf) & 1))` — note
**SetHoldKey's second arg is the cancel_moveto bit**, not a constant; @306213
byte1&0x20 = `0x2000` modify_raw_state → RawMotionState::ApplyMotion mirror.
DoInterpretedMotion @305597 full-dword `& 0x20000` = disable_jump_during_link →
jump_error_code forced `0x48`; @305609/305617 byte1&0x40 = `0x4000`
modify_interpreted_state. StopMotion @305684 sign → interrupt; @305705
byte1&0x20 → RawState.RemoveMotion.
**ACE-divergence traps for W1:** ACE's MovementParameters ctor sets
`CanCharge = true` (MovementParameters.cs:58) — retail's default has `0x10`
CLEAR; port `can_charge = false`. ACE also changed Default_WalkRunThreshold to
1.0 (L50) vs retail's **15.0** (@300519). Retail's ctor caches the computed
default in a static (`normal_bitfield`, @300523300533) whose AND-mask preserves
uninitialized bits 1831 on first call — harmless (only bits 017 defined); port
as a plain 0x1EE0F default.
- **A5 — `get_jump_v_z` (0x00527aa0): PINNED to ACE's shape; the BN text is
x87-flag garbled exactly as suspected** (raw 304953304984 returns FCOM flag
synthesis on two paths — unreadable either way; the Ghidra decompile is clean
and adjudicates). Behavior: `jump_extent < 0.000199999995f` → **0.0f**; else
clamp extent to max **1.0**; `weenie_obj == NULL` → **10.0f** (`___real_41200000`);
`InqJumpVelocity(extent, &out)` false → **0.0f**; true → out. Matches ACE
L634-652. The epsilon literal is verbatim in the raw @304959: `long double
temp0 = ((long double)0.000199999995f);`. **acdream's 0.001
(MotionInterpreter.cs:278 JumpExtentEpsilon) must become 0.000199999995f.**
Confidence: **high**.
- **A6 — `get_leave_ground_velocity` (0x005280c0) zero-fallback matrix direction:
PINNED as GLOBAL→LOCAL** — decisively, by index-pattern match against Frame's
own transform pair. The fallback (raw 305434305440) computes `out.i =
fl2gv[3i+0]*v.x + fl2gv[3i+1]*v.y + fl2gv[3i+2]*v.z` — the IDENTICAL row-linear
pattern as `Frame::globaltolocalvec` 0x00452550 (raw 9146691471), while
`Frame::localtoglobalvec` 0x004524f0 (raw 9145291457) uses the transpose
(`fl2gv[0]*x + fl2gv[3]*y + fl2gv[6]*z`). BN's "m_fl2gv = local→global" reading
was name-misled; the applied math is global→local, matching ACE's
`GlobalToLocalVec(Velocity)`. **acdream's `Quaternion.Inverse(Orientation)`
transform (MotionInterpreter.cs:1024) IS global→local — keep.** Mechanics:
function is void (writes the caller's Vector3); body order =
`get_state_velocity(out)` → `out.z = get_jump_v_z()` → fallback fires ONLY when
`|x| AND |y| AND |z|` are ALL `< 0.000199999995f` (epsilon appears three times,
@305412/305420/305427, one per component) and then overwrites ALL THREE
components (including z, discarding the jump v_z) with
`globaltolocal(physics_obj->m_velocityVector)`. Epsilon must become 0.0002
(currently 0.001). Confidence: **high**.
- **A7 — `MovementManager::MotionDone` uninit-`edx` relay + dead `arg2` in
`CMotionInterp::MotionDone`.** Decompiler artifact (adjudicated in the decomp
doc §6d). Port as `MotionDone(uint motion, bool success)` passing the completed
node's motion id; the interp body reads NEITHER param in this build — **pop the
HEAD unconditionally, never match by motion id**. Keep the params for R5
signature parity. (Copied verbatim from the plan §0 adjudication — no new
research needed.)
- **A8 — `enter_default_state` (0x00528c80) does NOT clear `pending_motions`: it
APPENDS the `{0, 0x41000003, 0}` sentinel onto whatever queue exists.** Raw
306128306154: the body resets only raw_state + interpreted_state (fresh
default-constructed copies) and calls `CPhysicsObj::InitializeMotionTables`,
then `void* eax_2 = operator new(0x10); ... *(+8) = 0x41000003; *(+0xc) = 0;`
followed by the tail-append (`tail_ == 0 ? head_ = eax_2 : *tail_ = eax_2;
tail_ = eax_2`) — byte-identical in shape to `add_to_queue`'s append (raw
305047305057), just inlined — then `initted = 1; LeaveGround(this);`. No
drain/clear anywhere. ACE's `PendingMotions = new LinkedList` reset is the
divergence; pre-existing nodes survive in retail (W2 test asserts this). The
MovementManager lazy-create double-call (§6g) is genuine retail — do not "fix";
R3's direct-bind construction calls it once explicitly. Confidence: **high**.
- **A9 — `StopCompletely` (0x00527e40) jump-snapshot quirk: PINNED verbatim.**
Raw 305216305227 ordering: `interrupt_current_movement(physics_obj);` → `eax_2
= motion_allows_jump(this, this->interpreted_state.forward_command);` (**OLD**
forward_command, BEFORE the overwrite) → write raw_state
{forward_command=0x41000003, forward_speed=1f, sidestep_command=0,
turn_command=0} → same four writes to interpreted_state →
`StopCompletely_Internal(physics_obj)` → `add_to_queue(this, 0, 0x41000003,
eax_2)` → cell==0 → `RemoveLinkAnimations` → `return 0`. Entry: physics_obj==0
→ return 8. **Retail touches ONLY forward cmd/speed + sidestep/turn COMMANDS —
it does NOT write sidestep_speed or turn_speed** (acdream's 1.0 speed resets
are a divergence to remove per plan J9). Golden: the queued node's error code
reflects the pre-stop command. Confidence: **high**.
- **A10 — error-code semantics: PINNED with the definitive table** (exhaustive
`return <code>` sweep over raw 304908306277 + 300150300540; 19 return sites +
1 store site, all attributed). Codes are local-only (never on the wire) — safe
renumber in W1. Confidence: **high**.
| Code | Retail meaning | Sites (address @ raw line) |
|---|---|---|
| `8` | no physics_obj | StopCompletely @305214; DoInterpretedMotion @305579; StopInterpretedMotion @305639; StopMotion @305680; jump @305798; DoMotion @306165 |
| `0x24` | not grounded / no contact | jump_is_allowed @305570 (0x005282f0 — gravity-state creature without Contact+OnWalkable; **also covers its physics_obj==0 case**, see note); **DoInterpretedMotion @305622-305623** (action-class motion `arg2 & 0x10000000` blocked by contact_allows_move — second site, absent from the plan row) |
| `0x3f` | Crouch 0x41000012 in combat stance | DoMotion @306196 |
| `0x40` | Sitting 0x41000013 in combat stance | DoMotion @306199 |
| `0x41` | Sleeping 0x41000014 in combat stance | DoMotion @306202 |
| `0x42` | `motion & 0x2000000` outside NonCombat (0x8000003d) | DoMotion @306205 |
| `0x45` | action depth: GetNumActions ≥ 6 | DoMotion @306209 |
| `0x47` | general movement failure | jump_is_allowed @305525 (IsFullyConstrained); jump_is_allowed @305549+305556 (staged, returned when `JumpStaminaCost(arg2, arg3) == 0`); CMotionInterp::PerformMovement @306227 (`type-1 > 4`); MovementManager::PerformMovement @300201 (`type-1 > 8`) |
| `0x48` | jump BLOCKED by motion/position (**NOT a success sentinel** — the A1 polarity fix) | motion_allows_jump @304930; jump_charge_is_allowed @304948 (Fallen or Crouch..Sleeping); charge_jump @305459 (same predicate); STORED (not returned) as node jump_error_code in DoInterpretedMotion @305605 on disable_jump_during_link |
| `0x49` | weenie `CanJump(jump_extent)` refused | jump_charge_is_allowed @304941; charge_jump @305454 |
Note: `jump_is_allowed` returns **0x24** (not 8) when `physics_obj == 0` (raw
structure 305512 + 305570: the `if (this->physics_obj != 0) {...}` wrapper falls
out to `return 0x24;`) — the "8 = no physics obj" convention holds everywhere
else but NOT there; ACE (L744-745) returns NoPhysicsObject there — small ACE
divergence, port retail. W1 renames acdream's WeenieError values to these
numerics (adds 0x3f/0x40/0x41/0x42/0x45; fixes 0x24/0x47/0x48 semantics — the
current enum returns 0x48 for airborne where retail returns 0x24).
## Adjacent findings (load-bearing for W-commits, discovered while pinning)
- **`move_to_interpreted_state` call-site garble (W-commit trap):** raw
305946305949 renders `int32_t esi_1 = -(eax_2); ...
apply_current_movement(this, 1, -((esi_1 - esi_1)));` — the second arg
algebraically collapses to constant 0, which is decompiler nonsense (sbb/neg
idiom). Intended semantics per ACE MotionInterp.cs:796-799: `allowJump =
(motion_allows_jump(OLD forward_command) == 0)` passed as
apply_current_movement's second arg. Port `allowJump = (eax_2 == 0)` — another
minor polarity trap in the A1 family.
- **`jump` (0x00528780) details for J7:** contains `interrupt_current_movement`
(@305800); on success stores `jump_extent = arg2` and calls
`set_on_walkable(obj, 0)`; clears `standing_longjump` ONLY on the failure path.
## W0 cdb capture (pending, non-blocking)
No pin above depends on it — all ten are textually pinned with adversarial
verification. One live session feeds the R3 goldens (W2/W3/W5/W6): bp
`CMotionInterp::MotionDone` / `add_to_queue` / `jump` / `jump_is_allowed` /
`jump_charge_is_allowed` / `charge_jump` / `motion_allows_jump` / `HitGround` /
`LeaveGround` / `StopCompletely` / `set_hold_run` / `SetHoldKey` /
`enter_default_state` with arg+ret logging (pattern `tools/cdb/l2g-observer.cdb`);
user protocol per `r3-port-plan.md` §0 (idle / walk / run / shift-toggle /
tap-jump / full-charge jump / running jump / standing long-jump / jump-while-
crouched / jump-in-combat-stance (0x3f0x42 family) / walk off a ledge (the A6
momentum fallback) / land / emote-while-running / 7 rapid emotes (0x45 cap) /
logout drain). Nice-to-have live spot-checks it will also provide: A1 polarity
(log `motion_allows_jump` arg2+eax during walk/run/mid-fall/crouch jumps) and the
A2 head-peek short-circuit.