fix(animation): restore retail transition sign gates
Match the v11.4186 CSequence transition assembly and ACE cross-reference, including direction-specific pose gates and the strict physics epsilon boundary. Add conformance tests and correct the stale research interpretation. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
c5ab99081c
commit
31a0889f08
4 changed files with 155 additions and 42 deletions
|
|
@ -1140,6 +1140,16 @@ still moves the frame even with nothing animating.
|
|||
|
||||
## 23. `CSequence::advance_to_next_animation` (line 301622, addr `0x005252b0`)
|
||||
|
||||
> **2026-07-19 assembly correction:** the cleaned pseudocode below lost four
|
||||
> branch predicates. Matching v11.4186 assembly and ACE agree: elapsed ≥ 0
|
||||
> subtracts the outgoing pose only for negative framerate and combines the
|
||||
> incoming pose only for positive framerate; elapsed < 0 subtracts only for
|
||||
> non-negative outgoing framerate and combines only for negative incoming
|
||||
> framerate. The nested physics gate is strict
|
||||
> `abs(framerate) > F_EPSILON`. Use
|
||||
> `r1-ace-sequence.md:70-86` and the production `CSequence` conformance tests,
|
||||
> not the unconditional-pose cleanup later in this section.
|
||||
|
||||
**Signature:** `advance_to_next_animation(CSequence const* this, double arg2,
|
||||
AnimSequenceNode const** arg3, double* arg4, Frame* arg5)` — `arg2` is
|
||||
the signed elapsed/rate value carried over from `update_internal`
|
||||
|
|
|
|||
|
|
@ -23,13 +23,11 @@ Current code: `src/AcDream.Core/Physics/AnimationSequencer.cs` (1584 lines) +
|
|||
`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.
|
||||
- **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
|
||||
|
|
@ -49,7 +47,7 @@ stage; **LOW** = dormant/textual.
|
|||
| G6 | **Two-stage hook dispatch.** Retail `execute_hooks` QUEUES matching `CAnimHook*` into `CPhysicsObj.anim_hooks` (SmartArray); `CPhysicsObj::process_hooks` executes them 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 pose-dependent presentation delivery until current root/part 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-update-object-order-pseudocode.md` | `AnimationSequencer.cs` pending-hook queue; `AnimationHookFrameQueue.Capture` semantic completion and `Drain` presentation delivery | RETIRED by R6 semantic-order cutover 2026-07-19 |
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
|
@ -80,7 +78,7 @@ divergence-register row when touched.
|
|||
| 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") |
|
||||
| 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` | `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") |
|
||||
|
|
@ -98,10 +96,10 @@ New code target: `src/AcDream.Core/Physics/Motion/` (plan rule 4). Naming: retai
|
|||
(`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).**
|
||||
**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), and pin the ONE open ambiguity:
|
||||
leftover-time carry vs `arg2=0` at `0x0052598a`.
|
||||
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
|
||||
|
|
@ -151,7 +149,8 @@ to `get_high_frame()`/`get_low_frame()` + `frameTimeElapsed` leftover + animDone
|
|||
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
|
||||
`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
|
||||
|
|
|
|||
|
|
@ -424,14 +424,13 @@ public sealed class CSequence
|
|||
|
||||
/// <summary>
|
||||
/// <c>CSequence::advance_to_next_animation</c> (0x005252b0, §23): four
|
||||
/// pose operations per transition — un-apply the outgoing node's pose
|
||||
/// (subtract1 + residual physics), step (forward wraps to
|
||||
/// <c>first_cyclic</c>; REVERSE wraps to the LIST TAIL — asymmetric by
|
||||
/// design), reseed <see cref="FrameNumber"/> from the incoming node's
|
||||
/// direction-aware boundary, apply the incoming pose (combine +
|
||||
/// physics). Pose ops run in BOTH directions (ACE's framerate-sign
|
||||
/// gates are ACE-isms; the decomp is unconditional apart from the
|
||||
/// degenerate-framerate guard).
|
||||
/// sign-gated pose operations per transition. Positive elapsed time
|
||||
/// removes only a reverse-playing outgoing pose, steps next (wrapping to
|
||||
/// <c>first_cyclic</c>), and applies only a forward-playing incoming pose.
|
||||
/// Negative elapsed time mirrors that over previous/list-tail. The
|
||||
/// framerate sign gates are present in matching v11.4186 assembly and in
|
||||
/// ACE's line-for-line port; physics has a separate strict
|
||||
/// <c>abs(framerate) > F_EPSILON</c> gate.
|
||||
/// </summary>
|
||||
public void AdvanceToNextAnimation(double timeElapsed, Frame? frame)
|
||||
{
|
||||
|
|
@ -440,39 +439,57 @@ public sealed class CSequence
|
|||
|
||||
var outgoing = _currAnim.Value;
|
||||
|
||||
// (a) un-apply the outgoing node's pose at the CURRENT FrameNumber.
|
||||
if (frame is not null && Math.Abs(outgoing.Framerate) >= FrameOps.FEpsilon)
|
||||
{
|
||||
var pos = outgoing.GetPosFrame((int)FrameNumber);
|
||||
if (pos is not null)
|
||||
FrameOps.Subtract1(frame, pos);
|
||||
ApplyPhysics(frame, 1.0 / outgoing.Framerate, timeElapsed);
|
||||
}
|
||||
|
||||
// (b) step; (c) reseed FrameNumber from the incoming boundary.
|
||||
if (timeElapsed >= 0.0)
|
||||
{
|
||||
if (frame is not null && outgoing.Framerate < 0f)
|
||||
{
|
||||
var pos = outgoing.GetPosFrame((int)FrameNumber);
|
||||
if (pos is not null)
|
||||
FrameOps.Subtract1(frame, pos);
|
||||
if (Math.Abs(outgoing.Framerate) > FrameOps.FEpsilon)
|
||||
ApplyPhysics(frame, 1.0 / outgoing.Framerate, timeElapsed);
|
||||
}
|
||||
|
||||
_currAnim = _currAnim.Next ?? _firstCyclic;
|
||||
if (_currAnim is null)
|
||||
return;
|
||||
FrameNumber = _currAnim.Value.GetStartingFrame();
|
||||
|
||||
var incoming = _currAnim.Value;
|
||||
if (frame is not null && incoming.Framerate > 0f)
|
||||
{
|
||||
var pos = incoming.GetPosFrame((int)FrameNumber);
|
||||
if (pos is not null)
|
||||
FrameOps.Combine(frame, pos);
|
||||
if (Math.Abs(incoming.Framerate) > FrameOps.FEpsilon)
|
||||
ApplyPhysics(frame, 1.0 / incoming.Framerate, timeElapsed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (frame is not null && outgoing.Framerate >= 0f)
|
||||
{
|
||||
var pos = outgoing.GetPosFrame((int)FrameNumber);
|
||||
if (pos is not null)
|
||||
FrameOps.Subtract1(frame, pos);
|
||||
if (Math.Abs(outgoing.Framerate) > FrameOps.FEpsilon)
|
||||
ApplyPhysics(frame, 1.0 / outgoing.Framerate, timeElapsed);
|
||||
}
|
||||
|
||||
_currAnim = _currAnim.Previous ?? _animList.Last;
|
||||
if (_currAnim is null)
|
||||
return;
|
||||
FrameNumber = _currAnim.Value.GetEndingFrame();
|
||||
}
|
||||
|
||||
// (d) apply the incoming node's pose at the new FrameNumber.
|
||||
var incoming = _currAnim.Value;
|
||||
if (frame is not null && Math.Abs(incoming.Framerate) >= FrameOps.FEpsilon)
|
||||
{
|
||||
var pos = incoming.GetPosFrame((int)FrameNumber);
|
||||
if (pos is not null)
|
||||
FrameOps.Combine(frame, pos);
|
||||
ApplyPhysics(frame, 1.0 / incoming.Framerate, timeElapsed);
|
||||
var incoming = _currAnim.Value;
|
||||
if (frame is not null && incoming.Framerate < 0f)
|
||||
{
|
||||
var pos = incoming.GetPosFrame((int)FrameNumber);
|
||||
if (pos is not null)
|
||||
FrameOps.Combine(frame, pos);
|
||||
if (Math.Abs(incoming.Framerate) > FrameOps.FEpsilon)
|
||||
ApplyPhysics(frame, 1.0 / incoming.Framerate, timeElapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ namespace AcDream.Core.Tests.Physics.Motion;
|
|||
/// Skeleton per the ACE-verified structure (P0-pins.md): overshoot →
|
||||
/// clamp to the RAW high/low frame + leftover carry + animDone →
|
||||
/// per-crossed-frame pose/physics/hook loop → AnimDone gate
|
||||
/// (list HEAD != first_cyclic) → advance (pose-out both directions,
|
||||
/// forward wraps to first_cyclic, reverse wraps to the LIST TAIL) →
|
||||
/// (list HEAD != first_cyclic) → advance (sign-gated pose transition,
|
||||
/// forward wraps to first_cyclic, negative elapsed wraps to the LIST TAIL) →
|
||||
/// carry the leftover (P0 pin) → loop. NO safety cap, NO boundary
|
||||
/// epsilon.
|
||||
/// </summary>
|
||||
|
|
@ -68,6 +68,24 @@ public class CSequenceUpdateTests
|
|||
return new AnimData { AnimId = qid, LowFrame = low, HighFrame = high, Framerate = framerate };
|
||||
}
|
||||
|
||||
private static Animation MakePoseAnim(float poseX)
|
||||
{
|
||||
var anim = new Animation();
|
||||
var part = new AnimationFrame(1u);
|
||||
part.Frames.Add(new Frame
|
||||
{
|
||||
Origin = Vector3.Zero,
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
anim.PartFrames.Add(part);
|
||||
anim.PosFrames.Add(new Frame
|
||||
{
|
||||
Origin = new Vector3(poseX, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
return anim;
|
||||
}
|
||||
|
||||
private static (CSequence seq, RecordingHookQueue hooks) Cyclic(int frames, float framerate = 30f)
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
|
|
@ -195,6 +213,75 @@ public class CSequenceUpdateTests
|
|||
Assert.Equal(3, seq.CurrAnim!.HighFrame); // B (4 frames → high 3)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceToNextAnimation_PositiveElapsed_SkipsPositiveOutgoingPose()
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
loader.Add(1u, MakePoseAnim(1f));
|
||||
loader.Add(2u, MakePoseAnim(2f));
|
||||
var seq = new CSequence(loader);
|
||||
seq.AppendAnimation(Ad(1u, framerate: 30f, high: 0));
|
||||
seq.AppendAnimation(Ad(2u, framerate: 30f, high: 0));
|
||||
seq.FrameNumber = 0;
|
||||
var frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(10f, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
seq.AdvanceToNextAnimation(timeElapsed: 0.1, frame);
|
||||
|
||||
// Retail's positive-elapsed branch removes the outgoing pose only
|
||||
// when that node plays backward. The positive incoming pose applies.
|
||||
Assert.Equal(12f, frame.Origin.X, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceToNextAnimation_NegativeElapsed_SkipsNegativeOutgoingPose()
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
loader.Add(1u, MakePoseAnim(1f));
|
||||
loader.Add(2u, MakePoseAnim(2f));
|
||||
var seq = new CSequence(loader);
|
||||
seq.AppendAnimation(Ad(1u, framerate: -30f, high: 0));
|
||||
seq.AppendAnimation(Ad(2u, framerate: -30f, high: 0));
|
||||
seq.FrameNumber = 0;
|
||||
var frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(10f, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
seq.AdvanceToNextAnimation(timeElapsed: -0.1, frame);
|
||||
|
||||
// Retail's negative-elapsed branch removes only a non-negative
|
||||
// outgoing node, then applies the negative incoming node.
|
||||
Assert.Equal(12f, frame.Origin.X, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceToNextAnimation_ExactEpsilon_AppliesPoseButNotPhysics()
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
loader.Add(1u, MakePoseAnim(1f));
|
||||
loader.Add(2u, MakePoseAnim(2f));
|
||||
var seq = new CSequence(loader);
|
||||
seq.AppendAnimation(Ad(1u, framerate: 30f, high: 0));
|
||||
seq.AppendAnimation(Ad(2u, framerate: FrameOps.FEpsilon, high: 0));
|
||||
seq.SetVelocity(Vector3.UnitX);
|
||||
var frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(10f, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
seq.AdvanceToNextAnimation(timeElapsed: 0.1, frame);
|
||||
|
||||
// The sign gate owns pose application. Physics has the separate,
|
||||
// strict abs(framerate) > F_EPSILON boundary.
|
||||
Assert.Equal(12f, frame.Origin.X, 5);
|
||||
}
|
||||
|
||||
// ── stationary / degenerate (G8 + else-branch) ──────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue