docs(R2-Q0): motion-table research base + ambiguity pins
- r2-motiontable-decomp.md: 1,603-line verbatim extraction — full CMotionTable (GetObjectSequence all 4 class branches, get_link, is_allowed, re_modify, StopSequenceMotion, SetDefaultState, wrappers), the free functions (add/combine/subtract_motion, change_cycle_speed, same_sign), all 16 MotionTableManager members (pending_animations, add_to_queue, remove_redundant_links with the 0xb0000000/0x70000000 block masks, truncate, AnimationDone vs CheckForCompletedMotions, PerformMovement with the 0x41000003 stop sentinel), MotionState's full modifier-stack/action-FIFO cast, verbatim struct layouts + constants table. BN mistypings identified (SurfInfo lookups are style_defaults/links hashes). - r2-ace-motiontable.md: ACE cross-ref with the two-tracker headline (MotionTableManager UPSTREAM of MotionInterp — never merged) + 5 flagged ACE oddities. - r2-port-plan.md: 17 gaps (H1-H17), keep list, Q0-Q6 commit sequence, the MotionDone->R3 boundary contract. - Q0-pins.md: A1/A2 pinned to ACE's reading (three corroborations; cdb confirmation folds into the next live session), A3 outTicks decode, A4 ACE-oddity adjudications (the Action-branch double-count is an ACE bug — do not copy), A5 Bitfield check at Q2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a987cad182
commit
dc54a3e41f
4 changed files with 2493 additions and 0 deletions
43
docs/research/2026-07-02-r2-motiontable/Q0-pins.md
Normal file
43
docs/research/2026-07-02-r2-motiontable/Q0-pins.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# R2-Q0 — ambiguity pins and ACE-oddity adjudications
|
||||||
|
|
||||||
|
The verbatim extraction is `r2-motiontable-decomp.md` (1,603 lines, line-anchored);
|
||||||
|
the port plan with the full ambiguity table is `r2-port-plan.md` §0. This note
|
||||||
|
records the PINNED resolutions the Q1–Q5 ports code against.
|
||||||
|
|
||||||
|
## Pinned
|
||||||
|
|
||||||
|
- **A1 — `get_link` branch predicate: PINNED to ACE's reading** ("EITHER speed
|
||||||
|
negative → swapped-key branch"). Three independent corroborations: ACE
|
||||||
|
`MotionTable.cs:395-426`; the working adapter's field-validated `GetLink`
|
||||||
|
(the reversed-key branch fixed the Ready→WalkBackward glitch); call-site arg
|
||||||
|
roles. The BN "both negative" reading is the same x87-flag-noise class as
|
||||||
|
`is_newer`'s garbled setcc and the R1 hook-direction swap. cdb confirmation
|
||||||
|
(bp `CMotionTable::get_link`, Ready→WalkBackward vs Ready→WalkForward)
|
||||||
|
folds into the next live retail session — non-blocking.
|
||||||
|
- **A2 — Branch-2 `signedSpeed`: PINNED to ACE** (`SubstateMod < 0 &&
|
||||||
|
speedMod > 0 → -speedMod`, the single-direction flip) pending the same cdb
|
||||||
|
session (golden: a Walk(−)→Walk(+) flip).
|
||||||
|
- **A3 — outTicks** = sum of each appended MotionData's `num_anims` (+ base
|
||||||
|
cycle in Branch 1/2) − 1. The `action_head` rendering in BN is the packed
|
||||||
|
`num_anims` byte (decomp's own note).
|
||||||
|
- **A4 — ACE oddities adjudicated:** (1) Action-branch numAnims double-count =
|
||||||
|
ACE BUG, do not copy (retail sums outHop + actionLink [+ returnHop] only);
|
||||||
|
(2) `change_cycle_speed` old≈0 silent no-op = RETAIL (port verbatim,
|
||||||
|
including the gap); (3) `GetLinkData` 0xFFFF mask = ACE-only helper, not
|
||||||
|
ported; (4) `StopObjectCompletely` return = `finalStopOk ? 1 :
|
||||||
|
anyModifierStopOk`, port verbatim; (5) `re_modify` snapshot = deep-copied
|
||||||
|
MotionState used ONLY as the loop-termination bound — C# port deep-copies,
|
||||||
|
pops both, terminates on snapshot empty.
|
||||||
|
- **A5 — `MotionData.Bitfield`** (bit1 = substate-gated for `is_allowed`,
|
||||||
|
bit0 = clear-modifiers-on-entry): confirm on DatReaderWriter
|
||||||
|
`Types.MotionData` with a one-line Humanoid-table test at Q2.
|
||||||
|
|
||||||
|
## Q0 cdb capture (pending, non-blocking)
|
||||||
|
|
||||||
|
One live session feeds all R2 goldens: bp GetObjectSequence / get_link /
|
||||||
|
StopSequenceMotion / add_to_queue / remove_redundant_links /
|
||||||
|
truncate_animation_list / AnimationDone / CheckForCompletedMotions with
|
||||||
|
arg+ret logging (pattern `tools/cdb/l2g-observer.cdb`); protocol per
|
||||||
|
`r2-port-plan.md` §0. Until then Q2/Q3 rest on dat fixtures + synthetic
|
||||||
|
state tables + the archived 2026-05-03 walk→run trace golden (quoted in the
|
||||||
|
old Fix B comment block).
|
||||||
548
docs/research/2026-07-02-r2-motiontable/r2-ace-motiontable.md
Normal file
548
docs/research/2026-07-02-r2-motiontable/r2-ace-motiontable.md
Normal file
|
|
@ -0,0 +1,548 @@
|
||||||
|
# ACE MotionTable / MotionTableManager port — cross-reference map
|
||||||
|
|
||||||
|
Files read in full:
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Animation/MotionTable.cs` (615 lines)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Managers/MotionTableManager.cs` (251 lines)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Animation/AnimNode.cs` (16 lines)
|
||||||
|
- `references/ACE/Source/ACE.DatLoader/Entity/MotionData.cs` (34 lines)
|
||||||
|
- Cross-refs: `PhysicsObj.cs` (L296-300, L653-657, L899-902), `MovementManager.cs` (L118-121),
|
||||||
|
`MotionInterp.cs` (L210-231, L260), `PartArray.cs` (L52-56, L72-76, L135, L253-293, L425-435, L577-586),
|
||||||
|
`WeenieObject.cs` (L256-259)
|
||||||
|
|
||||||
|
**NOTE (important for the parent report):** ACE has TWO parallel "pending motion" trackers that
|
||||||
|
both descend from a `PhysicsObj`, and they are easy to conflate:
|
||||||
|
|
||||||
|
1. `MotionTableManager.PendingAnimations` (`LinkedList<AnimNode>`) — driven by
|
||||||
|
`Table.DoObjectMotion`/`GetObjectSequence` (the **interpreted-command / high-level motion**
|
||||||
|
path). Its `AnimationDone`/`CheckForCompletedMotions` are the ones requested for this doc.
|
||||||
|
2. `MotionInterp.PendingMotions` — a *separate* list inside `MotionInterp.cs` (not covered file,
|
||||||
|
but referenced at MotionInterp.cs:210-231) that also has a `MotionDone(bool success)` method.
|
||||||
|
`MovementManager.MotionDone` calls `MotionInterpreter.MotionDone(success)`, i.e. the **raw**
|
||||||
|
motion-interp side, NOT `MotionTableManager.AnimationDone`. `MotionTableManager` and
|
||||||
|
`MotionInterp` are wired independently; `MotionTableManager` lives under `PartArray`, while
|
||||||
|
`MotionInterp` lives under `MovementManager`. Both ultimately get fed by `PhysicsObj.MotionDone`
|
||||||
|
but through different owner objects (`PartArray.MotionTableManager` vs
|
||||||
|
`MovementManager.MotionInterpreter`), and only one of the two is authoritative depending on
|
||||||
|
whether the object is server-simulated purely by object-broadcast Motion commands (uses
|
||||||
|
MotionTableManager via PartArray) vs. by the mover's own physics timestep + MoveToManager
|
||||||
|
(uses MotionInterp). Do not assume `MotionTableManager.AnimationDone` is "the" MotionDone path
|
||||||
|
for player-driven movement — cross-check which owner actually gets ticked for the acdream use
|
||||||
|
case before porting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MotionTable.cs (`ACE.Server.Physics.Animation.MotionTable`)
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
```
|
||||||
|
uint ID
|
||||||
|
Dictionary<uint,uint> StyleDefaults // style -> default substate
|
||||||
|
Dictionary<uint,MotionData> Cycles // key = (style<<16)|substate -> cycle anim data
|
||||||
|
Dictionary<uint,MotionData> Modifiers // key = (style<<16)|modifierID or just modifierID -> modifier anim data
|
||||||
|
Dictionary<uint,Dictionary<uint,MotionData>> Links // key = (style<<16)|fromSubstate -> { toMotion -> transition MotionData }
|
||||||
|
uint DefaultStyle
|
||||||
|
static ConcurrentDictionary<uint,float> WalkSpeed / RunSpeed / TurnSpeed // per-motionTableID cache
|
||||||
|
```
|
||||||
|
Constructed either empty (`Allocator()`) or from the dat-loaded
|
||||||
|
`DatLoader.FileTypes.MotionTable` (straight field copy, no transform — L40-48).
|
||||||
|
|
||||||
|
### `DoObjectMotion(motion, currState, sequence, speedMod, ref numAnims)`
|
||||||
|
Trivial forwarder: `return GetObjectSequence(motion, currState, sequence, speedMod, ref numAnims, stopModifiers: false);`
|
||||||
|
(L55-58)
|
||||||
|
|
||||||
|
### `GetObjectSequence(motion, currState, sequence, speedMod, ref numAnims, stopModifiers)` — L60-257
|
||||||
|
The core state-transition dispatcher. `numAnims` is reset to 0 up front.
|
||||||
|
|
||||||
|
Early-out: if `currState.Style == 0 || currState.Substate == 0` → `false` (uninitialized state).
|
||||||
|
|
||||||
|
Looks up `substate = StyleDefaults[currState.Style]` (the *default substate for the current
|
||||||
|
style*, e.g. "Standing" under style "NonCombat").
|
||||||
|
|
||||||
|
**Guard (L73-74):** if `motion == substate` (i.e. caller is asking to enter the style's own
|
||||||
|
default substate) AND `!stopModifiers` AND current substate already has the Modifier bit set
|
||||||
|
(`CommandMask.Modifier`) → return `true` immediately (no-op — already effectively there via a
|
||||||
|
modifier).
|
||||||
|
|
||||||
|
Then four command-mask branches, checked with **plain OR semantics — NOT else-if.** Each branch
|
||||||
|
can independently fire and `return true` from inside if it succeeds; falling out of a branch
|
||||||
|
(motionData null, is_allowed false, etc.) falls through to the next mask check.
|
||||||
|
|
||||||
|
**`CommandMask.Style` branch (L76-120)** — motion requests a stance/style change (e.g. switch
|
||||||
|
combat stance):
|
||||||
|
- If `currState.Style == motion` already → `true` (no-op).
|
||||||
|
- If `substate != currState.Substate`: compute `motionData = get_link(currState.Style,
|
||||||
|
currState.Substate, currState.SubstateMod, substate, speedMod)` — the transition INTO the new
|
||||||
|
style's default substate from the current substate.
|
||||||
|
- If `substate != 0`: look up `cycles = Cycles[(motion<<16)|substate]` — the cycle anim for the
|
||||||
|
new style's default substate.
|
||||||
|
- If found:
|
||||||
|
- `(cycles.Bitfield & 1) != 0` → `currState.clear_modifiers()` (bit 1 = "clears modifiers on
|
||||||
|
entry", e.g. entering a stance that can't carry over swimming/etc modifiers).
|
||||||
|
- `link = get_link(currState.Style, substate, currState.SubstateMod, motion, speedMod)` —
|
||||||
|
transition from (old style, NEW style's default substate) to the target style itself.
|
||||||
|
- **Fallback chain if `link == null` and `currState.Style != motion`:** re-resolve via
|
||||||
|
`DefaultStyle` — `link = get_link(currState.Style, substate, 1.0f, DefaultStyle, 1.0f)`,
|
||||||
|
then `motionData_ = get_link(DefaultStyle, StyleDefaults[DefaultStyle], 1.0f, motion, 1.0f)`.
|
||||||
|
This is the "no direct link exists, route through the global default style" path (e.g.
|
||||||
|
Standing).
|
||||||
|
- `sequence.clear_physics(); sequence.remove_cyclic_anims();` — wipe outstanding velocity/
|
||||||
|
omega contributions and any looping cycle anims before splicing in the new chain.
|
||||||
|
- Append in strict order: `add_motion(motionData)`, `add_motion(link)`,
|
||||||
|
`add_motion(motionData_)`, `add_motion(cycles)` — i.e. [transition-to-default-substate] →
|
||||||
|
[transition-to-target-style] → [fallback-via-default-style, usually null] →
|
||||||
|
[new cycle]. Each `add_motion` no-ops silently on null `motionData`.
|
||||||
|
- Commits state: `currState.Substate = substate; currState.Style = motion; currState.SubstateMod = speedMod;`
|
||||||
|
- `re_modify(sequence, currState)` — replays any still-active modifiers (see below) on top
|
||||||
|
of the newly spliced sequence.
|
||||||
|
- `numAnims = sum of each non-null MotionData's Anims.Count, minus 1` (the `-1` is
|
||||||
|
because the queue entry itself represents completion of ONE playthrough of the LAST
|
||||||
|
appended motion's cycle, not a raw anim count — cross-check against `add_to_queue` in
|
||||||
|
MotionTableManager, which stores this count as `AnimNode.NumAnims`, i.e. the number of
|
||||||
|
"hook" firings, one per queued sub-anim minus a terminal borrow).
|
||||||
|
- Returns `true`.
|
||||||
|
|
||||||
|
**`CommandMask.SubState` branch (L121-188)** — motion requests a substate change within the
|
||||||
|
current style (e.g. Standing → Crouching), OR a coalesced-speed no-op update to the CURRENT
|
||||||
|
cycle:
|
||||||
|
- `motionID = motion & 0xFFFFFF` (strip command-class bits).
|
||||||
|
- `motionData = Cycles[(currState.Style<<16)|motionID]`, falling back to
|
||||||
|
`Cycles[(DefaultStyle<<16)|motionID]` if the current style doesn't define that substate.
|
||||||
|
- If found and `is_allowed(motion, motionData, currState)` (see below):
|
||||||
|
- **Speed-only fast path (L132-139):** if `motion == currState.Substate` AND
|
||||||
|
`sequence.HasAnims()` AND `Math.Sign(speedMod) == Math.Sign(currState.SubstateMod)` — i.e.
|
||||||
|
caller is re-issuing the SAME substate at a new speed, same direction of travel (fwd vs
|
||||||
|
reverse) — then: `change_cycle_speed` (rescale the already-playing cyclic anim's framerate),
|
||||||
|
`subtract_motion` (remove the old velocity/omega contribution at the old speed),
|
||||||
|
`combine_motion` (add back at the new speed), update `currState.SubstateMod = speedMod`,
|
||||||
|
return `true`. **No new anims queued, no sequence splice** — this is the "already running,
|
||||||
|
just changing speed" branch (e.g. walk→run without breaking stride).
|
||||||
|
- Otherwise (new substate, or sign flip == direction reversal):
|
||||||
|
- `(motionData.Bitfield & 1) != 0` → `currState.clear_modifiers()`.
|
||||||
|
- `link = get_link(currState.Style, currState.Substate, currState.SubstateMod, motion, speedMod)`
|
||||||
|
— direct transition from current substate to target substate.
|
||||||
|
- **Fallback (L145-151):** if `link == null` OR the sign of `speedMod` differs from
|
||||||
|
`currState.SubstateMod` (direction reversal, e.g. forward↔backward) — route through the
|
||||||
|
style's default substate: `link = get_link(currState.Style, currState.Substate,
|
||||||
|
currState.SubstateMod, defaultMotion, 1.0f)` (transition out to default),
|
||||||
|
`motionData_ = get_link(currState.Style, defaultMotion, 1.0f, motion, speedMod)`
|
||||||
|
(transition from default into target).
|
||||||
|
- `sequence.clear_physics(); sequence.remove_cyclic_anims();`
|
||||||
|
- If `motionData_ != null` (fallback path taken): append `link` at `currState.SubstateMod`,
|
||||||
|
then `motionData_` at `speedMod`.
|
||||||
|
- Else (direct link path): `newSpeedMod = speedMod`, but **flip sign** if
|
||||||
|
`currState.SubstateMod < 0 && speedMod > 0` (i.e. reversing FROM negative — asymmetric
|
||||||
|
handling, only corrects one direction of sign mismatch, not both) — append `link` at
|
||||||
|
`newSpeedMod`.
|
||||||
|
- Always append `motionData` (the new cycle) at `speedMod`.
|
||||||
|
- **Modifier carry-over (L170-176):** if the OLD substate differs from the new `motion` AND
|
||||||
|
the old substate had the Modifier bit set, AND the old substate isn't just the style's
|
||||||
|
default motion, re-add it as an active modifier via `currState.add_modifier_no_check`
|
||||||
|
(i.e. modifiers riding on a substate survive a substate change unless they equal the new
|
||||||
|
target or the style default).
|
||||||
|
- Commit `currState.SubstateMod = speedMod; currState.Substate = motion;`, then `re_modify`.
|
||||||
|
- `numAnims = motionData.Anims.Count + link.Anims.Count + motionData_.Anims.Count - 1`
|
||||||
|
(nulls treated as 0).
|
||||||
|
- Returns `true`.
|
||||||
|
|
||||||
|
**`CommandMask.Action` branch (L189-233)** — one-shot action motions (attacks, jumps, etc,
|
||||||
|
things layered on TOP of a cycle without replacing it):
|
||||||
|
- `cycleKey = (currState.Style<<16)|(currState.Substate & 0xFFFFFF)`; `motionData =
|
||||||
|
Cycles[cycleKey]` (the CURRENT cycle, must exist).
|
||||||
|
- If found:
|
||||||
|
- `link = get_link(currState.Style, currState.Substate, currState.SubstateMod, motion, speedMod)`.
|
||||||
|
- **If `link != null` (direct action link exists):** `currState.add_action(motion, speedMod)`
|
||||||
|
(push onto the action stack — see MotionState, not read this pass), clear physics/cyclic,
|
||||||
|
append `link` at `speedMod` then re-append `motionData` (the ORIGINAL cycle, at the OLD
|
||||||
|
`currState.SubstateMod` — i.e. resume the cycle after the action plays), `re_modify`,
|
||||||
|
`numAnims = link.Anims.Count` (note: NOT including motionData's count here — only the
|
||||||
|
action-link portion counts toward completion tracking, the re-appended base cycle is
|
||||||
|
presumably a normal looping anim not subject to the same "done" semantics).
|
||||||
|
- **Else (no direct action link — fallback via style default, L209-231):**
|
||||||
|
`motionData = get_link(currState.Style, currState.Substate, currState.SubstateMod, substate, 1.0f)`
|
||||||
|
(transition current substate → style's own default substate) as a NEW `motionData`
|
||||||
|
(shadows the cycle lookup above). If that resolves: `link = get_link(currState.Style,
|
||||||
|
substate, 1.0f, motion, speedMod)` (default substate → action), and re-fetch `cycles =
|
||||||
|
Cycles[cycleKey]` (original cycle again). If `link != null` and `cycles` found:
|
||||||
|
`motionData_ = get_link(currState.Style, substate, 1.0f, currState.Substate,
|
||||||
|
currState.SubstateMod)` (default substate → back to original substate, for resuming after).
|
||||||
|
`currState.add_action(...)`, clear physics/cyclic, append in order `motionData` (→default),
|
||||||
|
`link` (default→action), `motionData_` (default→back), `cycles` (resume original cycle at
|
||||||
|
`currState.SubstateMod`). `re_modify`. `numAnims = motionData.Anims.Count +
|
||||||
|
link.Anims.Count + (motionData_ != null ? motionData.Anims.Count : 0)` — **NOTE: this last
|
||||||
|
term reads `motionData.Anims.Count` again, NOT `motionData_.Anims.Count` — looks like a copy-
|
||||||
|
paste bug in ACE's port** (compare to the Style/SubState branches which correctly sum each
|
||||||
|
distinct MotionData). Flag this explicitly when cross-checking against 2013 retail —
|
||||||
|
likely a genuine ACE divergence, not a retail behavior to replicate.
|
||||||
|
|
||||||
|
**`CommandMask.Modifier` branch (L234-255)** — continuous modifiers layered on the current cycle
|
||||||
|
(e.g. sneaking, aiming overlay) that don't interrupt it:
|
||||||
|
- `styleKey = currState.Style<<16`; `cycles = Cycles[styleKey|(currState.Substate&0xFFFFFF)]`
|
||||||
|
(current cycle must exist).
|
||||||
|
- If found AND `(cycles.Bitfield & 1) == 0` (cycle does NOT forbid modifiers):
|
||||||
|
- `motionData = Modifiers[styleKey|(motion&0xFFFFFF)]`, falling back to
|
||||||
|
`Modifiers[motion&0xFFFFFF]` (style-agnostic modifier) if not found.
|
||||||
|
- If found:
|
||||||
|
- `if (!currState.add_modifier(motion, speedMod))` — if the state rejects adding this
|
||||||
|
modifier (e.g. already present / list full):
|
||||||
|
- `StopSequenceMotion(motion, 1.0f, currState, sequence, ref numAnims)` — force-remove it
|
||||||
|
first, then retry `add_modifier`. If STILL fails, return `false`.
|
||||||
|
- `combine_motion(sequence, motionData, speedMod)` — layer the modifier's velocity/omega +
|
||||||
|
anims onto the sequence WITHOUT clearing physics or cyclic anims (additive, unlike the
|
||||||
|
other three branches which splice/replace).
|
||||||
|
- Returns `true`. **No numAnims write here — stays whatever it was reset to (0) at entry,
|
||||||
|
i.e. modifiers are not tracked for animation-completion purposes.**
|
||||||
|
|
||||||
|
Falls through all four branches unmatched → `return false`.
|
||||||
|
|
||||||
|
### `Get(uint motionTableID)` — static factory, L259-264
|
||||||
|
Directly `DatManager.PortalDat.ReadFromDat<FileTypes.MotionTable>(id)` wrapped in `new
|
||||||
|
MotionTable(...)`. Comment `//return ObjCache.GetMotionTable(mtableID);` — retail apparently
|
||||||
|
caches motion tables by ID; ACE re-reads from dat every call (no caching layer here, though the
|
||||||
|
dat reader itself likely caches file reads elsewhere).
|
||||||
|
|
||||||
|
### `SetDefaultState(state, sequence, ref numAnims)` — L266-291
|
||||||
|
Resets to the table's global default (style, substate) pair:
|
||||||
|
- `defaultSubstate = StyleDefaults[DefaultStyle]`; if missing → `false`.
|
||||||
|
- `state.clear_modifiers(); state.clear_actions();`
|
||||||
|
- `cycle = (DefaultStyle<<16)|defaultSubstate`; `motionData = Cycles[cycle]`; if missing → `false`.
|
||||||
|
- `numAnims = motionData.Anims.Count - 1`.
|
||||||
|
- `state.Style = DefaultStyle; state.Substate = defaultSubstate; state.SubstateMod = 1.0f;`
|
||||||
|
- `sequence.clear_physics(); sequence.clear_animations();` (note: `clear_animations`, not
|
||||||
|
`remove_cyclic_anims` — a harder reset, used only here and presumably at spawn/enter-world).
|
||||||
|
- `add_motion(sequence, motionData, 1.0f)`.
|
||||||
|
|
||||||
|
### `StopObjectCompletely(currState, sequence, ref numAnims)` — L293-313
|
||||||
|
Iterates and removes ALL active modifiers via `StopSequenceMotion` (loop drains
|
||||||
|
`currState.Modifiers.First` until empty — relies on `StopSequenceMotion` removing the head each
|
||||||
|
call), tracking whether ANY modifier stop succeeded (`success`). Then stops the current substate
|
||||||
|
itself: `StopSequenceMotion(currState.Substate, currState.SubstateMod, ...)`. Returns `true` if
|
||||||
|
EITHER the substate-stop succeeded OR any earlier modifier-stop succeeded (`success ||
|
||||||
|
substateStopSucceeded`, though written as an if/else that returns `true` whenever the final
|
||||||
|
substate-stop call returns non-... — re-read: `if (!StopSequenceMotion(...)) return success; else
|
||||||
|
return true;` — i.e. final result is `true` unless the LAST stop call fails, in which case it
|
||||||
|
falls back to whatever `success` was from the modifier loop).
|
||||||
|
|
||||||
|
### `StopObjectMotion` / `StopSequenceMotion` — L315-356
|
||||||
|
`StopObjectMotion` is a trivial forwarder to `StopSequenceMotion`.
|
||||||
|
|
||||||
|
`StopSequenceMotion(motion, speed, currState, sequence, ref numAnims)`:
|
||||||
|
- `numAnims = 0`.
|
||||||
|
- **SubState case:** if `(motion & CommandMask.SubState) != 0 && currState.Substate == motion` —
|
||||||
|
i.e. caller wants to stop the CURRENT substate → resolve the style's default substate and
|
||||||
|
re-enter it via `GetObjectSequence(style, currState, sequence, 1.0f, ref numAnims,
|
||||||
|
stopModifiers: true)` (recursion into the main dispatcher with `stopModifiers=true`, which
|
||||||
|
suppresses the early "already at default via modifier" guard at L73). Returns `true`
|
||||||
|
unconditionally after this call (return value of the inner `GetObjectSequence` is discarded).
|
||||||
|
- **Non-modifier, non-matching-substate case:** if `(motion & CommandMask.Modifier) == 0` →
|
||||||
|
`false` (nothing to stop — motion isn't a substate-stop or a modifier).
|
||||||
|
- **Modifier case:** linear-scan `currState.Modifiers` linked list for a node whose `.ID ==
|
||||||
|
motion`. On match:
|
||||||
|
- `key = (currState.Style<<16)|(motion&0xFFFFFF)`; `Modifiers[key]`, falling back to
|
||||||
|
`Modifiers[motion&0xFFFFFF]`. If neither resolves → `false`.
|
||||||
|
- `subtract_motion(sequence, motionData, modifier.Value.SpeedMod)` — reverse the modifier's
|
||||||
|
velocity/omega contribution using its ORIGINAL speed (not the `speed` parameter passed in —
|
||||||
|
`speed` param appears unused in this branch; only used implicitly via the SubState-case call
|
||||||
|
above). `currState.remove_modifier(modifier)`. Returns `true`.
|
||||||
|
- No match found after scanning entire list → `false`.
|
||||||
|
|
||||||
|
### `add_motion` / `combine_motion` / `subtract_motion` / `change_cycle_speed` — L358-393
|
||||||
|
- **`add_motion(sequence, motionData, speed)`:** no-op if `motionData == null`. Otherwise
|
||||||
|
`sequence.SetVelocity(motionData.Velocity * speed)`, `sequence.SetOmega(motionData.Omega *
|
||||||
|
speed)` (REPLACES, not adds — "Set" not "Combine"), then for each anim in `motionData.Anims`
|
||||||
|
wraps as `new AnimData(anim, speed)` and `sequence.append_animation(animData)`.
|
||||||
|
- **`combine_motion(sequence, motionData, speed)`:** no-op if null. Otherwise
|
||||||
|
`sequence.CombinePhysics(motionData.Velocity * speed, motionData.Omega * speed)` — additive
|
||||||
|
variant used by the Modifier branch (doesn't touch the anim queue at all, only velocity/omega).
|
||||||
|
- **`subtract_motion(sequence, motionData, speed)`:** no-op if null. `sequence.subtract_physics
|
||||||
|
(motionData.Velocity * speed, motionData.Omega * speed)` — inverse of combine, used when
|
||||||
|
removing a modifier or an old-speed cycle contribution.
|
||||||
|
- **`change_cycle_speed(sequence, motionData, substateMod, speedMod)`:** if
|
||||||
|
`|substateMod| > PhysicsGlobals.EPSILON` → `sequence.multiply_cyclic_animation_framerate
|
||||||
|
(speedMod/substateMod)` (rescale by the RATIO of new to old speed). Else-if `|speedMod| <
|
||||||
|
EPSILON` → `multiply_cyclic_animation_framerate(0)` (freeze the anim — old speed was ~0 so no
|
||||||
|
ratio is definable, new speed is also ~0). **Gap: if `substateMod` ~0 AND `speedMod` is
|
||||||
|
NON-zero, neither branch fires — no framerate change is applied.** Worth checking against
|
||||||
|
retail whether this is a real edge case (resuming a stopped cycle at nonzero speed without a
|
||||||
|
ratio) — could be a silent no-op bug carried from retail or an ACE gap.
|
||||||
|
|
||||||
|
### `get_link(style, substate, substateSpeed, motion, speed)` — L395-426
|
||||||
|
Direction-aware transition lookup with two symmetric lookup CHAINS depending on sign of the
|
||||||
|
speeds:
|
||||||
|
- **If EITHER `speed < 0` or `substateSpeed < 0` (reverse-direction transition):** look up
|
||||||
|
`Links[(style<<16)|(motion&0xFFFFFF)]` (keyed by DESTINATION motion) then `.TryGetValue
|
||||||
|
(substate, ...)` (indexed by SOURCE substate) — i.e. reversed key order vs the forward case.
|
||||||
|
If that fails, fall back through `StyleDefaults[style]` and
|
||||||
|
`Links[(style<<16)|(substate&0xFFFFFF)]` → `.TryGetValue(defaultMotion, ...)`.
|
||||||
|
- **Else (forward / non-negative speeds):** look up `Links[(style<<16)|(substate&0xFFFFFF)]`
|
||||||
|
(keyed by SOURCE substate) then `.TryGetValue(motion, ...)` (indexed by DESTINATION). Fallback:
|
||||||
|
`Links[style<<16]` (style-wide, substate-agnostic) → `.TryGetValue(motion, ...)`.
|
||||||
|
- Returns `null` if nothing resolves in either chain.
|
||||||
|
This encodes retail's dat-side Links table having asymmetric (from,to) vs (to,from) storage
|
||||||
|
depending on animation reversibility — reverse playback (e.g. walking backward) reuses the
|
||||||
|
FORWARD anim's link table keyed the other way around rather than storing a mirrored copy.
|
||||||
|
|
||||||
|
### `is_allowed(motion, motionData, state)` — L428-438
|
||||||
|
`false` if `motionData == null`. `true` if `(motionData.Bitfield & 2) == 0` (bit 2 = "always
|
||||||
|
allowed" flag) OR `motion == state.Substate` (re-entering the same substate is always allowed
|
||||||
|
regardless of the bit). Otherwise: only allowed if the CURRENT substate IS the style's own
|
||||||
|
default substate (`StyleDefaults[state.Style] == state.Substate`) — i.e. bit-2-gated substates
|
||||||
|
can only be entered FROM the style's default/neutral substate, not chained from an arbitrary
|
||||||
|
other substate.
|
||||||
|
|
||||||
|
### `re_modify(sequence, pstate)` — L440-458
|
||||||
|
No-op if `pstate.Modifiers.First == null`. Otherwise snapshots `pstate` into a NEW `MotionState
|
||||||
|
state = new MotionState(pstate)` (deep-ish copy incl. its own Modifiers list), then drains
|
||||||
|
`pstate`'s modifier list one node at a time: pops `speedMod`/`motion` off `pstate.Modifiers.First`,
|
||||||
|
removes the SAME logical entry from BOTH `pstate` and the snapshot `state` (odd double-removal —
|
||||||
|
removing from the snapshot copy seems purposeless unless `MotionState`'s copy ctor shares the
|
||||||
|
underlying list nodes, in which case this is defensive against aliasing), then calls
|
||||||
|
`GetObjectSequence(motion, pstate, sequence, speedMod, ref numAnims, stopModifiers: false)` to
|
||||||
|
RE-APPLY each modifier on top of the now-current (post-transition) `pstate`. This is how the
|
||||||
|
Style/SubState branches "carry forward" active modifiers across a cycle-changing transition —
|
||||||
|
after splicing the new base cycle in, `re_modify` walks the still-active modifier list (which at
|
||||||
|
that point is whatever the branch didn't already clear) and re-runs each one through the full
|
||||||
|
dispatcher so its layered anims/physics get re-appended onto the NEW sequence.
|
||||||
|
|
||||||
|
### Static helpers (L460-613)
|
||||||
|
- **`GetAttackFrames(motionTableId, stance, motion)`** — dat-lookup passthrough to
|
||||||
|
`DatLoader.FileTypes.MotionTable.GetAttackFrames` (not in this file). Returns cached
|
||||||
|
`emptyList` for `motionTableId == 0`.
|
||||||
|
- **`GetAnimationLength(motionTableId, stance, motion, speed=1)`** / the 2-motion overload that
|
||||||
|
also takes `currentMotion` — the latter, if `motion` has the Style bit set and `currentMotion
|
||||||
|
!= Ready`, first adds the Ready→currentMotion transition length, forces `currentMotion =
|
||||||
|
Ready`, THEN adds `currentMotion→motion`. Divides everything by `speed`.
|
||||||
|
- **`GetCycleLength`** — dat passthrough / speed.
|
||||||
|
- **`GetRunSpeed(motionTableID)`** — cached in static `RunSpeed` dict. Computes via
|
||||||
|
`GetMotionData(id, MotionCommand.RunForward)` → `GetAnimDist(motionData)`.
|
||||||
|
- **`GetTurnSpeed(motionTableID)`** — cached in static `TurnSpeed` dict.
|
||||||
|
`Math.Abs(GetMotionData(id, MotionCommand.TurnRight).Omega.Z)`.
|
||||||
|
- **`GetMotionData(motionTableID, motion, currentStyle=null)`** — resolves `currentStyle` to
|
||||||
|
`motionTable.DefaultStyle` if unspecified, strips command bits (`motion & 0xFFFFFF`), looks up
|
||||||
|
`Cycles[(style<<16)|motionID]`.
|
||||||
|
- **`GetLinkData(motionTableID, motion, currentStyle=null)`** — looks up
|
||||||
|
`Links[(style<<16)|((int)MotionCommand.Ready & 0xFFFF)]` (**NOTE: masks with `0xFFFF` here,
|
||||||
|
NOT `0xFFFFFF` like everywhere else in this file — inconsistent mask width, likely harmless
|
||||||
|
since `MotionCommand.Ready`'s low bits fit in 16 bits, but worth flagging as an ACE
|
||||||
|
inconsistency if porting verbatim**), then `.TryGetValue(motion, ...)`.
|
||||||
|
- **`GetAnimDist(motionData)`** — sums `frame.Origin` across every `PosFrames` frame of every
|
||||||
|
anim in `motionData.Anims` (reads each `Animation` fresh from dat by `anim.AnimId`), takes
|
||||||
|
`.Length()` of the summed offset vector, divides by `totalFrames`, multiplies by
|
||||||
|
`motionData.Anims[0].Framerate` → "distance per second". Returns 0 if the vector length is 0.
|
||||||
|
- **`HasDefaultScript(motionTableID, motion, currentStyle)`** — `GetLinkData` then scans every
|
||||||
|
anim's `PartFrames[*].Hooks` for `AnimationHookType.DefaultScript`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MotionTableManager.cs (`ACE.Server.Physics.Managers.MotionTableManager`) — owned by `PartArray`
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
```
|
||||||
|
PhysicsObj PhysicsObj
|
||||||
|
MotionTable Table
|
||||||
|
MotionState State
|
||||||
|
uint AnimationCounter
|
||||||
|
LinkedList<AnimNode> PendingAnimations
|
||||||
|
```
|
||||||
|
`AnimNode { uint Motion; uint NumAnims; }` (trivial struct-like class, `AnimNode.cs`).
|
||||||
|
|
||||||
|
### `AnimationDone(bool success)` — L28-61
|
||||||
|
Called from `PartArray.AnimationDone` ← `PhysicsObj.Hook_AnimDone()` (fired when a per-frame
|
||||||
|
animation HOOK signals completion — the frame-based `Hook_AnimDone` callback, NOT a polling
|
||||||
|
check). Logic:
|
||||||
|
- If `PendingAnimations.First == null` → no-op (nothing pending).
|
||||||
|
- `AnimationCounter++` (one hook fired, counts toward the FIRST queued `AnimNode`'s
|
||||||
|
`NumAnims` threshold).
|
||||||
|
- **Loop** while `node != null`:
|
||||||
|
- `entry = node.Value`. If `entry.NumAnims > AnimationCounter` → **break** (head entry hasn't
|
||||||
|
accumulated enough hook-fires yet — stop, this increment wasn't enough to finish it).
|
||||||
|
- If entry's motion has the Action bit set → `State.remove_action_head()` (pop the action
|
||||||
|
stack — the completed queue entry was an action, so remove its tracking entry from
|
||||||
|
`MotionState`).
|
||||||
|
- `motionID = entry.Motion`; `PhysicsObj.MotionDone(motionID, success)` — fires the
|
||||||
|
completion callback chain (→ `MovementManager.MotionDone` → `MotionInterpreter.MotionDone`,
|
||||||
|
see note at top: this is the OTHER pending-motion tracker, decoupled from this one except by
|
||||||
|
sharing the `PhysicsObj` "owner").
|
||||||
|
- `AnimationCounter -= entry.NumAnims` (consume the threshold — any EXCESS hooks beyond this
|
||||||
|
entry's requirement roll over to satisfy the NEXT queued entry, hence the outer `do...while`
|
||||||
|
loop can complete MULTIPLE queue entries from ONE `AnimationDone` call if `AnimationCounter`
|
||||||
|
still exceeds the next entry's `NumAnims`).
|
||||||
|
- If `PendingAnimations.First != null` → `RemoveFirst()` (dequeue the just-completed entry).
|
||||||
|
- If `PhysicsObj.WeenieObj != null` → `WeenieObj.OnMotionDone(motionID, success)` (separate
|
||||||
|
weenie-level hook, forwards to `WorldObject.HandleMotionDone` — the game-logic-facing
|
||||||
|
callback, distinct from the physics-level `PhysicsObj.MotionDone`).
|
||||||
|
- `node = PendingAnimations.First` (re-check for another completable entry).
|
||||||
|
- After the loop: if `AnimationCounter != 0 && node == null` (queue fully drained but counter
|
||||||
|
still has leftover) → **reset `AnimationCounter = 0`** (defensive clamp — discards any
|
||||||
|
leftover hook-credit once nothing remains queued, rather than carrying it forward to a future
|
||||||
|
`add_to_queue` call).
|
||||||
|
|
||||||
|
### `CheckForCompletedMotions()` — L63-85
|
||||||
|
Called from `PartArray.CheckForCompletedMotions` ← `PhysicsObj.CheckForCompletedMotions()` —
|
||||||
|
this one is POLLED (search found no evidence of a per-frame automatic caller within these two
|
||||||
|
files; likely ticked once per physics update from `PhysicsObj.UpdateObjectInternal` or similar,
|
||||||
|
not confirmed in this pass — flag for follow-up if the caller chain matters). Distinct from
|
||||||
|
`AnimationDone`: this drains any HEAD entries whose `NumAnims == 0` **already** (i.e. entries
|
||||||
|
that never needed any hook fires to complete — e.g. zero-length transitions), NOT
|
||||||
|
counter-driven:
|
||||||
|
- Loop while `PendingAnimations.First != null`:
|
||||||
|
- If `pendingAnimation.Value.NumAnims != 0` → **return** (head isn't a zero-length entry —
|
||||||
|
stop, do NOT touch `AnimationCounter`, this is purely for immediate 0-anim entries).
|
||||||
|
- Otherwise: pop the same way as `AnimationDone` (Action-bit → `RemoveActionHead`,
|
||||||
|
`PhysicsObj.MotionDone(motionID, true)` (always `success=true` here — no `success` param on
|
||||||
|
this method), remove from list, `WeenieObj.OnMotionDone(motionID, true)`.
|
||||||
|
- Continues looping (could drain several consecutive 0-`NumAnims` entries in one call).
|
||||||
|
|
||||||
|
### `PerformMovement(mvs, seq)` — L116-145
|
||||||
|
Dispatches on `mvs.Type` (`MovementStruct.Type`):
|
||||||
|
- `Table == null` → `WeenieError.NoAnimationTable`.
|
||||||
|
- **`InterpretedCommand`:** `Table.DoObjectMotion(mvs.Motion, State, seq, mvs.Params.Speed, ref
|
||||||
|
counter)`; if it returns `false` → `WeenieError.NoMtableData`. Else `add_to_queue(mvs.Motion,
|
||||||
|
counter, seq)`, return `None`.
|
||||||
|
- **`StopInterpretedCommand`:** `Table.StopObjectMotion(mvs.Motion, mvs.Params.Speed, State,
|
||||||
|
seq, ref counter)`; failure → `NoMtableData`. Success → `add_to_queue((uint)MotionCommand.Ready,
|
||||||
|
counter, seq)` (**note: queues under `MotionCommand.Ready`, NOT `mvs.Motion`** — a stop always
|
||||||
|
enqueues completion-tracking keyed to Ready, regardless of what was stopped).
|
||||||
|
- **`StopCompletely`:** `Table.StopObjectCompletely(State, seq, ref counter)` (return value
|
||||||
|
ignored), `add_to_queue((uint)MotionCommand.Ready, counter, seq)`, return `None`
|
||||||
|
unconditionally.
|
||||||
|
- **default:** `WeenieError.None` (comment `// ??` — ACE itself flags this as uncertain; other
|
||||||
|
`MovementType` values like `RawCommand`/`StopRawCommand` fall through here untouched by this
|
||||||
|
manager, presumably handled instead by `MotionInterp.PerformMovement`).
|
||||||
|
|
||||||
|
### `SetMotionTableID(mtableID)` — `Table = MotionTable.Get(mtableID); return Table != null;`
|
||||||
|
|
||||||
|
### `UseTime()` — `CheckForCompletedMotions();` — the manager's per-tick entry point (called
|
||||||
|
from `PartArray.cs:265`, itself presumably ticked from `PhysicsObj`'s per-update pass — same
|
||||||
|
follow-up caveat as above).
|
||||||
|
|
||||||
|
### `add_to_queue(motion, num_anims, sequence)` — L163-167
|
||||||
|
`PendingAnimations.AddLast(new AnimNode(motion, num_anims))`, then immediately
|
||||||
|
`remove_redundant_links(sequence)` — every enqueue triggers a redundancy pass over the WHOLE
|
||||||
|
list (not just checking the new tail against its immediate predecessor blindly; see below).
|
||||||
|
|
||||||
|
### `initialize_state(sequence)` — L169-177
|
||||||
|
`numAnims = 0`; if `Table != null` → `Table.SetDefaultState(State, sequence, ref numAnims)`.
|
||||||
|
Always `add_to_queue((uint)MotionCommand.Ready, numAnims, sequence)` regardless of whether
|
||||||
|
`SetDefaultState` succeeded (numAnims stays 0 on failure, so this enqueues a same-tick-complete
|
||||||
|
Ready entry).
|
||||||
|
|
||||||
|
### `remove_redundant_links(sequence)` — L179-205
|
||||||
|
Walks `PendingAnimations` from **tail to head** (`node = PendingAnimations.Last`, then
|
||||||
|
`node.Previous`). For each entry with `NumAnims != 0`:
|
||||||
|
- If the entry's motion does NOT have the SubState bit set, OR it DOES have the Modifier bit set
|
||||||
|
(i.e. it's a Style/Action/Modifier-class entry, not a plain substate cycle): only proceed if
|
||||||
|
it also has the Style bit set (`entry.Motion & CommandMask.Style`), else **return** (stop
|
||||||
|
scanning entirely — non-style, non-substate-eligible entries block further redundancy
|
||||||
|
checking). If Style-bit set: call `remove_redundant_links_inner(node, sequence, first: true)`;
|
||||||
|
if it returns `true` → **return** (done, something was truncated).
|
||||||
|
- Else (a plain substate-cycle entry): `remove_redundant_links_inner(node, sequence, first:
|
||||||
|
false)`; if `true` → return.
|
||||||
|
- Otherwise continue to `node = node.Previous` (walk further back toward the head).
|
||||||
|
|
||||||
|
### `remove_redundant_links_inner(node, sequence, first)` — L207-231
|
||||||
|
Scans BACKWARD from `node.Previous` looking for an earlier entry with the SAME `Motion` value:
|
||||||
|
- `motion = first ? 0x70000000 : 0xB0000000` — these are raw `CommandMask`-shaped bit patterns
|
||||||
|
used as an early-abort guard (0x70000000 covers Style|SubState|Action bits per typical AC
|
||||||
|
command-mask layout; 0xB0000000 a different combination — exact bit semantics live in
|
||||||
|
`CommandMask` enum, not read this pass, but functionally: differing abort masks depending on
|
||||||
|
whether we're scanning for a Style-class or SubState-class duplicate).
|
||||||
|
- While walking back (`prev = prev.Previous` each iteration):
|
||||||
|
- If `prevEntry.Motion == entry.Motion` AND (`first` is true, OR `prevEntry.NumAnims != 0`) →
|
||||||
|
found a duplicate → `trancuate_animation_list(prev, sequence)`, return `true`.
|
||||||
|
- Else if `prevEntry.NumAnims != 0 && (prevEntry.Motion & motion) != 0` → an intervening entry
|
||||||
|
of the "abort mask" class with pending anims blocks the search → return `true` WITHOUT
|
||||||
|
truncating (stops the outer loop but did nothing — prevents redundancy removal across a
|
||||||
|
still-animating Style/Action boundary).
|
||||||
|
- Else continue.
|
||||||
|
- If the walk exhausts (`prev == null`) without matching → return `false` (caller's outer loop
|
||||||
|
continues scanning further back from the ORIGINAL node).
|
||||||
|
|
||||||
|
### `trancuate_animation_list(node, sequence)` — L233-249 (note: "trancuate" — misspelling of
|
||||||
|
"truncate" preserved verbatim from ACE source, matches the mismatched name used at the call
|
||||||
|
site `remove_redundant_links_inner`)
|
||||||
|
Walks from `PendingAnimations.Last` backward toward (but not including) `node`: sums every
|
||||||
|
visited entry's `NumAnims` into `totalAnims`, then **zeroes each entry's `NumAnims` in place**
|
||||||
|
(`entry.Value.NumAnims = 0` — does NOT remove them from the list, just neuters their
|
||||||
|
completion-tracking contribution). Finally `sequence.remove_link_animations(totalAnims)` — tells
|
||||||
|
the `Sequence` to physically drop that many trailing "link" animations from its playback queue
|
||||||
|
(the actual anim-clip splice), while `PendingAnimations` keeps the now-inert `AnimNode` entries
|
||||||
|
in place with `NumAnims=0` (they'll be silently skipped/instantly-completed by
|
||||||
|
`CheckForCompletedMotions`'s 0-check, or bypass `AnimationDone`'s counter entirely since their
|
||||||
|
threshold is unreachable-but-trivially-satisfied at 0... actually re-check: `AnimationDone`'s
|
||||||
|
break condition is `entry.NumAnims > AnimationCounter`; with `NumAnims == 0` this is never true,
|
||||||
|
so a zeroed entry always immediately qualifies for completion processing on the NEXT
|
||||||
|
`AnimationDone` call, consistent with `CheckForCompletedMotions` also draining 0-count heads).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MotionDone hook chain (full call graph, both trackers)
|
||||||
|
|
||||||
|
```
|
||||||
|
PhysicsObj.Hook_AnimDone() [per-frame anim-hook fire from Sequence/AFrame dispatch]
|
||||||
|
-> PartArray.AnimationDone(true)
|
||||||
|
-> MotionTableManager.AnimationDone(true) [drains PendingAnimations by counter]
|
||||||
|
-> PhysicsObj.MotionDone(motionID, success)
|
||||||
|
-> MovementManager.MotionDone(motion, success)
|
||||||
|
-> MotionInterpreter.MotionDone(success) [SEPARATE tracker: MotionInterp.PendingMotions]
|
||||||
|
-> PhysicsObj.WeenieObj.OnMotionDone(motionID, success)
|
||||||
|
-> WorldObject.HandleMotionDone(motionID, success) [game-logic level]
|
||||||
|
|
||||||
|
PhysicsObj.CheckForCompletedMotions() [polled, presumably per physics tick]
|
||||||
|
-> PartArray.CheckForCompletedMotions()
|
||||||
|
-> MotionTableManager.CheckForCompletedMotions() [drains 0-NumAnims heads only]
|
||||||
|
-> (same PhysicsObj.MotionDone / WeenieObj.OnMotionDone chain as above)
|
||||||
|
|
||||||
|
MotionInterp.MotionDone(success) [the OTHER path — NOT reached via MotionTableManager]
|
||||||
|
-> pops MotionInterp.PendingMotions.First
|
||||||
|
-> if Action bit set: PhysicsObj.unstick_from_object(), InterpretedState.RemoveAction(),
|
||||||
|
RawState.RemoveAction()
|
||||||
|
-> PhysicsObj.IsAnimating = PendingMotions.Count > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key divergence risk for acdream:** `MovementManager.MotionDone` ALWAYS routes to
|
||||||
|
`MotionInterpreter.MotionDone`, never to `MotionTableManager`. The two trackers are fed by
|
||||||
|
DIFFERENT owners (`PartArray.MotionTableManager` vs `MovementManager.MotionInterpreter`) and
|
||||||
|
`PhysicsObj.MotionDone(motion, success)` only calls the `MovementManager` one
|
||||||
|
(`PhysicsObj.cs:899-902`). `MotionTableManager.AnimationDone`/`CheckForCompletedMotions` DO call
|
||||||
|
`PhysicsObj.MotionDone` internally (feeding MotionInterp), but nothing in these two files calls
|
||||||
|
BACK from `MotionInterp` into `MotionTableManager` — i.e. `MotionTableManager` is upstream of
|
||||||
|
`MotionInterp` in the completion-notification chain, not a peer. When porting to acdream's
|
||||||
|
`CMotionInterp`-based unification (per the D6.2a work already landed), confirm which ACE class
|
||||||
|
maps to which acdream responsibility — likely `MotionTableManager` ≈ acdream's discrete-command
|
||||||
|
completion queue (attached per-PartArray/per-object), `MotionInterp` ≈ acdream's
|
||||||
|
`CMotionInterp`-normalized local-player path (attached per-MovementManager). Do not assume they
|
||||||
|
merge into one queue — ACE keeps them structurally separate even though both trace back to the
|
||||||
|
same `PhysicsObj`.
|
||||||
|
|
||||||
|
## Flagged ACE-side oddities (verify against 2013 retail decomp before porting verbatim)
|
||||||
|
|
||||||
|
1. **Action-branch numAnims miscalculation** (`GetObjectSequence` L227): fallback Action path
|
||||||
|
sums `motionData.Anims.Count` TWICE instead of once for `motionData` and once for
|
||||||
|
`motionData_` — looks like a copy-paste error in ACE's port, not necessarily retail-faithful.
|
||||||
|
2. **`change_cycle_speed` silent no-op gap** (L372-379): when `substateMod ≈ 0` AND `speedMod`
|
||||||
|
is nonzero, neither branch fires — no framerate adjustment applied. Check retail's
|
||||||
|
`change_cycle_speed` equivalent for a third branch.
|
||||||
|
3. **`GetLinkData` mask width inconsistency** (L562): uses `& 0xFFFF` where every other
|
||||||
|
motion-ID mask in this file uses `& 0xFFFFFF`. Likely harmless (Ready's ID fits in 16 bits)
|
||||||
|
but inconsistent.
|
||||||
|
4. **`StopObjectCompletely` return-value semantics** (L293-313): returns `true` unless the FINAL
|
||||||
|
substate-stop call fails, in which case it falls back to whatever `success` was from the
|
||||||
|
modifier-draining loop — easy to misport if simplified to a single boolean accumulate.
|
||||||
|
5. **`re_modify` double-removal from both `pstate` and a snapshot `state`** (L440-458): only
|
||||||
|
makes sense if `MotionState`'s copy constructor shares the underlying `LinkedList<Motion>`
|
||||||
|
nodes with the source, which would make the second `state.remove_modifier(...)` call either
|
||||||
|
redundant or (if lists are NOT shared) load-bearing to unlink from the snapshot's OWN list —
|
||||||
|
not verifiable without reading `MotionState.cs`'s copy ctor (out of scope this pass).
|
||||||
|
|
||||||
|
## Files NOT read this pass (would need separate grep if pursued)
|
||||||
|
- `MotionState.cs` (referenced constantly: `.Modifiers`, `.add_modifier`,
|
||||||
|
`.add_modifier_no_check`, `.remove_modifier`, `.clear_modifiers`, `.add_action`,
|
||||||
|
`.remove_action_head`, `.clear_actions`, copy constructor semantics)
|
||||||
|
- `MotionInterp.cs` full file (only L190-260 read; `PendingMotions`, `enter_default_state`,
|
||||||
|
`apply_current_movement`, `get_leave_ground_velocity` not traced)
|
||||||
|
- `Sequence.cs` (`SetVelocity`, `SetOmega`, `CombinePhysics`, `subtract_physics`,
|
||||||
|
`append_animation`, `clear_physics`, `remove_cyclic_anims`, `clear_animations`,
|
||||||
|
`remove_link_animations`, `multiply_cyclic_animation_framerate`, `HasAnims`,
|
||||||
|
`remove_all_link_animations`)
|
||||||
|
- `CommandMask` enum exact bit values (Style/SubState/Action/Modifier)
|
||||||
|
- `WeenieObject.HandleMotionDone` in `AC.Server` (game-logic side, outside Physics/)
|
||||||
1603
docs/research/2026-07-02-r2-motiontable/r2-motiontable-decomp.md
Normal file
1603
docs/research/2026-07-02-r2-motiontable/r2-motiontable-decomp.md
Normal file
File diff suppressed because it is too large
Load diff
299
docs/research/2026-07-02-r2-motiontable/r2-port-plan.md
Normal file
299
docs/research/2026-07-02-r2-motiontable/r2-port-plan.md
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
# R2 port work-list — GetObjectSequence + MotionTableManager
|
||||||
|
|
||||||
|
Inputs: `r2-motiontable-decomp.md` (verbatim retail extraction, this scratchpad),
|
||||||
|
`r2-ace-motiontable.md` (ACE cross-ref, this scratchpad), plan of record
|
||||||
|
`docs/plans/2026-07-02-retail-motion-animation-rewrite.md` (stage R2), R1 gap map
|
||||||
|
`docs/research/2026-07-02-r1-csequence/r1-gap-map.md`, R1 core
|
||||||
|
`src/AcDream.Core/Physics/Motion/{CSequence,AnimSequenceNode,FrameOps}.cs`, adapter
|
||||||
|
`src/AcDream.Core/Physics/AnimationSequencer.cs`, S2b sink
|
||||||
|
`src/AcDream.App/Rendering/RemoteMotionSink.cs`.
|
||||||
|
|
||||||
|
**Precondition / state at R2 start:** R1 commits P0–P4 are landed (`1371c2a1`,
|
||||||
|
`778744bf`, `5138b8fb`, `658b91d8`); the P5 adapter cutover is IN the working tree
|
||||||
|
(AnimationSequencer already rehosted on `_core: CSequence`) but **uncommitted**, and
|
||||||
|
P6 (root-motion wiring + register sweep) is not started. **R2 depends on P5+P6 being
|
||||||
|
committed first.** Every CSequence primitive R2 needs already exists in the R1 core:
|
||||||
|
`AppendAnimation` (first_cyclic-slides, G10), `RemoveCyclicAnims`,
|
||||||
|
`RemoveLinkAnimations(count)`, `RemoveAllLinkAnimations`, `ClearPhysics`,
|
||||||
|
`Combine/SubtractPhysics`, `MultiplyCyclicAnimationFramerate` (framerates-only, G13),
|
||||||
|
`HasAnims`, and the `IAnimHookQueue.AddAnimDoneHook` seam (G5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Decomp ambiguities to pin BEFORE porting (the Q0 pseudocode commit)
|
||||||
|
|
||||||
|
| # | Ambiguity | Evidence each way | Pin method |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | **`get_link` branch predicate is likely INVERTED in the BN extraction.** r2-motiontable-decomp §4 reads "both speed_mods negative → branch B (swap keys)". But under that reading the NORMAL forward case (both positive) takes branch A = outer hash keyed by **to**-substate, which contradicts (a) the dat Links layout the working adapter uses (outer key = `(style<<16)\|fromSubstate`, `AnimationSequencer.cs:989`), (b) ACE `MotionTable.cs:395-426` ("EITHER negative → reversed keys"), and (c) the call sites' arg roles (`get_link(style, currentSubstate, mod, targetSubstate, speed)`). If the predicate is "**either** negative → swapped-key branch", retail branch A == ACE's reverse branch and branch B == ACE's forward branch, and BOTH fallback blocks align exactly (either-neg fallback = style_defaults hop = ACE reverse fallback; both-pos fallback = `links[style<<16][to]` = ACE forward fallback). Same x87-flag-noise class as `same_sign` (§2) and the r1 gap-map hook-direction fix. | BN §4 vs ACE L395-426 + adapter GetLink (user-validated in the field: the reversed-key branch fixed the "left leg twitches" Ready→WalkBackward glitch) | Re-read the raw pseudo-C at @298552 for the flag test; if still ambiguous, cdb bp `acclient!CMotionTable::get_link` logging args + which hash bucket is probed during a Ready→WalkBackward start vs Ready→WalkForward start. Default resolution if cdb unavailable: ACE's reading (three independent corroborations). |
|
||||||
|
| A2 | **Branch-2 `signedSpeed` block** (no-double-hop leg): decomp cleans it as `(substate_mod==0 \|\| same_sign(substate_mod,arg5)) ? arg5 : -arg5` (§5 line ~550); ACE flips only one direction (`SubstateMod < 0 && speedMod > 0 → -speedMod`, MotionTable.cs:155-166). | BN x87 noise vs ACE asymmetry | Re-read raw @~0x00522bXX; conformance test both readings against a cdb golden of a Walk(-)→Walk(+) flip. |
|
||||||
|
| A3 | **outTicks field decode**: BN renders one term as `arg3->action_head` — actually MotionData's packed `num_anims` byte (decomp's own note, §5). All outTicks arithmetic = sum of each appended MotionData's `num_anims` (link chains) [+ base-cycle num_anims in Branch 1/2] − 1. | decomp note + ACE numAnims sums | Textual pin only; assert in tests: outTicks(walk→run direct) == link.num_anims + cycle.num_anims − 1. |
|
||||||
|
| A4 | **Adjudicate the 5 flagged ACE oddities** (r2-ace-motiontable "Flagged ACE-side oddities") against the decomp: (1) Action-branch numAnims double-count (ACE L227) — retail sums `outHop + actionLink [+ returnHop]` (never the base cycle, never one MotionData twice, §5 Branch 3) → **ACE bug, do not copy**; (2) `change_cycle_speed` silent no-op when old≈0 & new≠0 — retail §2 has the SAME structure → **port verbatim including the gap** (it's retail); (3) `GetLinkData` 0xFFFF mask — ACE-only helper, not ported; (4) `StopObjectCompletely` return = `finalStopOk ? 1 : anyModifierStopOk` (§9) — port verbatim; (5) `re_modify` snapshot double-removal — retail copy-ctor deep-copies the chains, the snapshot exists ONLY as the loop-termination bound (§6) → C# port: deep-copy MotionState, pop both, terminate on snapshot empty. | — | Documented adjudications in the pseudocode doc. |
|
||||||
|
| A5 | **DatReaderWriter `MotionData.Bitfield` availability** — `is_allowed` needs bit1 ("substate-gated") and Branches 1/2/4 need bit0 ("clear modifiers on entry"). `Bitfield` string is present in the DLL (`chorizite.datreaderwriter/1.0.0`); confirm it's on `Types.MotionData` (not just GfxObj etc.) at Q2 time. | DLL strings grep | One-line test against the Humanoid table (a known bit-2-gated cycle, e.g. TurnRight). |
|
||||||
|
|
||||||
|
**Q0 cdb capture (one session serves all of R2):** bp
|
||||||
|
`CMotionTable::GetObjectSequence` / `get_link` / `StopSequenceMotion`,
|
||||||
|
`MotionTableManager::add_to_queue` / `remove_redundant_links` /
|
||||||
|
`truncate_animation_list` / `AnimationDone` / `CheckForCompletedMotions` with arg+ret
|
||||||
|
logging (pattern: `tools/cdb/l2g-observer.cdb`), user protocol: walk / run / shift-toggle
|
||||||
|
(fast path) / backward / turn-in-place / run-while-turning / sidestep / emote-while-running
|
||||||
|
/ attack / stop / stance change / rapid W-tapping (redundant-link collapse) / jump.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ITEMIZED GAPS — current vs retail (R2 scope)
|
||||||
|
|
||||||
|
Severity: **BLOCKER** = R2's conformance harness meaningless without it; **HIGH** =
|
||||||
|
visible animation wrongness / blocks R3; **MED** = edge-visible; **LOW** = textual.
|
||||||
|
|
||||||
|
| # | Retail behavior acdream lacks/diverges on | Retail anchor | Current-code anchor | Severity |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| H1 | **No `CMotionTable::GetObjectSequence` — motion selection is a partial adapter hybrid.** Retail (0x00522860, decomp §5) is one dispatcher with 4 class branches (style `(int)id<0` / cycle `0x40000000` / action `0x10000000` / modifier `0x20000000`), plain-OR fallthrough, `is_allowed` gating, style-default double-hop link routing, modifier replay (`re_modify`), and outTicks. acdream's `SetCycle` implements only a subset of Branch 2 and its inventions (below) stand in for the rest. | decomp §5 @298636 | `AnimationSequencer.SetCycle` (:321-636), `PlayAction` (:791-888) | **BLOCKER** |
|
||||||
|
| H2 | **No `MotionState`** — no style/substate/substate_mod struct, no modifier stack (push-front `add_modifier_no_check` / dup-guarded `add_modifier` / `remove_modifier(node,prev)` / `clear_modifiers`), no action FIFO (`add_action` tail-append / `remove_action_head`). The adapter's `CurrentStyle/CurrentMotion/CurrentSpeedMod` is a 3-field flat approximation; modifiers/actions have no bookkeeping at all. | acclient.h:31081; decomp §13/§14 (all 9 members, 0x00525fd0-0x00526340) | `AnimationSequencer.cs` CurrentStyle/CurrentMotion/CurrentSpeedMod props; `RemoteMotionSink.cs:39-45` (per-UM axis duplicates) | **BLOCKER** |
|
||||||
|
| H3 | **No `MotionTableManager`** — no `pending_animations` DLList, no `animation_counter`, no `add_to_queue` (0x0051bfe0), no `remove_redundant_links` (0x0051bf20, 0xb0000000/0x70000000 block masks), no `truncate_animation_list` (0x0051bca0, zero-in-place + `remove_link_animations(ticks)`), no `AnimationDone` (0x0051bce0, counter-driven countdown-chain multi-pop + action-head pop + MotionDone), no `CheckForCompletedMotions`/`UseTime` (0x0051be00/0x0051bfd0, zero-tick sweep, success=1 hardcoded), no `initialize_state` (0x0051c030, `0x41000003` sentinel), no `HandleEnterWorld/HandleExitWorld` drains, no `PerformMovement` (0x0051c0b0, error codes 7/0x43/0). | decomp §11 (all 16 members) | nothing — the AnimationDoneSentinel the R1 core queues (CSequence.cs:413-418 → AnimationSequencer.cs:925) is consumed by NOBODY (gap map API table: "RemoteMotionSink/GameWindow don't consume AnimDone yet") | **BLOCKER** |
|
||||||
|
| H4 | **RemoteMotionSink's single-cycle pick** — axis collection (`_substate/_sidestep/_turn`), priority pick (fwd > sidestep > turn), `Commit()` one-SetCycle resolution. Retail: each funnel dispatch is its own `DoObjectMotion` → GetObjectSequence; turn/sidestep-while-moving resolve via `is_allowed` rejection of the (bitfield&2-gated) turn cycle → Branch 4 modifier combine (physics-only overlay), run cycle keeps playing; turn-in-place resolves via Branch 2 (cycle exists, allowed from the style default). Register row **AP-73** (retail-divergence-register.md:181). | decomp §3 (is_allowed) + §5 Branch 2/4 | `RemoteMotionSink.cs:141-216` (`Commit`), `:55-119` (`ApplyMotion` classify/collect) | **HIGH** — plan of record: "single-cycle pick DELETED — GetObjectSequence decides" |
|
||||||
|
| H5 | **HasCycle probe + Run→Walk→Ready missing-cycle fallback chain.** Existed because SetCycle clears the cyclic tail BEFORE knowing the cycle resolves ("torso on the ground"). Retail never has the problem: GetObjectSequence resolves ALL MotionData first, `clear_physics`/`remove_cyclic_anims` happen only inside a success path, missing cycles retry under `default_style` (Branch 2 second lookup) and otherwise `return 0` leaving the sequence untouched (PerformMovement → 0x43). | decomp §5 Branch 2 lines ~494-505 + return-0 tail | `RemoteMotionSink.cs:169-204`, `AnimationSequencer.HasCycle` (:281-295), GameWindow spawn fallbacks :3723-3825 | **HIGH** |
|
||||||
|
| H6 | **Fix B locomotion link-skip** (cyclic→cyclic transitions call `RemoveAllLinkAnimations` for the locomotion low-byte subset). Retail mechanism = `remove_redundant_links` on the PENDING QUEUE (tail-anchored backward scan for an earlier same-motion node; collapse via `truncate_animation_list` → zero ticks + `remove_link_animations(removedTicks)`; blocked by intervening non-zero 0xb0000000-class (cycle-tail scan) / 0x70000000-class (style-tail scan) nodes). The 2026-05-03 cdb trace in the Fix B comment block is the golden: cyclic→cyclic = `add_to_queue(45000005)` + `add_to_queue(44000007)`, truncate NOT firing — Fix B's outcome falls out of retail's structure (the link nodes the queue would truncate never get double-enqueued once GetObjectSequence owns link selection). | decomp §11 remove_redundant_links @290771 + truncate @290533 | `AnimationSequencer.cs:468-514` (Fix B block + `IsLocomotionCycleLowByte`) | **HIGH** — delete in favor of the queue mechanism |
|
||||||
|
| H7 | **Stop-anim fallback** (SetCycle low-byte remap WalkBackward→WalkForward etc. when linkData null). Retail: `adjust_motion` normalization happens UPSTREAM in CMotionInterp (already ported, D6.2a `0f099bb6`) so GetObjectSequence receives 0x05/0x0D/0x0F + signed speed; direction flips route via Branch 2's `link==null \|\| !same_sign` style-default double-hop (`get_link(...,styleDefault,1f)` + `get_link(styleDefault,1f,target,speed)`). | decomp §5 Branch 2 lines ~533-540 | `AnimationSequencer.cs:402-423` | **HIGH** |
|
||||||
|
| H8 | **Adapter fast path ≠ retail fast path.** Retail (§5 Branch 2): gate = `target==substate && same_sign(newSpeed, substate_mod) && has_anims()`, then `change_cycle_speed` (ratio, 0.0002f epsilon guards) + `subtract_motion(old)` + `combine_motion(new)` + commit substate_mod. acdream: gate keyed on Current* fields with its own 1e-4/1e-6 epsilons, and `MultiplyCyclicFramerate` folds a velocity/omega rescale composite in (the G13 stand-in, `AnimationSequencer.cs:673-686`). | decomp §5 lines ~513-522, §2 change_cycle_speed @298276 | `AnimationSequencer.cs:345-388`, `:673-686` | **HIGH** — G13/G17 retire here |
|
||||||
|
| H9 | **`add_motion` velocity gate (G17 adapter half).** Core `EnqueueMotionData` still gates `SetVelocity/SetOmega` on `MotionDataFlags.HasVelocity/HasOmega`; retail `add_motion` (0x005224b0) sets unconditionally (dat-silent MotionData carries zero → replace-with-zero), safe once modifiers route through `combine_motion` (Branch 4) instead of `add_motion`. | decomp §2 @298437 | `AnimationSequencer.cs:1044-1058` (documented "R2 stand-in") + `PlayAction:836-839,872-875` | MED — mechanical once Branch 4 exists |
|
||||||
|
| H10 | **PlayAction inventions**: (a) actions resolved via `GetLink` direct only — no default-substate out-and-back 4-layer chain (outHop@1.0 → actionLink@speed → returnHop@1.0 → base cycle@old substate_mod); (b) no `add_action` FIFO bookkeeping (so no completion pop, H3); (c) no `clear_physics`/`remove_cyclic_anims` rebuild — nodes are INSERTED before the cyclic tail + an invented cursor-jump (`AnimationSequencer.cs:877-887`); retail REBUILDS (base cycle restarts after the action); (d) modifier-class ids ENQUEUE ANIMS from the Modifiers dict — retail Branch 4 is **physics-only** (`combine_motion` velocity/omega; anims untouched) + `add_modifier` bookkeeping with the stop-then-re-add toggle. | decomp §5 Branch 3 (~591-647) / Branch 4 (~652-687) | `AnimationSequencer.PlayAction` :791-888 | **HIGH** |
|
||||||
|
| H11 | **No stop machinery**: `StopSequenceMotion` (0x00522fc0: cycle-stop = re-drive GetObjectSequence toward the style default with arg7=1; modifier-stop = `subtract_motion` + unlink), `StopObjectCompletely` (strip all modifiers then stop the substate), `SetDefaultState` (0x005230a0: full baseline reset, `clear_animations` not remove_cyclic). "Stop" today is whoever calls SetCycle(Ready 0x41000003). | decomp §7/§8/§9 | funnel `StopMotion` → `RemoteMotionSink.cs:121-131` (ObservedOmega zero only); GameWindow SetCycle(Ready) sites | **BLOCKER** for stop conformance |
|
||||||
|
| H12 | **The GetObjectSequence guard set**: entry guards (`style==0 \|\| substate==0` → 0), the modifier-class no-op fast path (`target==styleDefault && !stopCall && (substate & 0x20000000)` → 1), Branch 2's "leaving a modifier-class substate re-registers it as a modifier" (`add_modifier_no_check`) — none exist anywhere in acdream. | decomp §5 lines ~384-404, ~565-572 | — | HIGH (part of H1, called out because each is an easy silent omission) |
|
||||||
|
| H13 | **`re_modify` modifier replay across transitions** — every substate/style-changing branch replays the active modifier stack through recursive GetObjectSequence calls. This is DEV-9 / AP-73's "retail BLENDS modifiers over the substate cycle". | decomp §6 @298300 | — | **HIGH** — the plan of record's "(modifier blend — retires AP-73)" |
|
||||||
|
| H14 | **Style-change transitions absent** (Branch 1): stance switches never play the exit-link + style-to-style link + double-hop-via-default_style chain; RemoteMotionSink just stores `_style` and SetCycle keys the cycle dict with it. | decomp §5 Branch 1 (~411-487) | `RemoteMotionSink.cs:57-61`; `SetCycle` (style used only as key material) | MED-HIGH (visible on combat-stance changes) |
|
||||||
|
| H15 | **Spawn baseline**: retail `initialize_state` → `SetDefaultState` + queue `0x41000003` sentinel; enter/exit-world drain the queue (`AnimationDone(0)` loop; enter also `remove_all_link_animations`). acdream: RenderBootstrap 3-tier fallback + GameWindow SetCycle(Ready)/HasCycle chains. | decomp §11 initialize_state/HandleEnterWorld/HandleExitWorld | GameWindow :3723-3825; `RenderBootstrap.SequencerFactory` | MED |
|
||||||
|
| H16 | **MotionDone signal dead-ends.** Retail chain: CSequence AnimDone gate → AnimDoneHook singleton → `Hook_AnimDone` → `CPartArray::AnimationDone(1)` → `MotionTableManager::AnimationDone` → countdown pop → `CPhysicsObj::MotionDone(motion, success)` → (R3) CMotionInterp pending_motions. acdream stops at `AnimationDoneSentinel` in `_pendingHooks`; nothing counts it. | decomp §11 AnimationDone @290558 + gap map G5 | `AnimationSequencer.cs:918-931`; GameWindow :9882 drain ignores the sentinel type | **HIGH** — R2's named deliverable; see §4 below |
|
||||||
|
| H17 | **ObservedOmega side-write** — the sink seeds `RemoteMotion.ObservedOmega` from wire turns (`RemoteMotionSink.cs:95-101`) so remote rotation starts same-tick. Retail: turn omega enters the sequence via Branch 2 add_motion (turn cycle omega + adapter synthesis) or Branch 4 combine_motion, and body rotation comes from `CSequence.omega` through apply_physics per tick (R6 tick-order territory). | decomp §2 combine_motion; plan R6 | `RemoteMotionSink.cs:95-101`, `:127` | MED — carry the side-write into the replacement sink verbatim (register row), retire in R6 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. KEEP LIST — already matching retail (do not re-port)
|
||||||
|
|
||||||
|
| Behavior | Retail anchor | acdream anchor |
|
||||||
|
|---|---|---|
|
||||||
|
| `get_link` two-branch sign-aware lookup + both fallbacks (pending A1 pin, which almost certainly CONFIRMS it) | 0x00522710 §4 (predicate per A1) | `AnimationSequencer.GetLink` :961-1005 — re-home into CMotionTable verbatim, do not rewrite |
|
||||||
|
| Cycle/link/modifier hash keying incl. 32-bit `<<16` truncation of full command words (`(0x8000003D<<16)==0x003D0000`) | §5 key math throughout | `SetCycle`/`HasCycle`/`PlayAction` key builds (`:426,293,824`) — carry the full-command-word convention into CMotionTable |
|
||||||
|
| `AnimData` speed scaling: framerate only (`AnimData::operator*`) | 0x00525d00 (r1 §25) | `BuildNode` :1014-1024 / `EnqueueMotionData` append path |
|
||||||
|
| `append_animation` first_cyclic-slides-to-tail + curr_anim seed (the structural base every add_motion depends on) | 0x00525510 (r1 §24) | `CSequence.AppendAnimation` (Motion/CSequence.cs:109-123) |
|
||||||
|
| Full remove-family + apricot + combine/subtract_physics + multiply_cyclic_animation_fr (framerates-only) | r1 §5-§14 | `CSequence` :163-253 — R2 calls these, zero changes |
|
||||||
|
| AnimDone LIST-STRUCTURE gate (head != first_cyclic) + IAnimHookQueue seam | 0x00525943 (r1 G5) | `CSequence.UpdateInternal` :407-422 — R2 only adds the CONSUMER |
|
||||||
|
| adjust_motion normalization upstream of dispatch (left→right / backward→forward + sign) | CMotionInterp (D6.2a port) | `MotionInterpreter` normalization (commit `0f099bb6`) — stays upstream; Q4 verifies single-site and deletes the SetCycle-head duplicate |
|
||||||
|
| Inbound funnel: `MoveToInterpretedState` / `ApplyInterpretedMovement` / `DispatchInterpretedMotion` / `contact_allows_move` + 183-case conformance suite | S2a (7b0cbbda) | `MotionInterpreter.cs:1312-1420` — R2 sits BELOW it (replaces only the sink) |
|
||||||
|
| `MotionSequenceGate` (S1), `InterpolationManager` (L.3), outbound packers | plan "absorbed" list | untouched |
|
||||||
|
| K-fix18 `skipTransitionLink` instant-engage (jump/Falling) | none — invented; retires in **R3** jump family | `AnimationSequencer.cs:321,398,441-444`; GameWindow :4817-4831, :10224 — SURVIVES R2 at adapter, byte-identical, register row kept |
|
||||||
|
| Locomotion velocity synthesis (Walk 3.12 / Run 4.0 / Side 1.25 m·s⁻¹) + turn omega synthesis (π/2 rad·s⁻¹) | retail `get_state_velocity` (R3 scope) | `SetCycle` :539-635 — SURVIVES R2 (runs after PerformMovement), register rows kept |
|
||||||
|
| Retail slerp + BuildBlendedFrame render blend | FUN_005360d0 | `SlerpRetailClient` / `BuildBlendedFrame` — untouched |
|
||||||
|
| `0x41000003` == full-word MotionCommand.Ready == retail's stop/default sentinel | decomp §15 | adapter already uses 0x41000003 as the Ready id |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. COMMIT SEQUENCE — dependency-sorted, each ONE commit, tests-first
|
||||||
|
|
||||||
|
New code target: `src/AcDream.Core/Physics/Motion/` (plan rule 4). Tests:
|
||||||
|
`tests/AcDream.Core.Tests/Physics/Motion/` (pattern: R1's
|
||||||
|
`AnimSequenceNodeTests`/`CSequenceTests`). Every commit: build+test green, register
|
||||||
|
rows added/retired in-commit.
|
||||||
|
|
||||||
|
**Q0 — pseudocode + ambiguity pinning (docs only).**
|
||||||
|
`docs/research/2026-07-0x-motiontable-pseudocode.md` from r2-motiontable-decomp.md,
|
||||||
|
resolving A1–A5 (§0 above) and adjudicating the 5 ACE oddities (A4). Run the ONE cdb
|
||||||
|
capture session (protocol in §0) — it feeds Q2/Q3/Q4 goldens.
|
||||||
|
Fixture source: **cdb** (live retail, l2g-observer.cdb pattern).
|
||||||
|
Deps: none (R1-P5/P6 committed is a precondition for Q4+, not Q0).
|
||||||
|
|
||||||
|
**Q1 — `MotionState` verbatim.** (closes H2)
|
||||||
|
`Motion/MotionState.cs`: `Style/Substate/SubstateMod(=1f)` + modifier STACK
|
||||||
|
(`AddModifierNoCheck` push-front 0x00525ff0; `AddModifier` dup-guard + `substate==id`
|
||||||
|
refuse 0x00526340; `RemoveModifier(node, prev)` 0x00526040; `ClearModifiers`
|
||||||
|
0x00526070) + action FIFO (`AddAction` tail-append 0x005260a0; `RemoveActionHead`
|
||||||
|
0x00526120; `ClearActions` 0x005260f0) + deep-copy ctor (A4-#5: chains copied, not
|
||||||
|
shared — re_modify's snapshot is a termination bound).
|
||||||
|
Tests first: stack-vs-FIFO discipline tables; AddModifier rejection (already-present /
|
||||||
|
equals-substate); copy independence (mutate original, snapshot unchanged).
|
||||||
|
Fixture source: **synthetic**.
|
||||||
|
Deps: Q0.
|
||||||
|
|
||||||
|
**Q2 — `CMotionTable` verbatim (pure selection logic, no queue).** (closes H1, H5-resolve-side, H7-routing, H8-core, H10-core, H11, H12, H13, H14; A1/A2/A5 land here)
|
||||||
|
`Motion/CMotionTable.cs` wrapping the DatReaderWriter `MotionTable` DBObj
|
||||||
|
(style_defaults/cycles/modifiers/links/default_style; keys = `(style<<16)|(id&0xFFFFFF)`
|
||||||
|
full-command-word convention). Free functions in the same file (retail free fns):
|
||||||
|
`add_motion` 0x005224b0 (**unconditional** SetVelocity/SetOmega — G17 core — +
|
||||||
|
AppendAnimation per speed-scaled AnimData), `combine_motion` 0x00522580 /
|
||||||
|
`subtract_motion` 0x00522600 (CombinePhysics/SubtractPhysics only — never anims),
|
||||||
|
`change_cycle_speed` 0x00522290 (0.0002f epsilons, verbatim incl. the A4-#2 gap),
|
||||||
|
`same_sign` 0x00522260. Members: `get_link` 0x00522710 (per A1 pin — expected: the
|
||||||
|
adapter's existing port re-homed), `is_allowed` 0x005226c0 (`Bitfield & 2` gate),
|
||||||
|
`GetObjectSequence` 0x00522860 (ALL of: entry guards; modifier-class no-op fast path;
|
||||||
|
Branch 1 style-change with exit-link + direct link + default_style double-hop + commit
|
||||||
|
+ re_modify + outTicks; Branch 2 with default_style cycle retry, is_allowed, re-speed
|
||||||
|
fast path (change_cycle_speed + subtract + combine), clear-modifiers bit0, direct-link
|
||||||
|
vs `!same_sign` double-hop, A2 signedSpeed, outgoing-modifier-substate re-registration,
|
||||||
|
re_modify, outTicks; Branch 3 action direct + 4-layer out-and-back, add_action,
|
||||||
|
outTicks; Branch 4 modifier physics-only combine + stop-then-re-add toggle),
|
||||||
|
`re_modify` 0x005222e0, `StopSequenceMotion` 0x00522fc0, `SetDefaultState` 0x005230a0
|
||||||
|
(`clear_animations` hard reset), `DoObjectMotion`/`StopObjectMotion`/
|
||||||
|
`StopObjectCompletely` 0x00523e90/ec0/ed0 (A4-#4 return semantics).
|
||||||
|
Tests first — the R2 conformance harness core:
|
||||||
|
(a) **dat fixtures** (Humanoid MotionTable via DatCollection, R1-P1 pattern):
|
||||||
|
Ready→Walk link+cycle chain shape; walk↔run re-speed fast path (framerates rescaled,
|
||||||
|
velocity = subtract-old+combine-new, NO list change); Walk→WalkBackward-normalized
|
||||||
|
(0x05, −speed) sign-flip → style-default double-hop; stance change → Branch 1 chain;
|
||||||
|
emote-while-running → 4-layer out-and-back with base cycle re-added at OLD
|
||||||
|
substate_mod; turn-in-place → Branch 2 cycle; **run-while-turning → is_allowed rejects
|
||||||
|
the gated turn cycle → Branch 4 physics-only combine, run anims untouched** (the AP-73
|
||||||
|
mechanism test); modifier stop = subtract + unlink; StopObjectCompletely drains
|
||||||
|
modifiers then re-drives to style default; missing cycle → return 0, sequence
|
||||||
|
UNTOUCHED (H5); outTicks values per A3.
|
||||||
|
(b) **cdb goldens from Q0**: GetObjectSequence arg/ret + resolved-MotionData-key
|
||||||
|
conformance for the captured protocol.
|
||||||
|
Deps: Q1 (MotionState), R1 core.
|
||||||
|
|
||||||
|
**Q3 — `MotionTableManager` + pending_animations.** (closes H3, H15-core)
|
||||||
|
`Motion/MotionTableManager.cs` + `AnimNode {Motion, NumAnims}`. Fields per `Create`
|
||||||
|
0x0051bc50: table, state, animation_counter, pending queue (LinkedList<AnimNode>;
|
||||||
|
register row: managed list vs intrusive DLList — reuse the R1 AD-34 wording), plus an
|
||||||
|
`IMotionDoneSink` seam (stands in for `CPhysicsObj::MotionDone`; see §4). Methods:
|
||||||
|
`add_to_queue` 0x0051bfe0 (append + immediate `remove_redundant_links`),
|
||||||
|
`remove_redundant_links` 0x0051bf20 — **port retail's tail-anchored single scan**
|
||||||
|
(skip trailing zero-tick nodes; cycle-class-not-modifier tail: match earlier same-motion
|
||||||
|
non-zero node, blocked by intervening non-zero `0xb0000000`-class; style-class
|
||||||
|
(`(int)motion<0`) tail: exact match, blocked by non-zero `0x70000000`-class), NOT
|
||||||
|
ACE's restructured outer loop; `truncate_animation_list` 0x0051bca0 (zero `NumAnims`
|
||||||
|
in place — nodes stay queued — + `CSequence.RemoveLinkAnimations(removedTicks)`);
|
||||||
|
`AnimationDone(success)` 0x0051bce0 (counter += 1; pop every head with
|
||||||
|
`NumAnims <= counter`, action-class → `state.RemoveActionHead()`, fire
|
||||||
|
`sink.MotionDone(motion, success)`, counter −= NumAnims; drained-list counter reset);
|
||||||
|
`CheckForCompletedMotions` 0x0051be00 (zero-tick heads only, success=1, no counter
|
||||||
|
touch) + `UseTime` alias; `initialize_state` 0x0051c030 (SetDefaultState + queue
|
||||||
|
`0x41000003`/outTicks + redundancy pass); `HandleEnterWorld` (remove_all_link_animations
|
||||||
|
+ drain via AnimationDone(0)) / `HandleExitWorld` (drain only); `PerformMovement`
|
||||||
|
0x0051c0b0 (InterpretedCommand → DoObjectMotion → add_to_queue(motion, outTicks);
|
||||||
|
StopInterpretedCommand → StopObjectMotion → add_to_queue(**0x41000003**, outTicks) on
|
||||||
|
success; StopCompletely → StopObjectCompletely + **unconditional**
|
||||||
|
add_to_queue(0x41000003); error codes 7 / 0x43 / 0; other MovementTypes untouched).
|
||||||
|
Tests first: countdown-chain tables (one AnimationDone completing MULTIPLE entries via
|
||||||
|
counter rollover; leftover-counter reset on drain); truncate blocked/allowed matrices
|
||||||
|
for both masks; zero-tick sweep vs counter sweep distinction; enter/exit-world drains
|
||||||
|
fire MotionDone(success=0) for every queued motion; rapid same-motion re-issue →
|
||||||
|
collapse (the Fix B replacement proof); the Q0 golden: cyclic→cyclic walk→run yields
|
||||||
|
`add_to_queue(0x45000005)` + `add_to_queue(0x44000007)` with truncate NOT firing
|
||||||
|
(2026-05-03 trace, quoted in the Fix B comment block).
|
||||||
|
Fixture source: **synthetic** + **cdb goldens from Q0**.
|
||||||
|
Deps: Q2.
|
||||||
|
|
||||||
|
**Q4 — adapter cutover: SetCycle/PlayAction rehosted on PerformMovement; DELETE Fix B + stop-anim fallback + fast-path composite + G17 gate; wire the queue drain.** (closes H6, H7, H8, H9, H10-adapter, H16-wiring; H12 guards live via Q2)
|
||||||
|
`AnimationSequencer` gains a `MotionTableManager` (constructed with the same
|
||||||
|
DatReaderWriter table + the CSequence core). `SetCycle(style, motion, speedMod,
|
||||||
|
skipTransitionLink)` becomes: dispatch style-class id then motion through
|
||||||
|
`PerformMovement(InterpretedCommand)` (MotionState now OWNS style/substate/
|
||||||
|
substate_mod; `CurrentStyle/CurrentMotion/CurrentSpeedMod` become read-only mirrors —
|
||||||
|
GameWindow :3723/4827/4915/4919 + sink ctor keep compiling). `PlayAction` → the same
|
||||||
|
dispatch (action/modifier ids hit Branch 3/4). K-fix18 preserved byte-identical at
|
||||||
|
adapter: `skipTransitionLink` → post-dispatch `ClearAnimations`-of-links exactly as
|
||||||
|
today (register row survives → R3). Velocity/omega synthesis blocks run AFTER dispatch,
|
||||||
|
unchanged (→ R3). **DELETE:** the SetCycle-head adjust_motion duplicate (verify every
|
||||||
|
caller pre-normalizes via MotionInterpreter; if any GameWindow raw call site doesn't,
|
||||||
|
normalize at the adapter boundary ONCE and note it), the adapter fast-path block
|
||||||
|
(:345-388), the stop-anim low-byte fallback (:402-423), Fix B +
|
||||||
|
`IsLocomotionCycleLowByte` (:468-514), `MultiplyCyclicFramerate`'s velocity-rescale
|
||||||
|
composite (:681-686 — change_cycle_speed+subtract/combine are now real; G13 row
|
||||||
|
retired), `EnqueueMotionData`'s HasVelocity/HasOmega gate (:1055-1058 — add_motion
|
||||||
|
unconditional; G17 row retired), PlayAction's insert-before-tail + cursor-jump +
|
||||||
|
modifier-anim-enqueue (:836-887). **Queue drain wiring (same commit — the queue must
|
||||||
|
not grow unbounded):** GameWindow anim tick (:9876-9890) counts drained
|
||||||
|
`AnimationDoneSentinel` instances → `manager.AnimationDone(true)` per sentinel, and
|
||||||
|
calls `manager.UseTime()` once per tick (zero-tick completions: stop-with-no-link,
|
||||||
|
fast-path re-speed outTicks=0). `IMotionDoneSink` bound to a diagnostic recorder
|
||||||
|
(ACDREAM_DUMP_MOTION line) — consumed for real in R3 (register row: MotionDone
|
||||||
|
observed-not-consumed, retire R3).
|
||||||
|
Tests first: FULL existing suite green (parity bar) + pre-cutover recorded
|
||||||
|
SetCycle-sequence traces (captured BEFORE this commit) replayed → same selected
|
||||||
|
cycle/link identities + same hook stream, with EXPECTED-DIFF annotations documented
|
||||||
|
per case (known intentional changes: action overlays now rebuild → base cycle restarts;
|
||||||
|
direction flips route the double-hop) + #61 boundary-flash re-check under the new link
|
||||||
|
path. Empty-MotionTable tier: PerformMovement returns 7, sequencer stays do-nothing
|
||||||
|
(RenderBootstrap invariant, r1 API table).
|
||||||
|
Fixture source: **pre-cutover recorded adapter traces (synthetic harness)** + suite.
|
||||||
|
Deps: Q2+Q3 (+ R1-P5/P6 committed).
|
||||||
|
|
||||||
|
**Q5 — RemoteMotionSink DELETED; funnel dispatches straight into PerformMovement; spawn/world lifecycle; AP-73 retired.** (closes H4, H5-callers, H11-callers, H15, H17-carry)
|
||||||
|
Replace `RemoteMotionSink` with a thin Core `IInterpretedMotionSink` implementation
|
||||||
|
(`Motion/MotionTableDispatchSink.cs` or direct on the entity's manager):
|
||||||
|
`ApplyMotion(motion, speed)` → `PerformMovement(InterpretedCommand{motion, speed})`;
|
||||||
|
`StopMotion(motion)` → `PerformMovement(StopInterpretedCommand{motion, 1f})`. No axis
|
||||||
|
collection, no priority pick, no Commit, no HasCycle probe, no Run→Walk→Ready chain —
|
||||||
|
GetObjectSequence + is_allowed decide (H4/H5). The ObservedOmega turn seed (H17) moves
|
||||||
|
verbatim into the new sink (register row updated: retire in R6 when apply_physics
|
||||||
|
drives remote rotation). GameWindow: `RemoteMotionSink` ctor sites (:4643-4646) swap
|
||||||
|
to the new sink; spawn/fallback SetCycle(Ready)/HasCycle chains (:3723-3825) →
|
||||||
|
`manager.initialize_state`; teleport/despawn/enter-world → HandleEnterWorld/
|
||||||
|
HandleExitWorld. Delete `AnimationSequencer.HasCycle` if caller-free after this (else
|
||||||
|
keep as diagnostic, note it).
|
||||||
|
Tests first: S2a 183-case funnel conformance suite green with sink assertions
|
||||||
|
re-targeted (dispatch order is funnel-owned and unchanged; assertions move from
|
||||||
|
"collected axes" to "PerformMovement call sequence"); MotionInterpreterFunnelTests
|
||||||
|
green; live-protocol smoke via ACDREAM_DUMP_MOTION + ACDREAM_REMOTE_VEL_DIAG
|
||||||
|
(walk/run/toggle/backward/turn/circle/sidestep/emote/attack/stop, player+NPC+monster);
|
||||||
|
**ONE user visual pass** (walk↔run stride continuity, turn-while-running legs, emote
|
||||||
|
overlay, stop settle — the stage acceptance).
|
||||||
|
Registers: **AP-73 DELETED in this commit**; rows added for ObservedOmega side-write
|
||||||
|
(if not already covered) and any adapter boundary normalization from Q4.
|
||||||
|
Deps: Q4.
|
||||||
|
|
||||||
|
**Q6 — register sweep + roadmap + digest (docs/cleanup only).**
|
||||||
|
Grep-sweep dead code (IsLocomotionCycleLowByte remnants, HasCycle callers, fallback
|
||||||
|
chains, SCFAST/SCFULL diag re-anchoring); reconcile every touched register row
|
||||||
|
(retired: AP-73, Fix B, stop-anim fallback, G13, G17; surviving with rows: K-fix18→R3,
|
||||||
|
velocity/omega synthesis→R3, ObservedOmega seed→R6, MotionDone-unconsumed→R3,
|
||||||
|
managed-LinkedList-vs-DLList); roadmap stage table (R2 shipped); memory digest note
|
||||||
|
(animation sequencer deep-dive cross-link: pending_animations gap CLOSED).
|
||||||
|
Deps: Q5.
|
||||||
|
|
||||||
|
Parallelization note: Q1 and the Q0 cdb capture are independent; Q2/Q3 are
|
||||||
|
sequential-coupled (Q3 calls Q2); Q4/Q5 each touch GameWindow — do NOT fan out
|
||||||
|
(feedback_dont_parallelize_coupled_plan_slices).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. MotionDone → S2-funnel pending_motions (the R3 boundary contract)
|
||||||
|
|
||||||
|
What R2 ships and where R3 plugs in:
|
||||||
|
|
||||||
|
```
|
||||||
|
CSequence.UpdateInternal [R1, shipped]
|
||||||
|
└─ G5 gate (head != first_cyclic) → IAnimHookQueue.AddAnimDoneHook
|
||||||
|
└─ AdapterHookQueue → AnimationDoneSentinel into _pendingHooks [R1-P5]
|
||||||
|
GameWindow anim tick (:9876-9890) [R2-Q4 wiring]
|
||||||
|
├─ per drained AnimationDoneSentinel → manager.AnimationDone(success: true)
|
||||||
|
│ [retail: AnimDoneHook::Execute 0x00526c20 → Hook_AnimDone 0x0050fda0
|
||||||
|
│ → CPartArray::AnimationDone(1) — one call per queued hook]
|
||||||
|
└─ once per tick → manager.UseTime() ≡ CheckForCompletedMotions()
|
||||||
|
[retail call sites 0x00517d57/0x00517d67; drains zero-tick entries:
|
||||||
|
stops-without-links, fast-path re-speeds (outTicks=0), truncated nodes]
|
||||||
|
MotionTableManager.AnimationDone / CheckForCompletedMotions [R2-Q3]
|
||||||
|
├─ action-class head (0x10000000) → MotionState.RemoveActionHead() [R2-OWNED —
|
||||||
|
│ the action FIFO pop lives HERE, not in R3]
|
||||||
|
└─ IMotionDoneSink.MotionDone(uint motion, bool success) [R2 seam]
|
||||||
|
R2: diagnostic recorder only (register row: unconsumed until R3)
|
||||||
|
R3: MotionInterpreter.MotionDone — pops CMotionInterp.pending_motions,
|
||||||
|
action-class → RemoveAction from raw+interpreted state, recomputes
|
||||||
|
IsAnimating (ACE MotionInterp.cs:210-231; MovementManager.MotionDone
|
||||||
|
relay). The funnel's own note at MotionInterpreter.cs:1395 marks the
|
||||||
|
attachment point.
|
||||||
|
```
|
||||||
|
|
||||||
|
Two structural facts R3 must respect (from r2-ace-motiontable's headline finding,
|
||||||
|
decomp-confirmed): **MotionTableManager is UPSTREAM of CMotionInterp in the
|
||||||
|
completion chain, never a peer** — retail keeps TWO pending trackers
|
||||||
|
(`MotionTableManager.pending_animations` under CPartArray vs
|
||||||
|
`CMotionInterp.pending_motions` under MovementManager) and `CPhysicsObj::MotionDone`
|
||||||
|
feeds only the interp side; do not merge the queues. And the per-tick PLACEMENT of
|
||||||
|
both the sentinel drain and UseTime is provisional until **R6** installs retail's
|
||||||
|
`UpdateObjectInternal` order (process_hooks LAST; MovementManager.UseTime/
|
||||||
|
CPartArray.HandleMovement mid-tick) — R2 documents the current GameWindow drain point
|
||||||
|
as the G6 seam, unchanged.
|
||||||
|
|
||||||
|
Success-flag semantics to preserve: AnimationDone passes the CALLER's flag (true from
|
||||||
|
Hook_AnimDone; false from enter/exit-world drains); CheckForCompletedMotions hardcodes
|
||||||
|
true. R3's jump/HitGround logic keys off this flag — getting it wrong is invisible
|
||||||
|
until R3.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue