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>
31 KiB
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:
0x0052578cexecute_hooks(..., ebx_2, 1)withebx_2 += 1after = FORWARD crossing → +1;0x0052590cexecute_hooks(..., ebx_1, 0xffffffff)withebx_1 -= 1after = REVERSE → -1. Retail encoding == ACEAnimationHookDir(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=trueplaced unconditionally after the per-frame loop). The authoritative skeleton is ACESequence.cs:351-443(verified verbatim this pass, quoted below in P4), which matches the raw decomp's branch layout at0x005255d0-0x005259ca: overshoot check → clampframeNumtoget_high_frame()(fwd) /get_low_frame()(rev) + computeframeTimeElapsedleftover +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-0x0052598dappears to zeroarg2(elapsed) afteradvance_to_next_animationbefore looping, while ACE carriestimeElapsed = 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 onCSequence::advance_to_next_animationcounting invocations perCSequence::updatecall 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_frame↔high_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 0x00526c20 → Hook_AnimDone 0x0050fda0 → CPartArray::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 != null → apply_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 (AttachCycleVelocityAccessor → MotionInterpreter.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).