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

19 KiB
Raw Blame History

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) | 0x1ee0f0x1EE0F 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.000199999995f0.0f; else clamp extent to max 1.0; weenie_obj == NULL10.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 → RemoveLinkAnimationsreturn 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.