From 97e098bf912599290ca2f7b9004997ecdeeab707 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 2 Jul 2026 18:33:23 +0200 Subject: [PATCH] docs(L.2g-S2): verbatim inbound-funnel pseudocode + observer cdb trace script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pseudocode for the S2 port (unpack_movement case 0 / move_to_interpreted_state / apply_current_movement / apply_interpreted_movement / DoInterpretedMotion), anchored on decomp lines + validated against a LIVE cdb trace of a retail observer (per-UM DIM order confirmed: style -> forward -> sidestep-stop -> turn-stop; empty UM = wholesale Ready stop). Also settles the packer question: RawMotionState::Pack (0x0051ed10) is pure static-default-difference — outbound L.2b port already verbatim; the empty-vs-explicit walk variance between captures is driver-client state, handled identically by the wholesale apply. Co-Authored-By: Claude Fable 5 --- ...2026-07-02-s2-inbound-funnel-pseudocode.md | 165 ++++++++++++++++++ tools/cdb/l2g-observer.cdb | 42 +++++ 2 files changed, 207 insertions(+) create mode 100644 docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md create mode 100644 tools/cdb/l2g-observer.cdb diff --git a/docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md b/docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md new file mode 100644 index 00000000..ee8648c6 --- /dev/null +++ b/docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md @@ -0,0 +1,165 @@ +# L.2g S2 — inbound CMotionInterp funnel: verbatim pseudocode + +Date: 2026-07-02. Oracle: named decomp (line refs into +`docs/research/named-retail/acclient_2013_pseudo_c.txt`) + the LIVE cdb trace +of a retail observer (`l2g-observer-trace.log`, breakpoint script +`tools/cdb/l2g-observer.cdb`) which confirmed the exact runtime chain and +per-UM `DoInterpretedMotion` order: **style → forward → sidestep(-stop) → +turn(-stop)**, applied wholesale the tick the message arrives. + +Empty-UM semantics (settled): `InterpretedMotionState::UnPack` (0x0051f400, +294360) decodes absent fields to ctor defaults — style `0x8000003D`, forward +`0x41000003 Ready`, speeds `1.0`, sidestep/turn `0` — so a flags=0 UM is a +retail-verbatim FULL STOP. No special-casing. (The wire varies with the +DRIVER's client state; both explicit-walk and empty variants are handled by +the same wholesale apply. `RawMotionState::Pack` 0x0051ed10 confirmed pure +static-default-diff — outbound L.2b port stays as-is.) + +## 1. MovementManager::unpack_movement — case 0 (0x00524440, 300563) + +``` +unpack_movement(blob): + if minterp == null or physics_obj == null: return 0 + physics_obj.interrupt_current_movement() + physics_obj.unstick_from_object() + + u16 header = read_u16() # low byte: movement type; high byte: motionFlags + u16 styleIdx = read_u16() # outer style, command_ids[] index + style = command_ids[styleIdx] + if minterp.get_current_style() != style: # "GetPinVersion" in BN + minterp.DoMotion(style, default_params) # style applied ONLY on change + + switch header.low_byte: + case 0: # InterpretedMotionState + ims = InterpretedMotionState() # ctor defaults (see above) + ims.UnPack(blob) # absent fields keep defaults + stickyGuid = (header & 0x100) ? read_u32() : 0 + MovementManager.move_to_interpreted_state(ims) + if stickyGuid: physics_obj.stick_to_object(stickyGuid) + minterp.standing_longjump = (header & 0x200) + return 1 + case 6/7: MoveTo… (existing acdream path, keep) + case 8/9: TurnTo… (S6) + default: return 0 +``` + +## 2. CMotionInterp::move_to_interpreted_state (0x005289c0, 305936) + +``` +move_to_interpreted_state(ims): + if physics_obj == null: return 0 + raw_state.current_style = ims.current_style + physics_obj.interrupt_current_movement() + jumpAllowed = motion_allows_jump(interpreted_state.forward_command) # OLD state! + interpreted_state.copy_movement_from(ims) # FLAT overwrite (0x0051e750) + apply_current_movement(force=1, jumpAllowed) + + for action in ims.actions: # MotionItem list + # 15-bit wraparound stamp gate vs server_action_stamp (305953-305971) + if newer_15bit(action.stamp, server_action_stamp): + if weenie is player and action.autonomous: skip # local echo guard + server_action_stamp = action.stamp + DoInterpretedMotion(action.command, params(speed=action.speed)) + return 1 +``` + +## 3. CMotionInterp::apply_current_movement (0x00528870, 305838) + +``` +apply_current_movement(force, jumpAllowed): + if physics_obj == null or !initted: return + if (weenie==null or weenie.IsThePlayer()) and physics_obj.movement_is_autonomous(): + apply_raw_movement(force, jumpAllowed) # LOCAL player (already ported, D6) + else: + apply_interpreted_movement(force, jumpAllowed) # REMOTES — this port +``` + +## 4. CMotionInterp::apply_interpreted_movement (0x00528600, 305713) + +``` +apply_interpreted_movement(force, jumpAllowed): + if physics_obj == null: return + if interpreted_state.forward_command == RunForward (0x44000007): + my_run_rate = interpreted_state.forward_speed # cache server run rate + + DoInterpretedMotion(interpreted_state.current_style, {}) # stance + + if !contact_allows_move(interpreted_state.forward_command): + DoInterpretedMotion(0x40000015 Falling, {}) + elif standing_longjump: + DoInterpretedMotion(0x41000003 Ready, {}) + StopInterpretedMotion(0x6500000F SideStep…, {}) + else: + DoInterpretedMotion(interpreted_state.forward_command, + {speed: interpreted_state.forward_speed}) + if interpreted_state.sidestep_command == 0: + StopInterpretedMotion(0x6500000F, {}) + else: + DoInterpretedMotion(interpreted_state.sidestep_command, + {speed: interpreted_state.sidestep_speed}) + + if interpreted_state.turn_command != 0: + DoInterpretedMotion(interpreted_state.turn_command, + {speed: interpreted_state.turn_speed}) + return # early — no idle-stop this call + if StopInterpretedMotion(0x6500000D Turn…, {}) == 0: + add_to_queue(ctx=0, Ready, tick) # idle bookkeeping (S3 wires fully) +``` + +Live-trace confirmation (actor minterp 18e8b0f8): per UM exactly +`[DIM] 0x8000003D` then `[DIM] ` (0x45000005 / 0x41000003 / 0x44000007) +then sidestep/turn stops (0x6500000F / 0x6500000D) — order verbatim. + +## 5. CMotionInterp::DoInterpretedMotion (0x00528360, 305575) + +``` +DoInterpretedMotion(motion, params): + if physics_obj == null: return 8 + if contact_allows_move(motion): + if standing_longjump and motion in jump-set: goto apply_only + if motion == 0x40000011: physics_obj.RemoveLinkAnimations() + result = physics_obj.DoInterpretedMotion(motion, params) + # → CPartArray → MotionTableManager::PerformMovement + # → CMotionTable::GetObjectSequence ≙ AnimationSequencer.SetCycle + if result == 0: + add_to_queue(params.context_id, motion, jumpAllowed) # pending_motions (S3) + if params.flags & 0x40: interpreted_state.ApplyMotion(motion, params) + elif (motion & 0x10000000) == 0: + apply_only: + if params.flags & 0x40: interpreted_state.ApplyMotion(motion, params) + result = 0 + else: + result = 0x24 + if physics_obj != null and physics_obj.cell == 0: + physics_obj.RemoveLinkAnimations() + return result +``` + +## acdream mapping (surgical) + +- **New in `MotionInterpreter` (Core.Physics):** `MoveToInterpretedState(ims)`, + `ApplyInterpretedMovement()`, using the EXISTING `DoInterpretedMotion` / + `StopInterpretedMotion` (extended to retail semantics above) with the + sequencer as the `GetObjectSequence` backend. `InterpretedMotionStateData` + = a plain struct mirroring the ctor defaults; built from + `UpdateMotion.Parsed` (absent wire fields → defaults — the parser already + yields nullables; the CONVERSION applies the defaults). +- **`OnLiveMotionUpdated` remote SubState branch collapses** to: build ims → + `remoteMot.Motion.MoveToInterpretedState(ims, sequencer)`. Style-on-change + at the unpack level (today's `fullStyle` preserve-current logic maps to + `get_current_style()` compare). PRESERVE deliberately: K-fix17 airborne + cycle guard, HasCycle fallback chain, MoveTo (case 6/7) seeding, overlay + (Action-class) routing — these live in the DIM backend, not deleted. +- **Stop** rides the same path: empty UM → ims defaults → fwd=Ready → + apply → sequencer Ready + `get_state_velocity`→0 → body velocity zero. The + acdream-invented 300 ms stop-detection window + UP-near-zero StopCompletely + become dead code to remove in the same slice (DEV-3). +- **`my_run_rate` caching** per remote (apply_interpreted_movement head). +- Tests first: fake-sequencer call-order tests per UM shape (empty, walk, + run@2.85, run+turn, action list, stale action stamp), then live smoke. + +## Live-trace answer to the S0-open question + +Retail observer applies EVERY accepted UM wholesale — including empties as +full stops. "Correct behavior" (user-verified during the trace) comes from +this + the chase, not from any inference. acdream's S2 target = exactly this. diff --git a/tools/cdb/l2g-observer.cdb b/tools/cdb/l2g-observer.cdb new file mode 100644 index 00000000..c14ebb00 --- /dev/null +++ b/tools/cdb/l2g-observer.cdb @@ -0,0 +1,42 @@ +* L.2g S2-blocker trace — what does a RETAIL OBSERVER do with ACE's +* flags=0 "empty" UpdateMotion while the actor toggles walk/run? +* +* Attach target: the retail OBSERVER client (standing still, watching). +* Driver: the user Shift-toggles + starts/stops the ACTOR from the other +* retail client. +* +* Expected chain per decomp (to confirm or refute): +* CPhysics::SetObjectMovement (gates, stamps) [SOM] + obj id +* MovementManager::unpack_movement (10-way dispatch) [UNPACK] +* CMotionInterp::move_to_interpreted_state (flat copy) [MTIS] + IMS dump +* CMotionInterp::apply_interpreted_movement [AIM] +* CMotionInterp::apply_raw_movement (local-player alt) [ARM] +* CMotionInterp::DoInterpretedMotion per axis [DIM] + motion id +* +* The [MTIS] InterpretedMotionState dump on an empty UM is THE answer: +* if forward_command=0x41000003 (Ready) is flat-applied and [DIM] shows +* Ready, retail really does stop the remote — and the observer's clean +* rendering must come from somewhere else. If the dump or the DIM stream +* shows something else (e.g. WalkForward), we found the mechanism. +* +* Auto-detach: after 600 SetObjectMovement hits via .detach (NOT qd — +* qd is silently ignored in bp actions, see project_retail_debugger.md). + +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\vigorous-joliot-f0c3ad\l2g-observer-trace.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +x acclient!CPhysics::SetObjectMovement +x acclient!MovementManager::unpack_movement +x acclient!CMotionInterp::move_to_interpreted_state +x acclient!CMotionInterp::DoInterpretedMotion + +r $t0 = 0 +bp acclient!CPhysics::SetObjectMovement "r $t0 = @$t0 + 1; .printf \"\\n[SOM %d] obj=%p movSeq=%x scSeq=%x auto=%x id: \", @$t0, poi(@esp+4), poi(@esp+10)&0xffff, poi(@esp+14)&0xffff, poi(@esp+18); dt acclient!CPhysicsObj poi(@esp+4) id; .if (@$t0 >= 250) { .detach } .else { gc }" +bp acclient!MovementManager::unpack_movement ".printf \"[UNPACK] mm=%p\\n\", @ecx; gc" +bp acclient!CMotionInterp::move_to_interpreted_state ".printf \"[MTIS] minterp=%p ims:\\n\", @ecx; dt acclient!InterpretedMotionState poi(@esp+4); gc" +bp acclient!CMotionInterp::apply_interpreted_movement ".printf \"[AIM] minterp=%p\\n\", @ecx; gc" +bp acclient!CMotionInterp::apply_raw_movement ".printf \"[ARM] minterp=%p\\n\", @ecx; gc" +bp acclient!CMotionInterp::DoInterpretedMotion ".printf \"[DIM] minterp=%p motion=%08x\\n\", @ecx, poi(@esp+4); gc" +g