# L.3 port — per-axis UM dispatch DEEP DIVE

**Source:** `docs/research/named-retail/acclient_2013_pseudo_c.txt`
(Sept 2013 EoR build). All line numbers cite that file.

This document picks up where `02-um-handling.md` left off: it goes
DEEPER into the per-axis dispatch chain that fires on each UM (and
on every per-tick re-application). It is the canonical reference for
"when an UpdateMotion arrives with forward+sidestep+turn populated,
what does retail call, in what order, and what does it write?"

---

## 0. Ten-second summary

`MovementManager::unpack_movement` (300563) reads movement_type=0,
calls `InterpretedMotionState::UnPack` (294360, see §5 of doc 02), then
calls `MovementManager::move_to_interpreted_state` (300259) which
delegates to **`CMotionInterp::move_to_interpreted_state`** (305936).

That function does TWO things:

1. **`InterpretedMotionState::copy_movement_from`** — bulk-overwrite
   all 7 fields of the body's InterpretedState with the wire values
   (current_style, fwd_cmd, fwd_speed, side_cmd, side_speed, turn_cmd,
   turn_speed). Unconditional. No diffing.
2. **`apply_current_movement(cancelMoveTo=1, allowJump=…)`** (305838),
   which routes to either `apply_raw_movement` (autonomous local
   player) or **`apply_interpreted_movement`** (305713) — the latter
   is what fires for remote observers.

`apply_interpreted_movement` is the **per-axis dispatcher**. It calls
`DoInterpretedMotion` (305575) for each of: current_style, forward
axis, sidestep axis (or stop), turn axis (or stop). Each call ends in
`CPhysicsObj::DoInterpretedMotion` → `set_local_velocity(get_state_velocity())`.

**The "staircase" bug acdream's env-var path showed = absence of this
per-axis dispatch on the per-tick remote driver.** The default acdream
path runs `apply_current_movement` (correct) on UM intake, but the
env-var experimental path bypassed it and only ran the manager's
position lerp — so velocity was never re-derived, and Z stayed
flat between UMs even when the wire said "running up a slope."

---

## 1. CMotionInterp::PerformMovement (the top-level command pump)

**Function:** `CMotionInterp::PerformMovement`
**Address:** `0x00528e80`
**Lines:** 306221–306268.

```c
00528e80  uint32_t PerformMovement(this, MovementStruct const* arg2)
{
    int32_t ecx_1 = (arg2->type - 1);
    if (ecx_1 > 4) return 0x47;            // BadMovementType
    switch (ecx_1) {
        case 0: /* DoMotion */
            uint32_t eax = DoMotion(this, arg2->motion, arg2->params);
            CPhysicsObj::CheckForCompletedMotions(this->physics_obj);
            return eax;
        case 1: /* DoInterpretedMotion */
            uint32_t eax_2 = DoInterpretedMotion(this, arg2->motion, arg2->params);
            CPhysicsObj::CheckForCompletedMotions(this->physics_obj);
            return eax_2;
        case 2: /* StopMotion */
            uint32_t eax_4 = StopMotion(this, arg2->motion, arg2->params);
            CPhysicsObj::CheckForCompletedMotions(this->physics_obj);
            return eax_4;
        case 3: /* StopInterpretedMotion */
            uint32_t eax_6 = StopInterpretedMotion(this, arg2->motion, arg2->params);
            CPhysicsObj::CheckForCompletedMotions(this->physics_obj);
            return eax_6;
        case 4: /* StopCompletely */
            StopCompletely(this);
            CPhysicsObj::CheckForCompletedMotions(this->physics_obj);
            return 0;
    }
}
```

**Behavior:** dispatch by `MovementStruct::type` (1..5):

| type | meaning | handler |
|---|---|---|
| 1 | DoMotion (raw, with adjust_motion) | DoMotion (306159) |
| 2 | DoInterpretedMotion (already adjusted) | DoInterpretedMotion (305575) |
| 3 | StopMotion (raw stop) | StopMotion (305674) |
| 4 | StopInterpretedMotion (interpreted stop) | StopInterpretedMotion (305635) |
| 5 | StopCompletely | StopCompletely (305208) |

The wrapping `MovementManager::PerformMovement` (300194) chooses
between this command pump (types 1–5 → `CMotionInterp`) and the
MoveTo manager (types 6–9 → `MoveToManager`). **Inbound 0xF74C
RawCommand packets do NOT call PerformMovement** — they call
`move_to_interpreted_state` directly. PerformMovement is the API for
LOCAL command sources (CommandInterpreter, MoveToManager re-emits,
slash commands).

---

## 2. CMotionInterp::DoMotion (the raw command path)

**Function:** `CMotionInterp::DoMotion`
**Address:** `0x00528d20`
**Lines:** 306159–306217.

```c
00528d20  uint32_t DoMotion(this, uint32_t motion, MovementParameters const* params)
{
    if (physics_obj == 0) return 8;

    uint32_t ebp = motion;                                    // saved unmodified

    if ((params->__inner0.byte1 & 0x80) != 0)                 // CancelMoveTo bit
        CPhysicsObj::interrupt_current_movement(physics_obj);

    if ((params->__inner0.byte1 & 0x08) != 0)                 // SetHoldKey bit
        SetHoldKey(this, params->hold_key_to_apply, ((params->__inner0 >> 0xf) & 1));

    adjust_motion(this, &motion, &speed, params->hold_key_to_apply);

    // Combat-style guards on RAW (pre-adjust) motion
    if (this->interpreted_state.current_style != 0x8000003d /*NonCombat*/) {
        if (ebp == 0x41000012) return 0x3f;                   // CantCrouchInCombat
        if (ebp == 0x41000013) return 0x40;                   // CantSitInCombat
        if (ebp == 0x41000014) return 0x41;                   // CantSleepInCombat
        if ((ebp & 0x02000000) != 0) return 0x42;             // CantChatEmoteInCombat
    }

    // Action quota check on RAW (pre-adjust) motion
    if ((ebp & 0x10000000) != 0
        && InterpretedMotionState::GetNumActions(&this->interpreted_state) >= 6)
        return 0x45;                                          // TooManyActions

    uint32_t result = DoInterpretedMotion(this, motion /*adjusted*/, &var_2c);

    if (result == 0 && (params->__inner0.byte1 & 0x20) != 0)  // ModifyRawState bit
        RawMotionState::ApplyMotion(&this->raw_state, ebp /*pre-adjust*/, params);

    return result;
}
```

**Per-axis behavior:** DoMotion is a **single-axis** entry. It dispatches
ONE motion (passed as `arg2`) through `adjust_motion` → DoInterpretedMotion.
For multi-axis local input, the caller fires DoMotion three times
(once for forward, once for side, once for turn). Acdream's
`MotionInterpreter.DoMotion` (l.381) is structurally close BUT lacks
the `adjust_motion` call — meaning local `DoMotion(WalkBackward, +1.0)`
never gets sign-flipped to `WalkForward + -0.65`. (See §13 below.)

The **RawState.ApplyMotion uses the PRE-adjust `ebp`**, the
InterpretedState path inside DoInterpretedMotion uses the post-adjust
`motion`. This is what lets the wire carry both "user said walk
backward" (raw) and "physics ran walk forward at -0.65" (interpreted)
on the same packet.

---

## 3. CMotionInterp::adjust_motion (the canonical sign-flipper)

**Function:** `CMotionInterp::adjust_motion`
**Address:** `0x00528010`
**Lines:** 305343–305400.

Already documented in detail in `02-um-handling.md` §10. Repeating the
mappings table here for reference:

| Input | Speed | Output | Speed |
|---|---|---|---|
| WalkForward (0x45000005) | s | WalkForward | s |
| WalkBackward (0x45000006) | s | WalkForward | -0.65 × s |
| SideStepLeft (0x6500000e) | s | SideStepRight | -s |
| ??? (0x65000010) alias | s | SideStepRight | -s |
| SideStepRight (0x6500000f) | s | SideStepRight | 1.248 × s |
| RunForward (0x44000007) | s | RunForward | s (no change here) |

Then if `holdKey == HoldKey_Run`: **`apply_run_to_command(this, &motion, &speed)`** runs.

---

## 4. CMotionInterp::apply_run_to_command (HoldKey rewriter)

**Function:** `CMotionInterp::apply_run_to_command`
**Address:** `0x00527be0`
**Lines:** 305062–305123.

Documented in `02-um-handling.md` §11. Key facts:

- WalkForward + speed > 0 → **promotes to RunForward**, multiplies speed by speedMod (runRate).
- WalkForward + speed ≤ 0 → stays WalkForward, multiplies speed by speedMod (so backward stays backward but at run-pace × 0.65).
- TurnRight → speed *= 1.5 (RunTurnFactor).
- SideStepRight → speed *= speedMod, clamp |result| ≤ 3.0 (MaxSidestepAnimRate).

---

## 5. CMotionInterp::DoInterpretedMotion (the leaf executor)

**Function:** `CMotionInterp::DoInterpretedMotion`
**Address:** `0x00528360`
**Lines:** 305575–305631.

```c
00528360  uint32_t DoInterpretedMotion(this, motion, params)
{
    if (physics_obj == 0) return 8;
    uint32_t result;

    if (contact_allows_move(this, motion) != 0) {
        if (this->standing_longjump != 0
            && (motion == 0x45000005 || motion == 0x44000007 || motion == 0x6500000f))
        {
            // mid-longjump: skip engine-side action, just touch InterpretedState if asked
            if (params->__inner0.byte1 & 0x40 /*ModifyInterpretedState*/)
                InterpretedMotionState::ApplyMotion(&this->interpreted_state, motion, params);
            result = 0;
        }
        else {
            if (motion == 0x40000011 /*Dead*/)
                CPhysicsObj::RemoveLinkAnimations(this->physics_obj);

            // The HOT call: pushes velocity through CPhysicsObj
            result = CPhysicsObj::DoInterpretedMotion(this->physics_obj, motion, params);
            if (result == 0) {
                uint32_t jumpErr;
                if ((params->__inner0 & 0x20000) == 0) {
                    jumpErr = motion_allows_jump(this, motion);
                    if (jumpErr == 0 && (motion & 0x10000000) == 0)
                        jumpErr = motion_allows_jump(this, this->interpreted_state.forward_command);
                } else {
                    jumpErr = 0x48; /*disable*/
                }
                add_to_queue(this, params->context_id, motion, jumpErr);
                if (params->__inner0.byte1 & 0x40)
                    InterpretedMotionState::ApplyMotion(&this->interpreted_state, motion, params);
            }
        }
    }
    else if ((motion & 0x10000000 /*Action*/) == 0) {
        // Airborne but motion isn't an Action — just touch InterpretedState if asked
        if (params->__inner0.byte1 & 0x40)
            InterpretedMotionState::ApplyMotion(&this->interpreted_state, motion, params);
        result = 0;
    }
    else result = 0x24 /*YouCantJumpWhileInTheAir*/;

    if (physics_obj != 0 && physics_obj->cell == 0)
        CPhysicsObj::RemoveLinkAnimations(physics_obj);

    return result;
}
```

**This is the function `apply_interpreted_movement` calls 3× per UM**
(once each for forward/sidestep/turn axes). The actual physics velocity
push is inside `CPhysicsObj::DoInterpretedMotion` — that's where
`set_local_velocity(get_state_velocity(...))` happens, AND that's where
the animation sequencer is driven by `set_motion_table_data`.

**Critical:** when called from `apply_interpreted_movement`, `params`
is a fresh `MovementParameters()` with default flags — and the default
has **`ModifyInterpretedState = 0`**. So per-axis re-application is
purely a PHYSICS push; the state itself stays as `copy_movement_from`
bulk-loaded it.

---

## 6. CMotionInterp::apply_interpreted_movement (THE per-axis dispatcher)

**Function:** `CMotionInterp::apply_interpreted_movement`
**Address:** `0x00528600`
**Lines:** 305713–305788.

This is the heart of the per-axis dispatch. **Every UM that reaches
`move_to_interpreted_state` ends here**, and so does every state-change
that goes through `apply_current_movement` (e.g. landing, leave-ground,
SetWeenieObject, SetPhysicsObject, ReportExhaustion).

```c
00528600  void apply_interpreted_movement(this, int32_t arg2 /*cancelMoveTo*/, int32_t arg3 /*allowJump*/)
{
    if (physics_obj == 0) return;

    MovementParameters var_2c;
    MovementParameters::MovementParameters(&var_2c);          // default flags: NO ModifyState, NO ModifyRaw

    // (1) Cache MyRunRate from forward_speed if we are in run state
    if (this->interpreted_state.forward_command == 0x44000007 /*RunForward*/)
        this->my_run_rate = this->interpreted_state.forward_speed;

    // (2) STYLE axis — re-apply current_style as a motion (combat ↔ peace, etc)
    DoInterpretedMotion(this, this->interpreted_state.current_style, &var_2c);

    // (3) FORWARD axis (with airborne / longjump branches)
    if (contact_allows_move(this, this->interpreted_state.forward_command) == 0)
    {
        // Airborne / dead — force Falling animation
        DoInterpretedMotion(this, 0x40000015 /*Falling*/, &var_2c);
    }
    else if (this->standing_longjump != 0)
    {
        // In a charged longjump — pin forward to Ready, kill any sidestep
        DoInterpretedMotion(this, 0x41000003 /*Ready*/, &var_2c);
        StopInterpretedMotion(this, 0x6500000f /*SideStepRight*/, &var_2c);
    }
    else
    {
        // (3a) FORWARD: dispatch the wire-supplied forward command verbatim
        var_2c.speed = this->interpreted_state.forward_speed;
        DoInterpretedMotion(this, this->interpreted_state.forward_command, &var_2c);

        // (3b) SIDESTEP: dispatch OR explicit-stop
        if (this->interpreted_state.sidestep_command == 0)
            StopInterpretedMotion(this, 0x6500000f /*SideStepRight*/, &var_2c);
        else {
            var_2c.speed = this->interpreted_state.sidestep_speed;
            DoInterpretedMotion(this, this->interpreted_state.sidestep_command, &var_2c);
        }
    }

    // (4) TURN axis: dispatch OR explicit-stop
    uint32_t turn_command = this->interpreted_state.turn_command;
    if (turn_command != 0)
    {
        var_2c.speed = this->interpreted_state.turn_speed;
        DoInterpretedMotion(this, turn_command, &var_2c);
        return;
    }
    // turn_command == 0 → explicit stop on TurnRight, plus a Ready add_to_queue
    uint32_t eax_10 = CPhysicsObj::StopInterpretedMotion(physics_obj, 0x6500000d /*TurnRight*/, &var_2c);
    if (eax_10 == 0) {
        add_to_queue(this, var_c, 0x41000003 /*Ready*/, eax_10);
        if (params.byte1 & 0x40)
            InterpretedMotionState::RemoveMotion(&this->interpreted_state, 0x6500000d);
    }
}
```

### Per-axis dispatch order (CANONICAL)

When a UM with all three axes populated arrives, retail does:

1. `DoInterpretedMotion(current_style, default_params)` — applies
   stance (Combat/Peace/Magic/etc.).
2. **If airborne**: `DoInterpretedMotion(Falling)` — forward axis is
   skipped, body animates as Falling.
3. **Else if longjump**: `DoInterpretedMotion(Ready)` then
   `StopInterpretedMotion(SideStepRight)` — forward pinned, sidestep
   killed.
4. **Else (normal grounded path)**:
   a. `DoInterpretedMotion(forward_command, params{speed=fwd_speed})`
   b. If `sidestep_command == 0`: `StopInterpretedMotion(SideStepRight)`
      Else: `DoInterpretedMotion(sidestep_command, params{speed=side_speed})`
5. If `turn_command != 0`: `DoInterpretedMotion(turn_command, params{speed=turn_speed})` and **RETURN**.
6. If `turn_command == 0`: `CPhysicsObj::StopInterpretedMotion(TurnRight, default_params)` directly on physics_obj (skipping the CMotionInterp wrapper), plus `add_to_queue(Ready)`.

**Note the asymmetry:** the turn-stop bypasses CMotionInterp's
StopInterpretedMotion and goes straight to CPhysicsObj. This is
because the wrapper would re-enter the contact_allows_move check (turns
are always allowed per `contact_allows_move`'s special case), and the
direct call ensures the StopInterpretedMotion fires regardless of state.

---

## 7. CMotionInterp::apply_current_movement (the dispatcher chooser)

**Function:** `CMotionInterp::apply_current_movement`
**Address:** `0x00528870`
**Lines:** 305838–305857.

```c
00528870  void apply_current_movement(this, int32_t arg2, int32_t arg3)
{
    if (physics_obj == 0 || initted == 0) return;

    int32_t isPlayer = (weenie_obj != 0) ? weenie_obj->vtable->IsThePlayer() : 0;

    // Local player + autonomous → use RAW path (so HoldKey gets re-applied via adjust_motion)
    if ((weenie_obj == 0 || isPlayer != 0)
        && CPhysicsObj::movement_is_autonomous(this->physics_obj) != 0)
    {
        apply_raw_movement(this, arg2, arg3);
        return;
    }

    // Everyone else → INTERPRETED path
    apply_interpreted_movement(this, arg2, arg3);
}
```

**This is the bifurcation point**: local autonomous player runs
`apply_raw_movement`; remote observer runs `apply_interpreted_movement`.

Acdream's port (`MotionInterpreter.apply_current_movement`, l.653) is
**a heavily-simplified single-shot** that does NOT branch and does NOT
re-fire per-axis — it just calls `set_local_velocity(get_state_velocity())`
once, gated on `OnWalkable`. **This is the structural divergence.**

---

## 8. CMotionInterp::apply_raw_movement (LOCAL player path)

**Function:** `CMotionInterp::apply_raw_movement`
**Address:** `0x005287e0`
**Lines:** 305817–305834.

```c
005287e0  void apply_raw_movement(this, arg2, arg3)
{
    if (physics_obj == 0) return;

    // Bulk-copy 7 fields from RAW → INTERPRETED
    this->interpreted_state.current_style    = this->raw_state.current_style;
    this->interpreted_state.forward_command  = this->raw_state.forward_command;
    this->interpreted_state.forward_speed    = this->raw_state.forward_speed;
    this->interpreted_state.sidestep_command = this->raw_state.sidestep_command;
    this->interpreted_state.sidestep_speed   = this->raw_state.sidestep_speed;
    this->interpreted_state.turn_command     = this->raw_state.turn_command;
    this->interpreted_state.turn_speed       = this->raw_state.turn_speed;

    // Re-run adjust_motion ONCE PER AXIS (so HoldKey.Run promotes Walk→Run, etc.)
    adjust_motion(this, &interpreted_state.forward_command,
                        &interpreted_state.forward_speed,
                        raw_state.forward_holdkey);
    adjust_motion(this, &interpreted_state.sidestep_command,
                        &interpreted_state.sidestep_speed,
                        raw_state.sidestep_holdkey);
    adjust_motion(this, &interpreted_state.turn_command,
                        &interpreted_state.turn_speed,
                        raw_state.turn_holdkey);

    // Then dispatch each axis through DoInterpretedMotion (per-axis re-fire)
    apply_interpreted_movement(this, arg2, arg3);
}
```

**Crucial:** the local player path **also** ends in
`apply_interpreted_movement`. The only difference is that it first
copies RAW → INTERPRETED and runs `adjust_motion` per axis. The
per-axis dispatcher is the same.

This means there is **exactly ONE per-axis dispatch path** in retail:
`apply_interpreted_movement`. Both local autonomous and remote
observer go through it.

---

## 9. CMotionInterp::move_to_interpreted_state (the UM entry)

Already covered in `02-um-handling.md` §7. Quick recap:

```c
005289c0  int32_t move_to_interpreted_state(this, InterpretedMotionState const* arg2)
{
    if (physics_obj == 0) return 0;

    this->raw_state.current_style = arg2->current_style;          // raw mirrors style
    CPhysicsObj::interrupt_current_movement(physics_obj);

    uint32_t allowJump = motion_allows_jump(this, this->interpreted_state.forward_command);

    InterpretedMotionState::copy_movement_from(&this->interpreted_state, arg2);  // BULK COPY
    apply_current_movement(this, 1 /*cancelMoveTo*/, !!allowJump);                // PER-AXIS REFIRE

    // Then iterate actions[] with stamp-wrap protection
    for each action in arg2->actions where stamp_is_newer:
        DoInterpretedMotion(this, action.motion, params{action_stamp, autonomous});

    return 1;
}
```

**copy_movement_from** is unconditional — overwrites all 7 fields:

```c
InterpretedMotionState::copy_movement_from (lines 293301-293311):
  this->current_style     = src->current_style;
  this->forward_command   = src->forward_command;
  this->forward_speed     = src->forward_speed;
  this->sidestep_command  = src->sidestep_command;
  this->sidestep_speed    = src->sidestep_speed;
  this->turn_command      = src->turn_command;
  this->turn_speed        = src->turn_speed;
```

No diffing. No filter. **If the wire said it, it's in InterpretedState now.**

---

## 10. CMotionInterp::StopInterpretedMotion

**Function:** `CMotionInterp::StopInterpretedMotion`
**Address:** `0x00528470`
**Lines:** 305635–305670.

```c
00528470  uint32_t StopInterpretedMotion(this, motion, params)
{
    if (physics_obj == 0) return 8;

    uint32_t result;

    bool airborne_skip = (contact_allows_move(this, motion) == 0)
                      || (standing_longjump != 0
                          && (motion == 0x45000005 || motion == 0x44000007 || motion == 0x6500000f));

    if (airborne_skip) {
        if (params.byte1 & 0x40 /*ModifyInterpretedState*/)
            InterpretedMotionState::RemoveMotion(&this->interpreted_state, motion);
        result = 0;
    } else {
        result = CPhysicsObj::StopInterpretedMotion(this->physics_obj, motion, params);
        if (result == 0) {
            add_to_queue(this, params->context_id, 0x41000003 /*Ready*/, result);
            if (params.byte1 & 0x40)
                InterpretedMotionState::RemoveMotion(&this->interpreted_state, motion);
        }
    }

    if (physics_obj != 0 && physics_obj->cell == 0)
        CPhysicsObj::RemoveLinkAnimations(physics_obj);

    return result;
}
```

---

## 11. CMotionInterp::StopMotion

**Function:** `CMotionInterp::StopMotion`
**Address:** `0x00528530`
**Lines:** 305674–305708.

```c
00528530  uint32_t StopMotion(this, motion, params)
{
    if (physics_obj == 0) return 8;

    if (params.byte1 & 0x80) CPhysicsObj::interrupt_current_movement(physics_obj);

    // Capture all params fields locally (because adjust_motion will mutate motion+speed)
    float speed = params.speed;
    HoldKey hk  = params.hold_key_to_apply;

    arg2 = motion;
    int32_t var_2c = 0x7c83f8;  // some default flags
    adjust_motion(this, &arg2, &speed, hk);                       // sign-flip + run-promote

    uint32_t result = StopInterpretedMotion(this, arg2, &var_2c);

    if (result == 0 && (params.byte1 & 0x20 /*ModifyRawState*/) != 0)
        RawMotionState::RemoveMotion(&this->raw_state, motion);   // PRE-adjust motion

    return result;
}
```

Same pattern as DoMotion: adjust_motion the motion+speed, dispatch the
adjusted form to StopInterpretedMotion, then optionally update RawState
with the PRE-adjust form.

---

## 12. CMotionInterp::StopCompletely

**Function:** `CMotionInterp::StopCompletely`
**Address:** `0x00527e40`
**Lines:** 305208–305234.

```c
00527e40  uint32_t StopCompletely(this)
{
    if (physics_obj == 0) return 8;

    CPhysicsObj::interrupt_current_movement(physics_obj);
    uint32_t allowJump = motion_allows_jump(this, this->interpreted_state.forward_command);

    // Reset RAW (5 fields)
    this->raw_state.forward_command  = 0x41000003 /*Ready*/;
    this->raw_state.forward_speed    = 1.0f;
    this->raw_state.sidestep_command = 0;
    this->raw_state.turn_command     = 0;

    // Reset INTERPRETED (5 fields — note: side_speed and turn_speed NOT reset here!)
    this->interpreted_state.forward_command  = 0x41000003;
    this->interpreted_state.forward_speed    = 1.0f;
    this->interpreted_state.sidestep_command = 0;
    this->interpreted_state.turn_command     = 0;

    CPhysicsObj::StopCompletely_Internal(this->physics_obj);
    add_to_queue(this, 0, 0x41000003 /*Ready*/, allowJump);

    if (physics_obj != 0 && physics_obj->cell == 0)
        CPhysicsObj::RemoveLinkAnimations(physics_obj);

    return 0;
}
```

**Note:** `sidestep_speed` and `turn_speed` are NOT reset to 1.0f
(neither in raw nor interpreted). Only the commands and forward_speed
are touched. Acdream's port (l.510) goes further and resets all six
speeds — slightly more aggressive than retail but harmless because
the speeds are only consumed when the matching command is non-zero.

---

## 13. CMotionInterp::get_max_speed and get_adjusted_max_speed

**Function:** `CMotionInterp::get_max_speed`
**Address:** `0x00527cb0`
**Lines:** 305127–305141.

```c
00527cb0  void get_max_speed(this)
{
    CMotionInterp* this_1 = this;       // probably an out-param
    CWeenieObject* weenie = this->weenie_obj;
    this_1 = nullptr;

    if (weenie == 0) return;
    if (weenie->vtable->InqRunRate(&this_1) != 0) return;
    this->my_run_rate;                  // (compiler artifact — load only)
}
```

The decompile is hard to read because of x87 return-value handling, but
semantically: **return InqRunRate(weenie) if available, else my_run_rate**.
Used by `set_target_movement` (`0x00509ed5` references in 352395, 353112).

**Function:** `CMotionInterp::get_adjusted_max_speed`
**Address:** `0x00527d00`
**Lines:** 305145–305156.

```c
00527d00  void get_adjusted_max_speed(this)
{
    CWeenieObject* weenie = this->weenie_obj;
    if (weenie != 0 && weenie->vtable->InqRunRate(&this_1) == 0)
        this->my_run_rate;              // load
    if (this->interpreted_state.forward_command == 0x44000007 /*RunForward*/)
        ((long double)this->interpreted_state.forward_speed) / ((long double)this->current_speed_factor);
}
```

Adjusts the cap by the **current_speed_factor** (an internal multiplier
e.g. for encumbrance / spell effects) when the body is in run state.

Both functions return a float used as the speed cap inside
`get_state_velocity` (305160) — `len > 4.0L * rate` clamp scale.

---

## 14. InterpretedMotionState::UnPack — flag bits in detail

(Documented in `02-um-handling.md` §5; restating the bit table here
since this doc is the dispatch reference.)

| Bit | Field | When CLEAR |
|---|---|---|
| 0x01 | `current_style` | NonCombat (0x8000003d) |
| 0x02 | `forward_command` | Ready (0x41000003) |
| 0x04 | `forward_speed` | 1.0f |
| 0x08 | `sidestep_command` | 0 |
| 0x10 | `sidestep_speed` | 1.0f |
| 0x20 | `turn_command` | 0 |
| 0x40 | `turn_speed` | 1.0f |
| 0x80–0x800 | action count (5 bits) | 0 |

**Default-on-absence is the per-axis SEMANTIC that drives the stop
signal.** When ACE relays a stop, it omits the forward_command field
(bit 0x02 clear), and UnPack sets `forward_command = Ready`. The
copy_movement_from then writes Ready into InterpretedState. The next
`apply_interpreted_movement` calls `DoInterpretedMotion(Ready,
speed=1.0)`. `get_state_velocity` returns (0,0,0) because Ready is
neither WalkForward nor RunForward.

---

## 15. The complete dispatch sequence for an inbound UM (annotated)

For a UM with all three axes populated (forward + sidestep + turn) on
a remote observer, **the full call chain** is:

```
0xF74C wire packet
  CM_Physics::DispatchSB_*                         (357214)
    CPhysics::SetObjectMovement                     (271370)
      CPhysicsObj::unpack_movement                  (280179)
        MovementManager::unpack_movement            (300563)
          // read 2 bytes movement_type=0
          // read 2 bytes initial_style
          if (style != current) DoMotion(style)     (306159) <- one DoMotion for STANCE
          InterpretedMotionState::UnPack            (294360)
              // read flags uint32 + each conditional field
          MovementManager::move_to_interpreted_state(300259)
            CMotionInterp::move_to_interpreted_state(305936)
              raw_state.current_style = src.current_style
              interrupt_current_movement
              motion_allows_jump(prev forward_command)
              InterpretedMotionState::copy_movement_from        // BULK COPY 7 FIELDS
              apply_current_movement(cancelMoveTo=1, allowJump) (305838)
                if (local autonomous):
                  apply_raw_movement                            (305817)
                    bulk-copy raw → interpreted
                    adjust_motion × 3 (forward, sidestep, turn)
                    apply_interpreted_movement                  (305713)
                else:  // remote observer
                  apply_interpreted_movement                    (305713)
                    cache MyRunRate if RunForward
                    DoInterpretedMotion(current_style)          // STYLE
                      CPhysicsObj::DoInterpretedMotion          (varies)
                    if !contact_allows_move:
                      DoInterpretedMotion(Falling)              // FORWARD branch (airborne)
                    elif standing_longjump:
                      DoInterpretedMotion(Ready)
                      StopInterpretedMotion(SideStepRight)
                    else:
                      DoInterpretedMotion(forward_command)      // FORWARD axis
                        CPhysicsObj::DoInterpretedMotion
                          set_local_velocity(get_state_velocity)
                            // VELOCITY PUSH FOR FORWARD
                      if (sidestep_command == 0):
                        StopInterpretedMotion(SideStepRight)
                      else:
                        DoInterpretedMotion(sidestep_command)   // SIDESTEP axis
                          CPhysicsObj::DoInterpretedMotion
                            set_local_velocity(get_state_velocity)
                              // VELOCITY PUSH FOR SIDESTEP (additive over forward)
                    if (turn_command != 0):
                      DoInterpretedMotion(turn_command)         // TURN axis
                        CPhysicsObj::DoInterpretedMotion
                          (drives angular velocity / animation omega)
                    else:
                      CPhysicsObj::StopInterpretedMotion(TurnRight)
                      add_to_queue(Ready)
              // Then for each action in src.actions:
              for each action with newer stamp:
                DoInterpretedMotion(action.motion, params{action_stamp, autonomous})
                  // OVERLAY axis (Twitch / ChatEmote / etc — not a velocity push)
```

**Five DoInterpretedMotion calls per UM in the typical case (style +
forward + sidestep + turn + maybe one action).** Each call into
`CPhysicsObj::DoInterpretedMotion` pushes velocity via
`set_local_velocity(get_state_velocity())`.

The fact that the velocity is pushed **MULTIPLE TIMES PER UM** (once
after each axis) is what makes the body's local velocity in retail
respond instantly to a multi-axis state change. Acdream's
`apply_current_movement` only pushes velocity once.

---

## 16. Overlay (Action / ChatEmote / Modifier) dispatch

Action commands (bit 0x10000000) and ChatEmote commands (bit
0x02000000) are overlay motions. They flow through:

- **Inbound UM**: the `actions[]` list inside `InterpretedMotionState`
  (5-bit action count starting at flag 0x80). Each action is
  dispatched in `move_to_interpreted_state`'s for-loop via
  `DoInterpretedMotion(action.motion, params{action_stamp, autonomous})`.
- **Locally generated**: `DoMotion(motion, params)` with the action
  bit set. `DoMotion`'s combat-style guards reject ChatEmote in
  combat (`return 0x42`). Actions over the 6-action quota return
  0x45.

Within `DoInterpretedMotion`, an Action command **does** call
`CPhysicsObj::DoInterpretedMotion` (which routes the command to the
animation sequencer's overlay channel) but `get_state_velocity` does
NOT consume Action commands — they have no velocity contribution.
This is what makes "swing weapon while running" work: the forward
axis still produces RunForward velocity (because `forward_command`
in InterpretedState is still RunForward), and the action just plays
on the overlay channel.

**Acdream's `AnimationCommandRouter.RouteFullCommand`** is the analog
of the overlay-channel routing inside `CPhysicsObj::DoInterpretedMotion`.
Note that acdream's GameWindow (l.2887–2892) calls RouteFullCommand
**only** when `forwardIsOverlay` and `!remoteIsAirborne`. This ONLY
handles the "first-class" overlay flag in the wire (the special-case
where the wire sets `forward_command` to an Action command directly,
which retail handles via `move_to_interpreted_state`'s bulk copy
followed by `apply_interpreted_movement`'s forward-axis dispatch).

---

## 17. Cross-check: acdream `MotionInterpreter.cs` divergences

| Retail | Acdream (`MotionInterpreter.cs`) | Verdict |
|---|---|---|
| `apply_interpreted_movement` (305713) — fires DoInterpretedMotion for STYLE + FORWARD + SIDESTEP + TURN | **MISSING.** `apply_current_movement` (l.653) just calls `set_local_velocity(get_state_velocity())` once, gated on OnWalkable. | **CRITICAL GAP** |
| `apply_raw_movement` (305817) — bulk-copy RAW→INTERPRETED, then adjust_motion × 3, then apply_interpreted_movement | **MISSING.** Acdream's local autonomous player path is improvised in `PlayerMovementController` — does not run adjust_motion per axis. | Bug for local-input WalkBackward + HoldKey.Run interaction |
| `DoMotion` (306159) — adjust_motion → DoInterpretedMotion → optional RawState.ApplyMotion | `DoMotion` (l.381) just sets RawState.ForwardCommand+Speed and forwards to `DoInterpretedMotion(modifyInterpretedState:true)`. **No adjust_motion call.** | Bug for local backward/sidestep input |
| `move_to_interpreted_state` (305936) — bulk-copy InterpretedState (7 fields), apply_current_movement, iterate actions | **PARTIAL.** GameWindow.cs:2847 sets only ForwardCommand+ForwardSpeed (NOT current_style, NOT all 7 fields). Sidestep/Turn handled via separate `DoInterpretedMotion`/`StopInterpretedMotion` calls (l.3060, 3066, 3096, 3108). | Functional but structurally divergent |
| `StopMotion` (305674) — adjust_motion the motion, StopInterpretedMotion(adjusted), optional RawMotionState::RemoveMotion(pre-adjust) | `StopMotion` (l.431) — early-rewrites RawState, then forwards. **No adjust_motion call.** | Functional for sign-aligned cases only |
| `apply_run_to_command` (305062) — speed > 0 gate for promote, multiplies speed by speedMod | `MotionInterpreter` doesn't have this method. Wire-arrival gives the post-adjust form so it's not strictly needed for L.3 receive path; but the local SEND path is missing it. | Required for outbound bug parity |
| `ApplyMotion` (293531) — switch by command class (TurnRight / SideStepRight / 0x4xxxxxxx) | `ApplyMotionToInterpretedState` (l.993) — `switch` over **specific commands** (Walk/Run/WalkBackward/SideStepRight/SideStepLeft/TurnRight/TurnLeft/Ready). Other 0x4xxxxxxx commands (Stand, Falling, Crouch, Sit, Sleep) **fall through silently.** | Bug for non-locomotion forward commands |

---

## 18. Answers to the critical questions

### Q1: Does retail call DoInterpretedMotion separately for forward, sidestep, and turn axes EACH UM AND EACH TICK?

**EACH UM: YES.** `apply_interpreted_movement` (305713) makes 3–5
DoInterpretedMotion calls in sequence: style, forward (or Falling /
Ready+SideStop for longjump), sidestep (or stop), turn (or stop).

**EACH TICK: NO.** `apply_interpreted_movement` does NOT auto-fire on
every physics tick. It fires:
- On every UM intake (via `move_to_interpreted_state` → `apply_current_movement`).
- On `LeaveGround` / `LandingHandler` / `enter_default_state` / `SetWeenieObject` / `SetPhysicsObject` / `ReportExhaustion`.
- On `set_target_movement` and `cancel_moveto`.

Per-tick, the body's velocity is preserved by the physics solver
(`PhysicsBody::update`) using the velocity that was last set via
`set_local_velocity` from the most recent `apply_interpreted_movement`.
**The body integrates with the SAME velocity until a new state event
fires another `apply_interpreted_movement`.**

This is why retail does NOT have a "staircase" issue on slopes: the
last `set_local_velocity` from the last UM stays in effect, and the
collision sweep (`ResolveWithTransition`) handles the slope component
naturally. Acdream's env-var path bypassed this by skipping the
`set_local_velocity` re-push at UM time.

### Q2: When UM arrives with all 3 axes populated, does retail dispatch all 3? In what order?

**Yes.** Order is fixed in `apply_interpreted_movement`:

1. STYLE (`current_style` — stance change)
2. FORWARD (or Falling / Ready+SideStop)
3. SIDESTEP (or explicit Stop)
4. TURN (or explicit Stop)

Then iterate `actions[]` for overlays in stamp order.

### Q3: What's the canonical "play this cycle now" decision tree based on InterpretedState?

This is **NOT in CMotionInterp**. The cycle decision lives inside
`CPhysicsObj::DoInterpretedMotion` → `set_motion_table_data`. Each call
to `DoInterpretedMotion(motion, params)` from
`apply_interpreted_movement` ends in a `set_motion_table_data(motion,
speed)` that updates the corresponding cycle slot in the animation
sequencer.

**The sequencer maintains one cycle per "axis class"** roughly:

- Forward locomotion cycle: WalkForward / RunForward / Falling / Ready
- Sidestep cycle: SideStepRight (with sign-flip for left)
- Turn cycle: TurnRight (with sign-flip for left)
- Style: stance-class affects which (style, command) pair the motion
  table looks up

Multiple cycles play SIMULTANEOUSLY on different bone subsets, layered
by the motion table's part definitions. This is why "run forward AND
sidestep" produces a strafe-run animation: both cycles play, the parts
each owns are updated by their own cycle.

The "priority" acdream's `OnLiveMotionUpdated` uses (l.2905-2939:
forward → sidestep → turn → ready, single SetCycle) is **a
simplification** that picks ONE cycle. Retail plays multiple in
parallel via the motion-table layering.

### Q4: For overlay (Action / Modifier / ChatEmote) packets, does the dispatch chain differ?

**Slightly.** Overlays (Action bit 0x10000000, ChatEmote bit 0x02000000)
ride in the `actions[]` list inside InterpretedMotionState. After
`copy_movement_from` and `apply_interpreted_movement`, the action
loop iterates with stamp-wrap protection and fires
`DoInterpretedMotion(action.motion, params{action_stamp, autonomous_bit_via_0x1000})`.

Inside `DoInterpretedMotion`:
- `contact_allows_move` for Action commands always returns 1 (handled
  by `(motion & 0x10000000) == 0` check in the false-branch — Action
  airborne returns YouCantJumpWhileInTheAir).
- Action commands have no `get_state_velocity` contribution (the
  switch in `get_state_velocity` only matches WalkForward / RunForward
  / SideStepRight as side-step gate).

Net effect: Action overlay does NOT change body velocity (forward axis
keeps producing whatever it was producing), but it DOES drive an
animation overlay channel.

**HOWEVER**: when a UM's `forward_command` IS an Action (e.g. retail
sometimes encodes "swing this attack and stop running" by setting
forward_command = AttackHigh1 directly, bit 0x02 set, no separate
action entry), the bulk-copy lands AttackHigh1 in
`interpreted_state.forward_command`. `apply_interpreted_movement`
fires `DoInterpretedMotion(AttackHigh1, params{speed=fwd_speed})`,
and `get_state_velocity` returns 0 (no match for WalkForward /
RunForward) — body stops moving forward, attack animation plays
on the Action channel via DoInterpretedMotion's set_motion_table_data.

This is why GameWindow.cs:2802-2855 (acdream's "lifted bulk-copy"
block) is correctly doing `InterpretedState.ForwardCommand = fullMotion`
unconditionally — to match retail's bulk-copy semantics.

---

## 19. What would the "correct" UM handler do that acdream is missing?

Given an inbound 0xF74C UM with movement_type=0 (RawCommand), retail's
end-to-end flow is:

```
1. STALENESS — check 16-bit instance_ts via SetObjectMovement;
   drop if older.
2. STYLE PRE-DISPATCH — if (new_style != current), DoMotion(new_style)
   to swap stance.
3. UNPACK — InterpretedMotionState::UnPack reads flags + each
   conditional field.
4. BULK COPY — copy_movement_from writes all 7 fields into the body's
   InterpretedState.
5. PER-AXIS RE-FIRE — apply_interpreted_movement runs:
     a. DoInterpretedMotion(current_style)
     b. DoInterpretedMotion(forward_command, params{speed=fwd_speed})  OR  Falling-branch
     c. DoInterpretedMotion(sidestep_command, params{speed=side_speed})  OR  StopInterpretedMotion(SideStepRight)
     d. DoInterpretedMotion(turn_command, params{speed=turn_speed})  OR  StopInterpretedMotion(TurnRight) + Ready add_to_queue
   Each of these fires CPhysicsObj::DoInterpretedMotion, which calls
   set_local_velocity(get_state_velocity()) AND drives the animation
   sequencer's per-axis cycle slot.
6. ACTIONS LOOP — iterate actions[] with stamp-wrap protection;
   DoInterpretedMotion each newer action.
```

**Acdream is missing #5 (the per-axis re-fire) at the granularity
retail has.**

What acdream does on UM (`OnLiveMotionUpdated` + `MotionInterpreter.apply_current_movement`):

- ✅ Staleness (timestamp check in WorldSession parser).
- ✅ Style change (stance update).
- ✅ Unpack (parser handles flag bits).
- ✅ Bulk-copy ForwardCommand+ForwardSpeed (l.2847, 2855). **PARTIAL** —
  doesn't bulk-copy current_style separately (relies on `Stance`
  being lifted up by parser).
- ❌ Per-axis re-fire of `apply_interpreted_movement`. **MISSING.**
  Instead:
  - Sidestep: directly calls `DoInterpretedMotion(sideFull, sideSpd, modifyInterpretedState:true)` (l.3060).
  - Turn: directly calls `DoInterpretedMotion(turnFull, turnSpd, modifyInterpretedState:true)` (l.3096).
  - These each fire `MotionInterpreter.apply_current_movement` once, which only does ONE `set_local_velocity` call.
- ❌ Single `set_local_velocity` per UM rather than per-axis. **CRITICAL.**
  Effect: after-Forward velocity overwrites after-Sidestep velocity
  rather than building up. (Mitigated by `get_state_velocity` reading
  ALL three axis fields in one call — so the final velocity is correct.
  But the per-axis sequencer cycle slots may not all update because
  `MotionInterpreter.apply_current_movement` doesn't drive the sequencer.)
- ❌ `apply_interpreted_movement`'s STYLE pre-fire. **MISSING.** Stance
  changes don't get a DoInterpretedMotion call — this is why "draw
  weapon while running" sometimes shows wrong stance pose.
- ❌ `apply_interpreted_movement`'s longjump branch. **MISSING.**
  StandingLongJump-charged forward gets pinned to Ready in retail; in
  acdream the forward command is whatever the wire said.
- ❌ `apply_interpreted_movement`'s explicit StopInterpretedMotion for
  zero-axis. **PARTIAL.** Acdream's GameWindow.cs:3066-3070 calls
  `StopInterpretedMotion(SideStepRight)` AND `StopInterpretedMotion(SideStepLeft)`
  when sidestep is 0; same for turn at 3108-3111. Retail only stops
  SideStepRight and TurnRight (relies on adjust_motion having
  normalized Left → Right + sign).
- ❌ Falling-when-airborne fallback. **WORKAROUND.** acdream handles
  this via `remoteIsAirborne` checks scattered through
  `OnLiveMotionUpdated`, NOT in `apply_current_movement`.

### Concrete fix proposal

Port `apply_interpreted_movement` faithfully into MotionInterpreter.cs
and have `apply_current_movement` (l.653) **delegate** to it for the
remote-observer path. The single `set_local_velocity` call should
move INSIDE `CPhysicsObj.DoInterpretedMotion` (or a new method
`MotionInterpreter.DispatchAxis(motion, speed)`) so each axis call
both updates InterpretedState AND pushes velocity AND drives the
sequencer.

Then `OnLiveMotionUpdated` becomes much shorter:

```
OnLiveMotionUpdated:
  remoteMot.Motion.move_to_interpreted_state(wireInterpretedState)
  // That single call does bulk-copy + per-axis dispatch + sequencer drive
```

This eliminates the divergence between "what retail does on UM" and
"what acdream does on UM" and removes the need for the heavy ad-hoc
logic at GameWindow.cs:2774–3300.

---

## 20. Cross-references

- Retail decomp source: `docs/research/named-retail/acclient_2013_pseudo_c.txt`
- Companion docs:
  - `02-um-handling.md` — UM intake top-level chain
  - `03-up-routing.md` — UpdatePosition (0xF748) — separate path
  - `04-interp-manager.md` — MovementManager + MoveToManager
  - `06-acdream-audit.md` — full acdream audit
- Acdream code:
  - `src/AcDream.Core/Physics/MotionInterpreter.cs`
  - `src/AcDream.App/Rendering/GameWindow.cs::OnLiveMotionUpdated` (l.2579-3300)
  - `src/AcDream.Core/Physics/AnimationCommandRouter.cs` (overlay channel routing)
- Retail named symbols (all addresses 0x005xxxxx in acclient.exe v11.4186):
  - `CMotionInterp::PerformMovement` 0x00528e80 (306221)
  - `CMotionInterp::DoMotion` 0x00528d20 (306159)
  - `CMotionInterp::DoInterpretedMotion` 0x00528360 (305575)
  - `CMotionInterp::StopMotion` 0x00528530 (305674)
  - `CMotionInterp::StopInterpretedMotion` 0x00528470 (305635)
  - `CMotionInterp::StopCompletely` 0x00527e40 (305208)
  - `CMotionInterp::apply_current_movement` 0x00528870 (305838)
  - `CMotionInterp::apply_interpreted_movement` 0x00528600 (305713)
  - `CMotionInterp::apply_raw_movement` 0x005287e0 (305817)
  - `CMotionInterp::move_to_interpreted_state` 0x005289c0 (305936)
  - `CMotionInterp::adjust_motion` 0x00528010 (305343)
  - `CMotionInterp::apply_run_to_command` 0x00527be0 (305062)
  - `CMotionInterp::get_state_velocity` 0x00527d50 (305160)
  - `CMotionInterp::get_max_speed` 0x00527cb0 (305127)
  - `CMotionInterp::get_adjusted_max_speed` 0x00527d00 (305145)
  - `CMotionInterp::contact_allows_move` 0x00528240 (305471)
  - `CMotionInterp::enter_default_state` 0x00528c80 (306124)
  - `CMotionInterp::HandleExitWorld` 0x00527f30 (305275)
  - `MovementManager::PerformMovement` 0x005240d0 (300194)
  - `MovementManager::unpack_movement` 0x00524440 (300563)
  - `MovementManager::move_to_interpreted_state` 0x00524170 (300259)
  - `InterpretedMotionState::UnPack` 0x0051f400 (294360)
  - `InterpretedMotionState::ApplyMotion` 0x0051ea40 (293531)
  - `InterpretedMotionState::copy_movement_from` (293301-293311)
