# 13 — Retail's cycle decision tree **Question**: When `InterpretedMotionState` has simultaneous `forward_command=RunForward` + `sidestep_command=SidestepRight` + `turn_command=TurnLeft`, **what cycle plays in retail?** **Answer (TL;DR)**: All three. Retail does **not** pick "the winning substate" out of a 3-axis state. Instead, `apply_interpreted_movement` issues **three separate `DoInterpretedMotion` calls** — forward-cmd, then sidestep-cmd, then turn-cmd — each landing in `CMotionTable::GetObjectSequence`, which dispatches by command **class bits** (0x40000000 substate / 0x10000000 action / 0x20000000 modifier) to **either replace the substate or attach a modifier**. Forward goes into the substate slot; sidestep+turn go into the modifier list. The `CSequence` is rebuilt with all three layers via `add_motion`. This means **acdream's "priority winner" picker is wrong** — and so is the `RunForward → WalkForward → Ready` fallback chain. Retail has no fallback chain; it just calls `GetObjectSequence` per axis and ignores NULL results. --- ## A. Top of the call tree — `CMotionInterp::apply_interpreted_movement` Line **305713–305788** (`acclient_2013_pseudo_c.txt`), address `00528600`. ```c void CMotionInterp::apply_interpreted_movement(this, arg2, arg3) { if (!physics_obj) return; MovementParameters var_2c; // 305719 MovementParameters::MovementParameters(&var_2c); // Sync run-rate from forward_speed if running if (interpreted_state.forward_command == 0x44000007 /*RunForward*/) my_run_rate = (float)interpreted_state.forward_speed; // 305722 // 1) Always re-issue current_style (e.g. CombatMode_NonCombat) DoInterpretedMotion(this, interpreted_state.current_style, &var_2c); // 305724 // 2) Forward axis if (!contact_allows_move(this, interpreted_state.forward_command)) { var_18_2 = 0x3f800000; // 1.0f speed DoInterpretedMotion(this, 0x40000015 /*Stand*/, &var_2c); // 305729 } else if (standing_longjump) { DoInterpretedMotion(this, 0x41000003 /*Ready*/, &var_2c); // 305738 StopInterpretedMotion(this, 0x6500000f /*Sidestep*/, &var_2c); } else { DoInterpretedMotion(this, interpreted_state.forward_command, &var_2c); // 305744 // 3) Sidestep axis if (interpreted_state.sidestep_command == 0) StopInterpretedMotion(this, 0x6500000f /*Sidestep*/, &var_2c); // 305748 else DoInterpretedMotion(this, interpreted_state.sidestep_command, &var_2c); // 305752 } // 4) Turn axis if (interpreted_state.turn_command != 0) DoInterpretedMotion(this, interpreted_state.turn_command, &var_2c); // 305762 else // No turn — explicitly stop any prior TurnLeft modifier StopInterpretedMotion(this, 0x6500000d /*TurnLeft*/, &var_2c); // implied 305770 } ``` **Critical observation**: this is **four `DoInterpretedMotion` calls per UM** (current_style, forward, sidestep, turn). Each one is a distinct `MotionTableManager::PerformMovement` → `CMotionTable::DoObjectMotion` → `CMotionTable::GetObjectSequence` round-trip. The composite cycle is the sum of four state-machine transitions, not the result of a priority pick. `apply_interpreted_movement` is invoked by `apply_current_movement` (305838–305857). `apply_current_movement` is called by every UM arrival on the player or remote object after `RawMotionState::ApplyMotion` (or `InterpretedMotionState::ApplyMotion`) populates the state struct. --- ## B. The state-routing dispatcher — `RawMotionState::ApplyMotion` Line **293630–293703**, address `0051eb60`. This is the **retail analog of acdream's `AnimationCommandRouter.Classify`**. ```c void RawMotionState::ApplyMotion(this, arg2, arg3) { // arg2 = motion command (e.g. 0x44000007 RunForward) // arg3 = MovementParameters (speed, hold_key_to_apply, etc.) if ((arg2 - 0x6500000d) > 3) { // not Turn/Sidestep range if ((arg2 & 0x40000000) == 0) { // not substate if (arg2 >= 0) { if ((arg2 & 0x10000000) != 0) // ACTION class AddAction(this, arg2, ...); // 293640 → action queue } else if (current_style != arg2) { // STYLE change forward_command = 0x41000003; // Ready current_style = arg2; // 293645 } } else if (arg2 != 0x44000007 /*RunForward*/) { // 0x40000000-class but NOT RunForward (i.e. WalkForward, // BackForward etc) goes into FORWARD slot forward_command = arg2; // 293650 forward_holdkey = arg3->hold_key_to_apply; forward_speed = arg3->speed; } return; } switch (arg2) { // 293666 case 0x6500000d /*TurnLeft*/: case 0x6500000e /*TurnRight*/: turn_command = arg2; // 293671 turn_holdkey = arg3->hold_key_to_apply; turn_speed = arg3->speed; return; case 0x6500000f /*SidestepRight*/: case 0x65000010 /*SidestepLeft*/: sidestep_command = arg2; // 293688 sidestep_holdkey = arg3->hold_key_to_apply; sidestep_speed = arg3->speed; return; } } ``` **Routing classes** (matches `0x40000000`/`0x10000000`/`0x20000000` mask checks, also visible in `CMotionTable::GetObjectSequence`): | Class bit | Range | Slot | Effect | |---|---|---|---| | `0x40000000` | substate (e.g. `0x44000007` RunForward, `0x40000015` Stand) | `forward_command` (or replaces substate) | replaces previous substate; modifiers may be cleared | | `0x10000000` | action (e.g. emote) | `action_head` queue | overlay; substate cycle keeps running | | `0x20000000` | modifier | `modifier_head` list | overlay; substate cycle keeps running | | `0x6500000d-10` | turn/sidestep (special-cased) | `turn_command` / `sidestep_command` | dedicated slots (effectively modifiers) | | `< 0` (`0x80...`) | style change | `current_style` | full reset, forward_command → Ready | | `0x44000007` | **RunForward** is special-cased OUT of the forward slot here — see below | — | not stored in `forward_command` directly by `RawMotionState`; it's the result of `adjust_motion` running on `WalkForward + HoldKey.Run` | (The InterpretedMotionState equivalent at line 293531 is functionally the same with one extra branch — `current_style` initialization.) --- ## C. `adjust_motion` — the `WalkForward + Run` → `RunForward` transform Line **305343–305400**, address `00528010`. This is what `DoMotion` calls before `DoInterpretedMotion` to translate a raw key event into a substate. ```c void CMotionInterp::adjust_motion(this, arg2 /*&cmd*/, arg3 /*&speed*/, arg4 /*hold_key*/) { if (weenie_obj == 0 || weenie_obj->IsCreature()) { switch (*arg2) { case 0x65000010 /*SidestepLeft*/: *arg2 = 0x6500000f; // collapse Left → Right *arg3 *= -1; // with negative speed // fallthrough case 0x6500000f /*SidestepRight*/: // Sidestep speed-mod: (3.12/1.25) * 0.5 = 1.248 *arg3 = (3.12f / 1.25f) * 0.5f * (*arg3); break; case 0x6500000e /*TurnRight*/: *arg2 = 0x6500000d; // collapse Right → Left *arg3 *= -1; // with negative speed break; case 0x45000006 /*WalkBackward*/: *arg2 = 0x45000005; // collapse to BackForward *arg3 = -0.65f * (*arg3); break; case 0x44000007 /*RunForward*/: // already a run cmd — fall through to apply_run_to_command break; } // Then: if hold_key == HoldKey_Run, escalate to RunForward HoldKey current = arg4 == HoldKey_Invalid ? raw_state.current_holdkey : arg4; if (current == HoldKey_Run) apply_run_to_command(this, arg2, arg3); } } ``` `apply_run_to_command` (line 305062, addr `00527be0`): ```c void CMotionInterp::apply_run_to_command(this, arg2, arg3) { float run_rate = weenie_obj ? weenie_obj->InqRunRate() : my_run_rate; if (*arg2 == 0x45000005 /*WalkForward*/) { if (*arg3 != 0) *arg2 = 0x44000007; // → RunForward *arg3 *= run_rate; // speed *= runRate (e.g. 2.94) } else if (*arg2 == 0x6500000d /*TurnLeft*/) { *arg3 *= 1.5f; // turn 1.5x while running } else if (*arg2 == 0x6500000f /*SidestepRight*/) { *arg3 *= run_rate; // clamp to ±3 m/s if (fabs(*arg3) > 3.0f) *arg3 = (sign(*arg3)) * 3.0f; } } ``` So **the way `RunForward` gets into `forward_command` in retail is**: 1. Wire UM has `cmd=WalkForward (0x45000005)` + `hold_key=HoldKey_Run` 2. `DoMotion(0x45000005, params)` is called. 3. `adjust_motion` swaps `cmd → 0x44000007 RunForward`, `speed *= runRate`. 4. `RawMotionState::ApplyMotion(0x44000007, ...)` runs. The special-case `arg2 != 0x44000007` branch at line 293648 means RunForward is **NOT** stored in `forward_command` here. (This appears intentional — `RunForward` is the post-`adjust_motion` form; the persistent `RawMotionState` keeps the original WalkForward.) 5. **InterpretedMotionState** stores the post-adjust value because `apply_raw_movement` (305817) copies `raw_state.*` then runs `adjust_motion` over each of the three axes (305829-305831) before `apply_interpreted_movement` consumes it. ACE matches this: it auto-upgrades `WalkForward + HoldKey.Run` → `RunForward` on the **outbound** wire to remote observers, which is why our inbound parser sees `fwd=0x07` for "remote is running." --- ## D. The cycle-decision core — `CMotionTable::GetObjectSequence` Line **298636–298950**, address `00522860`. This is where a single motion command lands and the `CSequence` is rebuilt. It is invoked once per `DoInterpretedMotion` call. Signature: ```c int CMotionTable::GetObjectSequence( this, uint32_t motion, // arg2 — the command MotionState* state, // arg3 — table-internal state CSequence* sequence, // arg4 — the part-array sequence to mutate float speed_mod, // arg5 uint32_t* num_anims_out, // arg6 int32_t force_flag); // arg7 — re-modify recursion guard ``` **Three dispatch branches based on the high-bit class of `motion`**: ### D.1 — `motion < 0` (style change, e.g. `0x80000003D`) Lines 298661–298735. Substate's effect: reset to default substate of the new style, optionally clear modifiers, replace cycles. ### D.2 — `motion & 0x40000000` (substate) Lines 298737–298848. The **forward-axis path**. ```c if ((motion & 0x40000000) != 0) { // 298737 uint32_t key = (motion & 0xffffff); MotionData* incoming = LongHash::lookup(&this->cycles, (state->style << 0x10) | key); if (incoming == 0) incoming = LongHash::lookup(&this->cycles, (this->default_style << 0x10) | key); // fallback to default style if (incoming != 0 && is_allowed(this, motion, incoming, state)) { // Same-cycle re-speed shortcut: we're already on this cycle // and just changing speed (e.g. forward_speed delta) if (motion == state->substate && same_sign(speed_mod, state->substate_mod) && sequence->has_anims()) { change_cycle_speed(sequence, incoming, state->substate_mod, speed_mod); subtract_motion(sequence, incoming, state->substate_mod); combine_motion(sequence, incoming, speed_mod); state->substate_mod = speed_mod; return 1; } // Full transition: clear-anims + (link from current substate) + (incoming) if (incoming->bitfield & 1) state->clear_modifiers(); // some cycles clear modifiers on entry MotionData* link = get_link(this, state->style, state->substate, state->substate_mod, motion, speed_mod); // (with two-stage fallback through default_substate if direct link missing) sequence->clear_physics(); sequence->remove_cyclic_anims(); // If no direct link, route through default substate add_motion(sequence, link, ...); // transition anim add_motion(sequence, incoming, speed_mod); // new cycle // Re-add prior substate as a modifier if it had the 0x20000000 flag if (state->substate != motion && (state->substate & 0x20000000)) state->add_modifier_no_check(state->substate, state->substate_mod); state->substate_mod = speed_mod; state->substate = motion; re_modify(this, sequence, state); // re-attach all modifiers return 1; } } ``` **Key takeaway**: if the cycle-bound lookup `LongHash::lookup(&cycles, (style<<16)|key)` returns NULL **and** the default-style fallback also returns NULL, retail returns 0 (failure) and the call has **no effect**. There is **no `RunForward → WalkForward → Ready` fallback chain** — that is purely an acdream artifact. ### D.3 — `motion & 0x10000000` (action, e.g. emote) Lines 298850–298907. **Overlay path**: ```c if ((motion & 0x10000000) != 0) { uint32_t key = (state->style << 0x10) | (state->substate & 0xffffff); MotionData* current_substate_md = LongHash::lookup(&this->cycles, key); if (current_substate_md != 0) { MotionData* link = get_link(this, state->style, state->substate, state->substate_mod, motion, speed_mod); if (link != 0) { state->add_action(motion, speed_mod); // append to action queue sequence->clear_physics(); sequence->remove_cyclic_anims(); // remove looping anims add_motion(sequence, link, speed_mod); // transition anim (one-shot) add_motion(sequence, current_substate_md, state->substate_mod); // re-add substate cycle! re_modify(this, sequence, state); return 1; } } } ``` **Crucial: actions DO NOT replace the substate cycle.** They prepend a one-shot link animation, then re-add the current substate cycle so it keeps looping after the action. Acdream's "Action route" is correct in spirit but should preserve the running cycle exactly like this. ### D.4 — `motion & 0x20000000` (modifier — turn, sidestep, all overlay cycles) Lines 298909–298945. **Modifier list overlay**: ```c if ((motion & 0x20000000) != 0) { // current substate must be a non-OneShot cycle MotionData* current_substate_md = LongHash::lookup(&this->cycles, (state->style << 0x10) | (state->substate & 0xffffff)); if (current_substate_md != 0 && (current_substate_md->bitfield & 1) == 0) { // Look up the modifier cycle MotionData* mod_md = LongHash::lookup(&this->modifiers, (state->style << 0x10) | (motion & 0xffffff)); if (mod_md == 0) mod_md = LongHash::lookup(&this->modifiers, (motion & 0xffffff)); // default-style fallback if (mod_md != 0) { int rc = state->add_modifier(motion, speed_mod); // adds to modifier_head list if (rc == 0) { // already has a modifier with this motion — stop it and re-add StopSequenceMotion(this, motion, 1.0f, state, sequence, &num_out); rc = state->add_modifier(motion, speed_mod); } if (rc != 0) { combine_motion(sequence, mod_md, speed_mod); // BLEND velocity/omega into sequence return 1; } } } } ``` **`combine_motion`** (line 298472, addr `00522580`) — adds the modifier's velocity AND omega into the existing sequence via `CSequence::combine_physics`. So **turn modifiers contribute their omega on top of the substate's velocity**. This is how retail composes "running while turning while strafing": three layers of physics contributions in the same `CSequence`, animated by whichever layers brought animations in. --- ## E. `is_allowed` — the gating predicate Line **298526–298548**, address `005226c0`. Determines whether an incoming substate is legal in the current state. ```c int CMotionTable::is_allowed(this, motion, motion_data, state) { if (motion_data == 0) return 0; if ((motion_data->bitfield & 2) != 0) { // requires "default substate" if (motion != state->substate) { // Look up the default substate for this style; legal only if state is in it uint32_t default_substate; LongNIValHash::lookup(&style_defaults, state->style, &default_substate); return (default_substate == state->substate) ? 1 : 0; } } return 1; } ``` So a substate transition that requires the "ready" state (bitfield bit 1) will **fail** if the player is currently in a non-default substate. This is the retail-correct way to block (for example) a Sit cycle mid-Run — not a custom acdream "skip if airborne" hack. --- ## F. `re_modify` — the "re-attach modifiers after substate change" Line **298300–298328**, address `005222e0`. After a substate transition that may have cleared modifiers, this walks the modifier list and re-applies each via `GetObjectSequence`: ```c void CMotionTable::re_modify(this, sequence, state) { if (state->modifier_head == 0) return; MotionState backup; // 298308 MotionState::MotionState(&backup, state); while (i != 0) { MotionList* mod = state->modifier_head; uint32_t motion = mod->motion; float speed = mod->speed_mod; state->remove_modifier(mod, NULL); backup.remove_modifier(i, NULL); GetObjectSequence(this, motion, state, sequence, speed, &num_out, 0); // recurse } backup.~MotionState(); } ``` This is why turn + sidestep persist across forward-cycle transitions (WalkForward → RunForward) — they are stored in `modifier_head` and get re-blended every time the substate changes. --- ## G. Final critical answers ### G.1 — When `forward=RunForward` + `sidestep=SidestepRight` + `turn=TurnLeft` arrive in one UM, what cycle plays? **All three layered.** Specifically, after `apply_interpreted_movement` processes the UM: 1. `DoInterpretedMotion(current_style)` — re-asserts style; usually no-op if unchanged. 2. `DoInterpretedMotion(0x44000007 RunForward, speed=runRate*1.0)` — `GetObjectSequence` takes the **substate** path (D.2). Replaces prior substate. `state->substate = RunForward`. 3. `DoInterpretedMotion(0x6500000f SidestepRight, speed=1.248)` — `GetObjectSequence` takes the **modifier** path (D.4). Adds to `modifier_head`, calls `combine_motion` to blend sidestep velocity into the running `CSequence`. **Substate cycle is unchanged** (still RunForward). 4. `DoInterpretedMotion(0x6500000d TurnLeft, speed=1.5)` — same as 3 but for turn. Blends turn omega into the sequence. **Visual result**: the RunForward animation cycle plays. Sidestep and turn contribute velocity/omega only (their cycles are typically motion- data with `velocity != 0` and `omega != 0` but `num_anims == 0` — they're physics-only modifiers that don't override the running anim). Some MotionTables may have animation content on sidestep/turn modifiers for emphasis, in which case the bones get an additive blend. ### G.2 — Substate winner pick, sequential SetCycle, or Frame-level composition? **Sequential `GetObjectSequence` calls per axis** (current_style → forward → sidestep → turn), each mutating the same `CSequence` via: - substate: `clear_physics + remove_cyclic_anims + add_motion(link) + add_motion(new)` (replace) - modifier: `combine_motion` (additive blend) + `state->add_modifier` (track for re_modify) - action: `clear_physics + remove_cyclic_anims + add_motion(link) + add_motion(current_substate)` (overlay-with-restore) The final result is a single `CSequence` carrying: - One looping substate cycle (animation + velocity/omega contribution) - Zero or more queued action cycles (one-shot anims; auto-pop via `MotionState::remove_action_head` on completion) - Zero or more modifier cycles (additive velocity/omega; usually no animation content) There is **no priority pick**. There is **no Frame-level layering** — all three are blended into the single `CSequence`'s velocity/omega fields by `add_motion`/`combine_motion` and the result is integrated once per physics tick. ### G.3 — Does the `RunForward → WalkForward → Ready` fallback chain exist? **No.** `GetObjectSequence` has only one fallback: when the cycle for the current style isn't found, fall back to `default_style`'s version (line 298842 in style-change branch, line 298872-298886 in action branch via `style_defaults` lookup). If neither exists, return 0 and the call has no effect. The acdream fallback (`RunForward → WalkForward → Ready`) is a **port artifact** that papers over the fact that we're not using `MotionTableManager` — we're synthesizing cycle-anim association directly from a hardcoded enum. **In a faithful port this fallback goes away.** ### G.4 — For Action overlay packets, does retail leave the substate cycle running? **Yes, exactly.** D.3 above: ```c add_motion(sequence, link, speed_mod); // one-shot transition add_motion(sequence, current_substate_md, state->substate_mod); // re-add running cycle ``` The `MotionState::action_head` queue tracks the active actions; the sequence has both the action's transition anim AND the substate cycle re-applied. When the action's one-shot anim completes, `CSequence::CheckForCompletedMotions` (in `CPhysicsObj`) pops the action and re-runs `apply_interpreted_movement` to restore pure substate state. --- ## H. Acdream port implications 1. **Delete the priority cycle-picker** in `OnLiveMotionUpdated`. Replace with a faithful port of `apply_interpreted_movement`: 4 sequential `MotionTableManager.PerformMovement` calls (current_style, forward, sidestep, turn) per UM. 2. **Delete the `RunForward → WalkForward → Ready` fallback chain** entirely. If a MotionTable doesn't have a cycle, retail just silently fails to transition — there is no fallback. Our fallback is masking missing animation data. 3. **Port `MotionTableManager`** so we have an actual `MotionState` (style + substate + substate_mod + modifier_head + action_head) per remote object, and a `CMotionTable` lookup chain (`cycles`/`modifiers`/`links`/`style_defaults`). The current approach of "pick one cycle per UM and play it" cannot represent modifier overlay correctly. 4. **Run-detection: WalkForward+HoldKey.Run → RunForward** must happen in `adjust_motion` BEFORE the routing. Acdream's `AnimationCommandRouter.Classify` runs after this transform — correct in concept, but only if our outbound and inbound both apply the transform consistently. (ACE does this on the outbound, so inbound `0x07 RunForward` is post-adjusted.) 5. **Modifier physics**: `combine_motion` blends velocity AND omega into a single `CSequence`. Acdream's `ObservedOmega` workaround (audit doc 06 line 83) is a symptom of not blending omega into the per-tick velocity properly. Once `MotionTableManager` is ported, omega comes from `combine_motion` of TurnLeft's modifier cycle and the `update_object` MinQuantum hack disappears. 6. **Sidestep direction collapse**: retail collapses `SidestepLeft → SidestepRight (negative speed)` and `TurnRight → TurnLeft (negative speed)` in `adjust_motion`. The modifier list keys on the collapsed form. Acdream must do the same to match the modifier-table lookups. --- ## I. Citation index | Function | Address | File line | |---|---|---| | `CMotionInterp::DoMotion` | `00528d20` | 306159 | | `CMotionInterp::DoInterpretedMotion` | `00528360` | 305575 | | `CMotionInterp::adjust_motion` | `00528010` | 305343 | | `CMotionInterp::apply_run_to_command` | `00527be0` | 305062 | | `CMotionInterp::apply_interpreted_movement` | `00528600` | 305713 | | `CMotionInterp::apply_raw_movement` | `005287e0` | 305817 | | `CMotionInterp::apply_current_movement` | `00528870` | 305838 | | `CPhysicsObj::DoInterpretedMotion` | `0050ea70` | 276348 | | `CPartArray::DoInterpretedMotion` | `00518750` | 286772 | | `MotionTableManager::PerformMovement` | `0051c0b0` | 290906 | | `MotionTableManager::initialize_state` | `0051c030` | 290875 | | `CMotionTable::GetObjectSequence` | `00522860` | 298636 | | `CMotionTable::DoObjectMotion` | `00523e90` | 300045 | | `CMotionTable::StopObjectMotion` | `00523ec0` | 300053 | | `CMotionTable::StopSequenceMotion` | `00522fc0` | 298954 | | `CMotionTable::SetDefaultState` | `005230a0` | 299004 | | `CMotionTable::is_allowed` | `005226c0` | 298526 | | `CMotionTable::get_link` | `00522710` | 298552 | | `CMotionTable::re_modify` | `005222e0` | 298300 | | `RawMotionState::ApplyMotion` | `0051eb60` | 293630 | | `InterpretedMotionState::ApplyMotion` | `0051ea40` | 293531 | | `MotionState::add_modifier` | `00526340` | 303081 | | `MotionState::add_modifier_no_check` | `00525ff0` | 302772 | | `MotionState::add_action` | `005260a0` | 302828 | | `MotionState::clear_modifiers` | `00526070` | 302810 | | `MotionState::remove_modifier` | `00526040` | 302794 | | `add_motion` (free fn) | `005224b0` | 298437 | | `combine_motion` (free fn) | `00522580` | 298472 | | `subtract_motion` (free fn) | `00522600` | 298492 | | Constant | Value | Meaning | |---|---|---| | `0x40000000` | flag | substate class bit (forward axis) | | `0x10000000` | flag | action class bit | | `0x20000000` | flag | modifier class bit | | `0x44000007` | id | RunForward substate | | `0x45000005` | id | WalkForward substate | | `0x45000006` | id | WalkBackward substate (collapses to BackForward) | | `0x40000011` | id | (referenced in jump path) | | `0x40000015` | id | Stand substate | | `0x41000003` | id | Ready substate | | `0x6500000d` | id | TurnLeft modifier | | `0x6500000e` | id | TurnRight modifier (collapses to TurnLeft) | | `0x6500000f` | id | SidestepRight modifier | | `0x65000010` | id | SidestepLeft modifier (collapses to SidestepRight) | | `0x6500000f` (jump-charge) | id | charge_jump cycle | | `0x8000003d` | id | "no style" sentinel (CombatMode_NonCombat default) |