acdream/docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md
Erik 97e098bf91 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>
2026-07-02 18:33:23 +02:00

7.8 KiB

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.