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>
This commit is contained in:
Erik 2026-05-05 14:56:42 +02:00
parent a3f53c2644
commit de129bc164
18 changed files with 10721 additions and 190 deletions

View file

@ -0,0 +1,598 @@
# 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) |