docs(L.2g-S2): verbatim inbound-funnel pseudocode + observer cdb trace script

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 18:33:23 +02:00
parent a2f8104cbf
commit 97e098bf91
2 changed files with 207 additions and 0 deletions

View file

@ -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] <fwd>` (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.

View file

@ -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