acdream/docs/research/2026-07-02-r1-csequence/r1-gap-map.md
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

30 KiB
Raw Blame History

R1 gap map — retail CSequence vs acdream AnimationSequencer

Inputs: r1-csequence-decomp.md (verbatim retail extraction, line anchors into docs/research/named-retail/acclient_2013_pseudo_c.txt), r1-ace-sequence.md (ACE port cross-reference), r1-acdream-sequencer.md (current-code map). Plan of record: docs/plans/2026-07-02-retail-motion-animation-rewrite.md (R1 stage). Current code: src/AcDream.Core/Physics/AnimationSequencer.cs (1584 lines) + AnimationCommandRouter.cs / AnimationHookRouter.cs / IAnimationHookSink.cs.

Conflicts between the two research passes RESOLVED in this synthesis (re-read raw decomp):

  • Hook direction constants — r1-ace-sequence.md's claim ("forward pass at 0x0052590c passes 0xffffffff") was a branch misattribution. Raw decomp verified this pass: 0x0052578c execute_hooks(..., ebx_2, 1) with ebx_2 += 1 after = FORWARD crossing → +1; 0x0052590c execute_hooks(..., ebx_1, 0xffffffff) with ebx_1 -= 1 after = REVERSE → -1. Retail encoding == ACE AnimationHookDir (references/ACE/Source/ACE.Entity/Enum/AnimationHookDir.cs: Backward=-1, Both=0, Forward=1) == the DatReaderWriter enum acdream already uses. No enum-remap needed.
  • update_internal fine structure — r1-csequence-decomp.md §21's cleaned flow is garbled in two places (an "early return" where retail clamps+continues; hit_boundary=true placed unconditionally after the per-frame loop). The authoritative skeleton is ACE Sequence.cs:351-443 (verified verbatim this pass, quoted below in P4), which matches the raw decomp's branch layout at 0x005255d0-0x005259ca: overshoot check → clamp frameNum to get_high_frame() (fwd) / get_low_frame() (rev) + compute frameTimeElapsed leftover + animDone=true → per-frame crossing loop → if (!animDone) return → AnimDone gate (head != first_cyclic) → advance_to_next_animation → carry leftover → loop.
  • Leftover carry RESOLVED from matching v11.4186 assembly (2026-07-19). 0x00525982-0x0052598d copies the local frameTimeElapsed back into the elapsed argument before 0x00525990 loops to the update head. Retail carries leftover time across animation nodes exactly as ACE does; the apparent zero in the pseudo-C was x87 stack noise.

Severity key: BLOCKER = must be right or R1's conformance harness is meaningless; HIGH = visible animation wrongness / blocks R2+; MED = edge-case visible or blocks a later stage; LOW = dormant/textual.


1. ITEMIZED GAP LIST

# Retail behavior acdream lacks/diverges on Decomp anchor Current-code anchor Severity
G1 Direction-aware boundary pair with bare-int values. Retail get_starting_frame = framerate<0 ? high+1 : low, get_ending_frame = framerate<0 ? low : high+1 — plain ints, NO epsilon. acdream (following ACE) returns (EndFrame+1) - FrameEpsilon with FrameEpsilon = 1e-5 (ACE uses 0.0002); the epsilon is an ACE fabrication compensating for ACE's own float FrameNumber. 0x00525c80/0x00525cb0, pseudo-C lines 302483/302501 (r1-csequence-decomp §26/27); ACE divergence #2 (r1-ace-sequence) AnimationSequencer.cs:154,164 (GetStartFramePosition/GetEndFramePosition), :171 (FrameEpsilon=1e-5) BLOCKER — every boundary decision keys off these values
G2 multiply_framerate swaps low_framehigh_frame on negative factor. acdream keeps StartFrame ≤ EndFrame invariant and encodes direction only in Framerate sign, compensating in Advance — explicitly documented divergence. Verbatim port requires the swap + the direction-aware G1 pair; the two mechanisms are coupled. 0x00525be0, line 302425 (§14a) AnimationSequencer.cs AnimNode.MultiplyFramerate (r1-acdream map row "multiply_framerate") HIGH — reverse playback (WalkBackward links, reverse stops) correctness
G3 update_internal boundary model: clamp-to-high_frame/low_frame + leftover-time carry, boundary test floor(frameNum) > high_frame (i.e. ≥ high+1). acdream tests newPos >= maxBoundary - FrameEpsilon and clamps _framePosition = maxBoundary - FrameEpsilon — epsilon-shifted boundary, different clamp target, no strict retail equivalence at exact-integer landings. This is the #61 "link→cycle boundary flash" bug class. 0x005255d0 body (lines 301839-302235); ACE Sequence.cs:366-377 (fwd), :394-406 (rev) AnimationSequencer.cs:894,900 (maxBoundary - FrameEpsilon) BLOCKER
G4 safety=64 loop cap — retail has none; termination comes from frameTimeElapsed shrinking + the frametime == 0 branch returning. Mandate says delete bandaids on cutover. ACE Sequence.cs:351-443 (no cap); decomp 0x005255d0 (while-true loop) AnimationSequencer.cs:872-874 MED (delete in R1 core)
G5 AnimDone gate is a LIST-STRUCTURE test, not a node flag. Retail queues the global anim_done_hook singleton when animDone && hook_obj != null && (anim_list.head_ 4) != first_cyclic — i.e. "the old head has been consumed and we're not already in the cyclic tail". acdream pushes AnimationDoneSentinel gated on !_currNode.Value.IsLooping — a per-node flag that acdream itself invented (retail nodes have no IsLooping). Different gate ⇒ different MotionDone timing. R2's CheckForCompletedMotions → AnimationDone → MotionDone chain consumes this signal; it must be retail-exact in R1. 0x00525943-0x00525968 (verified raw this pass); AnimDoneHook singleton at data 0x0081d9fc, Execute 0x00526c20Hook_AnimDone 0x0050fda0CPartArray::AnimationDone(1) (§18a) AnimationSequencer.cs:1129 (AnimationDoneSentinel), Advance's !IsLooping gate (r1-acdream map row "AnimationDone hook") HIGH
G6 Two-stage hook dispatch. Retail execute_hooks QUEUES matching CAnimHook* into CPhysicsObj.anim_hooks (SmartArray); CPhysicsObj::process_hooks executes the complete stream inside UpdatePositionInternal, before the transition and manager tail, then resets m_num=0. acdream's _pendingHooks + ConsumePendingHooks() is structurally similar. R6 processes semantic AnimationDone at capture time while retaining every other pose-dependent hook until current root/part/equipped-child poses are published. execute_hooks 0x00524830 (line 300780), add_anim_hook 0x00514c20 (line 282906), process_hooks 0x00511550 (line 279431) (§18); corrected order pinned in 2026-07-19-r6-complete-root-frame-pseudocode.md AnimationSequencer.cs pending-hook queue; AnimationHookFrameQueue.Capture semantic completion and Drain presentation delivery PARTIAL: AnimationDone semantic order retired 2026-07-19; non-AnimationDone delivery remains TS-50
G7 Frame-target root motion. update(quantum, Frame*) accumulates literal velocity and omega plus authored PosFrames into one Frame. Local and remote movement now pass that complete Frame through PositionManager, Frame::combine, and collision; command-derived translation and manual rotation consumers are gone. 0x00524ab0 (line 300955, §19); Frame::rotate 0x004525b0; Frame::combine 0x005122E0 CSequence.ApplyPhysics; AnimationSequencer.Advance(dt, Frame); MotionDeltaFrame; PlayerMovementController; RemotePhysicsUpdater; 2026-07-19-r6-complete-root-frame-pseudocode.md RETIRED 2026-07-19
G8 Empty-list physics-only fallback. Retail update: if anim_list is empty and frame != nullapply_physics(frame, elapsed, elapsed) (accumulated velocity still moves the object — free-fall/knockback with no animation). The ported CSequence.Update implements that branch and AnimationSequencer.Advance(dt, Frame) exposes it to the ordinary-object scheduler. 0x00525b80 (line 302402, §22) CSequence.Update; AnimationSequencer.Advance(dt, Frame); empty-list/full-Frame scheduler conformance tests RETIRED 2026-07-19
G9 advance_to_next_animation uses elapsed-direction plus node-framerate sign gates. For elapsed ≥ 0, subtract the outgoing pose only when its framerate is negative, step Next/wrap first_cyclic, and combine the incoming pose only when its framerate is positive. For elapsed < 0, subtract when outgoing framerate is non-negative, step Previous/wrap list tail, and combine when incoming framerate is negative. Physics inside a selected pose operation uses the separate strict abs(framerate) > F_EPSILON gate. Matching v11.4186 assembly and ACE agree; the earlier “four unconditional pose ops” cleanup was wrong. 0x005252B0; matching assembly audit 2026-07-19; r1-ace-sequence.md:70-86 CSequence.AdvanceToNextAnimation + exact-boundary/sign conformance tests RETIRED 2026-07-19
G10 append_animation slides first_cyclic to the just-appended node on EVERY call. Retail's "cyclic tail" is always exactly the LAST appended anim (so a multi-anim cycle MotionData loops only its final AnimData node once earlier ones are consumed). acdream sets _firstCyclic to the FIRST node of the cycle MotionData. Also retail: if (curr_anim == null) { curr_anim = head; frame_number = get_starting_frame(head); } — acdream's equivalents are scattered through SetCycle's rebuild. 0x00525510 (line 301777, §24) AnimationSequencer.cs:634-645 region + EnqueueMotionData (r1-acdream map rows "Node list", "add_motion") HIGH — divergent loop membership for multi-anim cycles; also the retail invariant that makes remove_cyclic/apricot correct
G11 The remove-family with curr_anim snap semantics is missing. Retail: remove_cyclic_anims (0x00524e40) deletes first_cyclic→tail, snapping curr_anim back to prev + frame_number = get_ending_frame(prev) (or 0), then first_cyclic = tail; remove_link_animations(n) (0x00524be0) / remove_all_link_animations (0x00524ca0) delete predecessors of first_cyclic, snapping curr_anim FORWARD to first_cyclic + get_starting_frame; clear_animations (0x00524dc0) full wipe; apricot (0x00524b40) trims consumed leading nodes after every update, bounded by curr_anim/first_cyclic. acdream instead has ClearCyclicTail + wholesale queue clears + the invented "stale-head _currNode force-relocation" + "Fix B link-skip" — all approximations of what the retail remove-family + apricot do naturally. lines 301258/301060/301128/301207/300978 (§5-8, §20) AnimationSequencer.cs:1311 (ClearCyclicTail), :511, stale-head relocation + Fix B blocks in SetCycle (r1-acdream map rows "Stale-head handling", "Fix B") HIGH — these retire two invented mechanisms
G12 combine_physics/subtract_physics accumulators absent. Retail velocity += / -= element-wise (x87-widened). acdream only has replace (EnqueueMotionData) + ClearPhysics. Needed by R2's fast path (change_cycle_speed + subtract_motion(old) + combine_motion(new)) and by jump/knockback physics later. 0x005248c0/0x00524900 (lines 300818/300832, §12/13) no equivalent in AnimationSequencer.cs MED (trivial; part of the verbatim class)
G13 multiply_cyclic_animation_fr must touch ONLY node framerates. Retail walks first_cyclic→tail calling multiply_framerate. acdream's MultiplyCyclicFramerate additionally scales CurrentVelocity/CurrentOmega — algebraically equivalent to retail's change_cycle_speed+subtract/combine_motion composite (an R2 mechanism folded in). Core port must separate them or R2's verbatim fast path double-applies. 0x00524940 (line 300846, §14) AnimationSequencer.cs MultiplyCyclicFramerate (r1-acdream map row) MED (correct today by accident; wrong the moment R2 lands)
G14 Placement frames absent. Retail set_placement_frame/placement_frame_id + get_curr_animframe returning placement_frame when curr_anim == null (static pose for object with no active anims). acdream has no placement concept (identity frames only). Explicit R1 scope item in the plan. 0x005249b0/0x00524970 (lines 300872/300855, §15/16) no equivalent (grep "placement" hits only a doc comment at AnimationSequencer.cs:979) MED
G15 frame_number precision. Retail: x87 long double (80-bit; verbatim acclient.h:30747). acdream: double (AnimationSequencer.cs:303) — the best C# can do; ACE's float is worse. Residual double-vs-extended ULP divergence at exact frame boundaries is an unavoidable adaptation → needs a divergence-register row in the R1 core commit. acclient.h:30747; headline of r1-ace-sequence AnimationSequencer.cs:302-303 LOW runtime / process-MANDATORY register row
G16 Node ctor defaults + set_animation_id clamp order. Retail defaults: framerate=30f, low_frame=-1, high_frame=-1; AnimData defaults low=0, high=-1, framerate=30f; clamp order: high<0→num-1, low>=num→num-1, high>=num→num-1, low>high→high=low. acdream LoadAnimNode handles the -1 sentinel + low>high but not the full order; per-node it also stores IsLooping/HasPosFrames/Velocity/Omega fields retail nodes don't have (retail velocity/omega live on the SEQUENCE only). NOTE: r1-ace-sequence flagged set_animation_id as unverified, but r1-csequence-decomp §25 captured the FULL body — resolved, no follow-up grep needed. 0x00525d30/0x00525f90/0x00525d60 (lines 302547/302744/302561, §25); AnimData ctor 0x00525ce0 AnimationSequencer.cs AnimNode + LoadAnimNode (r1-acdream map §0, row "Placement / root frames") MED
G17 add_motion velocity semantics: unconditional REPLACE. Retail free fn add_motion (0x005224b0) calls set_velocity(motionData.velocity*speed)/set_omega(...) unconditionally (a MotionData without the dat HasVelocity bit carries zero → replace-with-zero). acdream gates on Flags.HasFlag(HasVelocity) and otherwise LEAVES the previous value (AnimationSequencer.cs:1288-1294 — comment claims "matches retail's conditional behavior", which the decomp contradicts). Retail avoids the zero-a-running-cycle problem via call-graph (modifiers go through combine_motion, not add_motion) — an R2 distinction acdream compensates for with this flag gate. 0x005224b0 (r1-ace-sequence add_motion section); combine_motion 0x00522580 AnimationSequencer.cs:1288-1294 MED — port unconditional replace in the R1 core; keep the gate in the adapter until R2 routes modifiers through combine_motion, then delete (register row if the adapter gate outlives R1)
G18 get_pos_frame returns null out-of-range (retail), not identity — and retail's execute_hooks has a latent null-deref on arg2->hooks (no null check). Port: null-return + the ACE-style null guard in execute_hooks as a documented safe divergence (crash-parity with retail is not a goal). acdream's ExecuteHooks already bounds-checks (:1373) — keep the guard, cite it. 0x00525c10/0x00524830 (§28/§18); ACE divergences #5/#8 AnimationSequencer.cs:1373 LOW
G19 update entry contract. Retail update(double quantum, Frame*): non-empty → update_internal then apricot; empty → physics-only. acdream Advance(float dt) returns blended PartTransform[] and never trims consumed nodes structurally (rebuilds hide it). Core port needs the retail entry + apricot; the blended-frame render output stays an adapter/render-side concern (retail renders off get_curr_animframe's FLOORED index; interpolation lives in CPartArray-land, out of R1 core scope). 0x00525b80 + apricot 0x00524b40 (§20/22) AnimationSequencer.cs:872+ (Advance), BuildBlendedFrame HIGH (apricot + entry contract); blend seam MED
G20 clear() scope. Retail clear = clear_animations()+clear_physics() ONLY (2 calls, 0x005255b0); the placement_frame reset belongs to UnPack (0x005259d0). Do not copy ACE's Clear() which folds the placement reset in. line 301828 (§3); ACE divergence #6 acdream Reset() (no external callers) LOW

Invented behaviors NOT in the gap list because they are R2/R3 scope and survive R1 in the adapter, unchanged: K-fix18 skipTransitionLink (retire in R3 jump family), Fix B locomotion link-skip (retail mechanism = remove_redundant_links 0x0051bf20, R2), stop-anim fallback + GetLink reversed branch (R2 get_link/GetObjectSequence), velocity-synthesis constants Walk=3.12/Run=4.0/Side=1.25 (R3 get_state_velocity), HasCycle probe (R2), retail slerp + BuildBlendedFrame (render-side, not CSequence). Each keeps/gets its divergence-register row when touched.


2. KEEP LIST — already matching retail

Behavior Retail anchor acdream anchor
execute_hooks direction filter dir==0(Both) || dir==caller 0x00524830 line 300780; constants verified: fwd=+1 @0x0052578c, rev=-1 @0x0052590c AnimationSequencer.cs:1371-1388; DatReaderWriter AnimationHookDir Backward=-1/Both=0/Forward=1 == retail encoding
Queue-then-drain hook model (hooks NOT executed inline during frame advance) add_anim_hook 0x00514c20 + process_hooks 0x00511550 _pendingHooks + ConsumePendingHooks() (drain placement moves in R6, mechanism correct)
Per-frame crossing walk fires pose+hooks for EVERY integer frame crossed, strict ascending (fwd) / descending (rev) order 0x005255d0 do/while loops (lines 302006-302056 + reverse mirror) AnimationSequencer.cs:910-941 (fwd lastFrame++ w/ Forward, rev lastFrame-- w/ Backward)
Forward node wrap to first_cyclic (loop-the-cycle mechanism) 0x005252b0 @0x005253xx: GetNext==null → first_cyclic AnimationSequencer.cs:1350-1358
Leftover-time carry into the next node after a boundary (multi-node fast-forward in one tick) Matching v11.4186 assembly 0x00525982-0x00525990 copies frameTimeElapsed back then loops; ACE Sequence.cs:436-442 agrees CSequence.UpdateInternal assigns timeElapsed = frameTimeElapsed before looping
Root-motion composition directions: combine (apply pose) forward, subtract (un-apply) reverse Frame::combine/Frame::subtract1 call sites in 0x005255d0/0x005252b0 CSequence writes the caller-supplied root Frame; local and remote physics consume that same Frame (G7 retired)
frame_number floored to int for pose lookup (get_curr_animframe/get_curr_frame_number shape) 0x00524970/0x005249d0 (§15/17) AnimationSequencer.cs:884 ((int)Math.Floor(_framePosition))
clear_physics zeroing before rebuild 0x00524d50 + GetObjectSequence's clear_physics; remove_cyclic_anims pairing @0x005229cf etc. ClearPhysics() called from SetCycle (r1-acdream map row "clear_physics")
AnimData speed scaling: only framerate × speed, low/high pass through operator* 0x00525d00 (invoked from add_motion @0x0052255b) LoadAnimNode (AnimData.Framerate * speedMod)
HighFrame == -1 sentinel → last frame; low > high → high = low degenerate guard set_animation_id 0x00525d60 clamps LoadAnimNode (r1-acdream map row "Placement / root frames") — partial (see G16 for full order)
Fast-path re-speed without restart on same motion (concept) ACE MotionTable.cs:132-139; retail change_cycle_speed 0x00522290 SetCycle early-return → MultiplyCyclicFramerate (G13 caveat)
frame_number at double ≥ ACE's float acclient.h:30747 (long double) AnimationSequencer.cs:303 — already the best-available C# type
Retail slerp incl. validation-fallback quirk (render blend, not CSequence) FUN_005360d0 (chunk_00530000.c:4799-4846) SlerpRetailClient — keep untouched

3. PORT ORDER — R1 commit sequence (tests-first, each one commit)

New code target: src/AcDream.Core/Physics/Motion/ (plan rule 4). Naming: retail names (CSequence, AnimSequenceNode) or thin C# equivalents — decided in P1, consistent after. Every commit: build+test green; register rows added/retired in-commit.

P0 — pseudocode + ambiguity pinning (docs only; leftover resolved 2026-07-19). Write docs/research/2026-07-xx-csequence-pseudocode.md from r1-csequence-decomp.md, CORRECTING §21 to the ACE-verified skeleton (this doc's header). The former leftover-time ambiguity is resolved by matching assembly: carry is authoritative. Fixture source: cdb trace — breakpoint acclient!CSequence::advance_to_next_animation + acclient!CSequence::update with hit counters (pattern: tools/cdb/l2g-observer.cdb); ratio >1 advance-per-update under a stall/lag proves the carry. Also capture append_animation/remove_cyclic_anims arg logs here (they feed P2/P5 goldens — one cdb session serves all of R1). Dependencies: none. This is the workflow's mandatory step-3 artifact.

P1 — AnimSequenceNode verbatim. (closes G1, G2, G16, G18-node-half) Fields anim/framerate(float)/low_frame/high_frame only (NO IsLooping/Velocity/Omega per-node); ctors with retail defaults (30f/-1/-1); set_animation_id full clamp order (§25); get_starting_frame/get_ending_frame bare-int direction-aware pair (NO epsilon); multiply_framerate with low/high swap on negative; get_pos_frame (null OOB, both overloads) / get_part_frame / has_anim. Uses existing IAnimationLoader for the DBObj::Get seam. Tests first: synthetic (all clamp branches; negative-multiply swap; direction-aware boundary mirror table) + dat fixtures (real Humanoid MotionTable AnimData via DatCollection: resolve, clamp, verify against Animation.PartFrames.Count). Dependencies: P0.

P2 — CSequence container + list surgery. (closes G10, G11, G12, G14, G15, G20; G5's structural precondition) anim_list (LinkedList), first_cyclic, curr_anim, frame_number:double, velocity/omega, placement_frame/placement_frame_id, hook_obj-seam. Methods: append_animation (first_cyclic-slides-to-tail-every-call + curr_anim seed), clear/clear_animations/clear_physics, remove_cyclic_anims (snap-back + get_ending_frame(prev)), remove_link_animations(n), remove_all_link_animations, has_anims, apricot, set_velocity/set_omega/combine_physics/ subtract_physics, set_placement_frame/get_curr_animframe/get_curr_frame_number, multiply_cyclic_animation_fr (framerates ONLY — G13). Register rows in-commit: double-vs-long-double (G15); managed LinkedList vs intrusive DLList. Tests first: list-surgery state tables (curr_anim/first_cyclic/frame_number after every op, incl. curr_anim-inside-removed-range snaps; apricot bounded by curr_anim AND first_cyclic). Fixture source: synthetic + cdb goldens from P0 (append_animation/remove_cyclic_anims arg sequences from a live Walk→Run→Stop cycle — replay the call sequence, assert list shape). Dependencies: P1.

P3 — apply_physics + Frame math. (closes G7's math half) apply_physics(Frame, quantum, sign) with copysign semantics; verbatim Frame.combine/ Frame.subtract1/Frame.rotate equivalents (port beside the existing ApplyPosFrame math, which becomes call-compatible). Tests first: numeric goldens — hand-computed copysign cases (±quantum × ±sign), combine∘subtract1 = identity round-trips, rotate vs quaternion reference; cross-check values against ACE Sequence.cs:221 + AFrame.cs. Fixture source: synthetic (pure math; no dat needed). Dependencies: P2 (fields), parallel-safe with P1 internals.

P4 — update_internal + update + advance_to_next_animation. (closes G3, G4, G5, G6-queue, G8, G9, G19) Verbatim per the ACE-verified skeleton: floor(lastFrame) → advance frame_number → overshoot clamp to get_high_frame()/get_low_frame() + frameTimeElapsed leftover + animDone → per-frame crossing loop (combine/subtract pos_frame if pos_frames != null; apply_physics(1/framerate, elapsed) if |framerate| ≥ 0.000199999995f; execute_hooks(part_frame, +1/-1)) → if (!animDone) return → AnimDone gate head != first_cyclic → queue global AnimDoneHook → advance_to_next_animation (elapsed-direction/framerate-sign-gated pose ops; positive elapsed wraps first_cyclic, negative elapsed wraps TAIL) → carry leftover (per P0's pin) → loop (iterative, not ACE's recursion). update: non-empty → internal+apricot; empty → apply_physics(frame, elapsed, elapsed). execute_hooks queues into an IAnimHookQueue host seam (stands in for CPhysicsObj.anim_hooks; GameWindow drain point unchanged until R6). NO safety cap. Tests first — the R1 conformance harness core: (a) dat fixture: Humanoid walk cycle advanced at fixed 1/30s quanta N ticks — golden frame_number series + hook-fire (frame,direction) sequence; (b) synthetic: multi-node fast-forward in one tick (lag spike) — hook ORDER across nodes + AnimDone timing; reverse playback; exact-integer boundary landings (the G3 class); zero-framerate node; empty-list physics fallback; (c) cdb goldens from P0: advance-per-update counts + frame_number progression trace. Dependencies: P1+P2+P3.

P5 — adapter cutover: AnimationSequencer rehosted on the core. (closes G13-split, G17-core; DELETES stale-head relocation, ClearCyclicTail surgery, per-node Velocity/Omega, safety cap, boundary-epsilon) SetCycle rebuild becomes: remove_cyclic_anims() [+ remove_all_link_animations where the old code cleared] → per-AnimData append_animation(speed-scaled AnimData) (= retail free-fn add_motion 0x005224b0, unconditional set_velocity/set_omega in core) → fast path = change_cycle_speed-equivalent (multiply_cyclic_animation_fr framerates-only) + adapter-level velocity rescale (the R2 subtract/combine composite, kept at adapter, register row). Invented behaviors that SURVIVE at adapter level, byte-identical: K-fix18, Fix B, stop-anim fallback, GetLink reversed branch, velocity synthesis (each verified to still have/get its register row). Advance becomes update(dt, frame) + BuildBlendedFrame reading core curr_anim/ frame_number. PlayAction inserts via core list ops. Tests first: FULL existing suite green (behavior-parity is the acceptance bar) + adapter parity tests (same SetCycle call sequences → same selected cycle/link + same hook stream as pre-cutover recordings taken BEFORE this commit). #61's boundary-hold re-tested against verbatim boundary math — if the flash is gone with the hold removed, delete the hold (register row retired); if not, keep + re-file #61 with the new evidence. Fixture source: recorded pre-cutover adapter traces (synthetic harness) + user visual smoke. Dependencies: P4.

P6 — root-motion/placement wiring + API narrowing + register sweep. (closes G7-wiring, G14-consumers, dead-API cleanup) update(quantum, Frame) output exposed to the GameWindow tick as the entity root-motion delta (replaces dead ConsumeRootMotionDelta — delete it); placement-frame path wired for anim-less objects (get_curr_animframe fallback); delete Reset()/HasCurrentNode or map to clear()/curr_anim != null; MultiplyCyclicFramerate public surface delegates to core. Consumers keep reading CurrentVelocity/CurrentOmega (adapter mirrors core velocity/omega + synthesis until R3). Register reconciliation + roadmap stage-table update + memory digest note. Fixture source: existing launch-protocol smoke (ACDREAM_REMOTE_VEL_DIAG off/on parity) + suite. Dependencies: P5.


4. API MIGRATION — consumer survival through the cutover

R1 is adapter-preserving: every public AnimationSequencer member keeps its signature through P5; the core replaces the internals. Narrowing happens only in P6 and touches only dead surface.

Consumer (from r1-acdream-sequencer map) Call sites R1 impact
GameWindow spawn/fallback cycles :3723/3728/3732/3824 (HasCycle), :3751/:3825 (SetCycle) none — adapter unchanged
GameWindow jump/land/stop :4830 (K-fix18), :5155/:5309/:9817 none — K-fix18 param preserved at adapter until R3
GameWindow NPC legacy path :4936 (ApplyServerControlledVelocityCycle) none — path dies in R2/R6, not R1
GameWindow local-player cycle :10223 none
GameWindow anim tick :9876 (Advance), :9882 (ConsumePendingHooks) signatures unchanged; Advance internally becomes update()+blend. Hook STREAM content must be parity-tested (P5) since AnimDone timing changes gate (G5) — RemoteMotionSink/GameWindow don't consume AnimDone yet (R2 does), so risk is bounded to the hook fan-out sinks
RemoteMotionSink.Commit RemoteMotionSink.cs:215 (SetCycle), + HasCycle, ApplyMotion→PlayAction none — sink dissolves in R2, not R1
AnimationCommandRouter RouteFullCommand → SetCycle/PlayAction none
CurrentVelocity/CurrentOmega readers GameWindow.cs:9331-9334 (remote body translation), :12917 (AttachCycleVelocityAccessorMotionInterpreter.GetCycleVelocity), MotionInterpreter docs semantics preserved: adapter keeps replace-gate + locomotion synthesis EXACTLY as today (G17 core/adapter split); values must be bit-identical for locomotion low-bytes — covered by P5 parity tests
CurrentStyle/CurrentMotion/CurrentSpeedMod readers GameWindow.cs:3723/4827/4915/4919, RemoteMotionSink ctor+Commit adapter-owned bookkeeping, untouched
Diagnostics CurrentNodeDiag/FirstCyclicAnimRefHash/QueueCount GameWindow.cs:9863-9871 [CURRNODE] block re-expressed over core list (AnimRefHash from core node's anim); tuple shape kept
ConsumeRootMotionDelta zero callers deleted in P6; replaced by update(quantum, Frame) output
Reset / HasCurrentNode / external MultiplyCyclicFramerate zero external callers P6: map to clear() / curr_anim != null / core delegate, or delete
AnimationHookRouter / IAnimationHookSink sinks GameWindow.cs:9890 fan-out unchanged in R1; hook payload type stays DatReaderWriter's AnimationHook. (Side note for a separate issue, NOT R1: router's silent catch-all has no logger seam — feedback_logger_injection_for_silent_catches.md)
RenderBootstrap.SequencerFactory :138/:147-174 (3-tier Setup/MotionTable fallback) ctor signature unchanged; empty-MotionTable tier must still yield a working do-nothing sequencer (add a P5 test: empty table → has_anims()==false → physics-only update path, no throw)

Cutover invariants (P5 acceptance): (1) full existing test suite green untouched; (2) recorded SetCycle→hook/pose traces byte-parity vs pre-cutover for the standard protocol (walk/run/toggle/ turn/stop/jump, player+NPC); (3) every deleted invented mechanism's register row retired in the deleting commit; every surviving adapter-level invention has a row; (4) one user visual pass at R1 end (plan: eyes are final sanity only).