acdream/docs/research/2026-05-04-l3-port/13-cycle-picker.md
Erik de129bc164 feat(motion): L.3 M1 — fresh InterpolationManager port + retail spec
Rewrites src/AcDream.Core/Physics/InterpolationManager.cs from the spec
in docs/research/2026-05-04-l3-port/04-interp-manager.md. Public API
preserved (Vector3-returning AdjustOffset, Enqueue, Clear, IsActive,
Count) so PositionManager + GameWindow callers continue to compile;
internals are full retail spec.

Bug fixes vs prior port (audit 04-interp-manager.md § 7):

  #1  progress_quantum accumulates dt (sum of frame deltas), not step
      magnitude. Retail line 353140; the prior port's `+= step` made
      the secondary stall ratio meaningless.

  #3  Far-branch Enqueue (dist > AutonomyBlipDistance = 100m) sets
      _failCount = StallFailCountThreshold + 1 = 4, so the next
      AdjustOffset call's post-stall check fires an immediate blip-to-
      tail snap. Retail line 352944. Prior port silently drifted
      toward far targets at catch-up speed instead of teleporting.

  #4  Secondary stall test ports the retail formula verbatim:
      cumulative / progress_quantum / dt < CREATURE_FAILED_INTERPOLATION_PERCENTAGE.
      Audit notes the units are 1/sec (likely Turbine bug or x87 FPU
      misread by Binary Ninja) — mirrored byte-for-byte regardless.

  #5  Tail-prune is a tail-walking loop, not a single-tail compare.
      Multiple consecutive stale tail entries within DesiredDistance
      (0.05 m) of the new target collapse together. Retail line 352977.

  #6  Cap-eviction at the HEAD when count reaches 20 (already correct
      in the prior port; verified).

New API: Enqueue gains an optional `currentBodyPosition` parameter so
the far-branch detection can reference the body when the queue is
empty. Backward-compatible (default null = pre-far-branch behavior).

UseTime collapsed into AdjustOffset's tail (post-stall blip check)
since acdream has no per-tick UseTime call separate from
adjust_offset; identical semantic outcome.

State fields renamed to retail names with sentinel values:
  _frameCounter, _progressQuantum, _originalDistance (init = 999999f
  sentinel per retail line 0x00555D30 ctor), _failCount.

Tests:
- 17/17 InterpolationManagerTests green.
- New test Enqueue_FarBranch_PrearmsImmediateBlipOnNextAdjustOffset
  pins the bug #3 fix: enqueueing 150 m away triggers a same-tick
  blip (delta length ≈ 150 m), and the queue clears.

Spec tree: 17 research docs (00–14) under docs/research/2026-05-04-l3-port/.
00-master-plan + 00-port-plan describe the 8-phase rollout. 01-per-tick,
03-up-routing, 04-interp-manager, 05-position-manager-and-partarray,
06-acdream-audit, 14-local-player-audit are the L.3 spec used by this
commit and the M2 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:56:42 +02:00

598 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 **305713305788** (`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`
(305838305857). `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 **293630293703**, 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 **305343305400**, 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 **298636298950**, 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 298661298735. Substate's effect: reset to default substate of
the new style, optionally clear modifiers, replace cycles.
### D.2 — `motion & 0x40000000` (substate)
Lines 298737298848. 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 298850298907. **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 298909298945. **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 **298526298548**, 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 **298300298328**, 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) |