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>
30 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. - Leftover carry RESOLVED from matching v11.4186 assembly (2026-07-19).
0x00525982-0x0052598dcopies the localframeTimeElapsedback into the elapsed argument before0x00525990loops 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_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); 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 != null → apply_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 (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).