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:
parent
a2f8104cbf
commit
97e098bf91
2 changed files with 207 additions and 0 deletions
165
docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md
Normal file
165
docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md
Normal 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.
|
||||
42
tools/cdb/l2g-observer.cdb
Normal file
42
tools/cdb/l2g-observer.cdb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue