acdream/docs/research/2026-07-02-r1-csequence/r1-gap-map.md
Erik 1371c2a14c feat(R1-P0/P1): CSequence research base + verbatim AnimSequenceNode
P0 — research + pins: full CSequence-family verbatim extraction (1756
lines, per-function raw pseudo-C + cleaned flow, decomp line anchors),
ACE cross-reference (9 ranked divergences; headline: retail frame_number
is x87 long double — ACE's float is the worst case, our double the best
available; ACE's frame-boundary epsilon is an ACE fabrication, NOT
retail), current-sequencer map, and the R1 gap map (20 gaps, 13 keeps,
P0-P6 port order). Pinned the one decomp ambiguity (leftover-time carry
after advance_to_next_animation — ACE reading adopted; cdb confirmation
protocol recorded, non-blocking).

P1 — AnimSequenceNode verbatim (gap G1/G2/G16/G18):
- direction-aware BARE-INT boundary pair (get_starting_frame 0x00525c80 /
  get_ending_frame 0x00525cb0): reverse starts at high+1, ends at low —
  NO epsilon;
- multiply_framerate (0x00525be0) swaps low/high on negative factor;
- set_animation_id (0x00525d60) retail clamp order (high<0 -> num-1;
  low>=num -> num-1; high>=num -> num-1; low>high -> high=low);
- ctors with retail defaults (30f/-1/-1; AnimData copy + clamp);
- get_pos_frame null out-of-range (retail; ACE returns identity),
  floor double overload; get_part_frame same discipline;
- NO per-node IsLooping/Velocity/Omega — loop membership is list
  structure, physics accumulators live on the sequence (G16).

22 conformance tests (clamp table, boundary mirror table, swap
round-trip, bounds/floor semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:45:56 +02:00

31 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.
  • ONE remaining unresolved decomp ambiguity — raw decomp at 0x0052598a-0x0052598d appears to zero arg2 (elapsed) after advance_to_next_animation before looping, while ACE carries timeElapsed = frameTimeElapsed (the leftover). If retail truly zeroed it, a lag spike could never fast-forward through multiple queued nodes in one tick (observable). ACE's reading is far more plausible (BN likely lost the var reassignment through the x87 stack slot). Pin in the R1 pseudocode commit (P0) — cdb breakpoint on CSequence::advance_to_next_animation counting invocations per CSequence::update call under induced multi-node overshoot.

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); a separate CPhysicsObj::process_hooks drains + Execute()s once per physics tick then resets m_num=0. acdream's _pendingHooks + ConsumePendingHooks() is structurally similar (queue then drain) but the drain point is GameWindow's render-tick loop immediately after Advance — not positioned per retail's UpdateObjectInternal order (process_hooks runs LAST, after MovementManager.UseTime etc.). R1 must expose the queue through a host seam so R6 can place the drain correctly. execute_hooks 0x00524830 (line 300780), add_anim_hook 0x00514c20 (line 282906), process_hooks 0x00511550 (line 279431) (§18) AnimationSequencer.cs:1371-1388 (ExecuteHooks_pendingHooks), GameWindow.cs:9882 (drain) MED for R1 (seam), HIGH by R6
G7 apply_physics + Frame-target root motion is unwired. Retail: update(quantum, Frame*) accumulates velocity*signed_quantum into frame.m_fOrigin and omega*signed_quantum via Frame::rotate, with signed_quantum = copysign(fabs(arg3), arg4); per crossed frame the quantum is 1.0/framerate signed by elapsed. acdream has NO apply_physics: velocity/omega are surfaced as CurrentVelocity/CurrentOmega properties consumed by external movement code, and pos-frame root motion accumulates into _rootMotionPos/_rootMotionRot that nothing drains (ConsumeRootMotionDelta() has zero callers). 0x00524ab0 (line 300955, §19); Frame::rotate 0x004525b0 AnimationSequencer.cs:975-979 region (ConsumeRootMotionDelta, dead); CurrentVelocity/CurrentOmega consumers GameWindow.cs:9331-9334, :12917 HIGH — this is retail's entire physics-from-animation mechanism; R6's CPartArray.Update → adjust_offset → Frame.combine tick order needs it
G8 Empty-list physics-only fallback. Retail update: if anim_list empty and frame != nullapply_physics(frame, elapsed, elapsed) (accumulated velocity still moves the object — free-fall/knockback with no anim). acdream Advance with no _currNode does nothing. 0x00525b80 (line 302402, §22) AnimationSequencer.cs:874 (while (... && _currNode != null ...) — falls out) MED
G9 advance_to_next_animation's four-pose-op transition + reverse node stepping + asymmetric wrap. Retail per node transition: (a) subtract outgoing node's pos_frame at current frame_number + apply_physics(1/framerate, sign=elapsed); (b) step node — forward: GetNext else wrap to first_cyclic; reverse: GetPrev else wrap to LIST TAIL; (c) frame_number = get_starting_frame() (fwd) / get_ending_frame() (rev); (d) combine incoming node's pos_frame + apply_physics. acdream AdvanceToNextAnimation only steps .Next/wraps to _firstCyclic/holds-on-last (invented hold), resets _framePosition, and does none of the pose subtract/combine ops and has no reverse branch at all. 0x005252b0 (line 301622, §23) AnimationSequencer.cs:1344-1364 HIGH (fwd pose ops + reverse branch); the hold-on-last-node is an acdream invention to delete
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) ACE Sequence.cs:436-442 (timeElapsed = frameTimeElapsed + recurse); decomp loop-back @0x005255e8 (see open ambiguity above) Advance's timeRemaining/overflow continue (r1-acdream map row "update_internal")
Root-motion composition directions: combine (apply pose) forward, subtract (un-apply) reverse Frame::combine/Frame::subtract1 call sites in 0x005255d0/0x005252b0 ApplyPosFrame(node, idx, reverse:) fwd/conjugate-reverse (r1-acdream map row "Root motion") — values correct, TARGET wrong (G7: accumulator never drained)
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). 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), and pin the ONE open ambiguity: leftover-time carry vs arg2=0 at 0x0052598a. 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 (four pose ops; fwd wrap first_cyclic, rev wrap 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).