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>
598 lines
26 KiB
Markdown
598 lines
26 KiB
Markdown
# 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) |
|