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:
Erik 2026-07-19 21:44:12 +02:00
parent c5ab99081c
commit 31a0889f08
4 changed files with 155 additions and 42 deletions

View file

@ -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) &gt; 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);
}
}
}