# 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; 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.