feat(R2-Q4): adapter cutover — SetCycle/PlayAction rehosted on PerformMovement; Fix B / fast-path / stop-anim fallback / G17 gate DELETED
AnimationSequencer becomes a thin shim over the verbatim R2 stack:
SetCycle dispatches the style change (Branch 1) then the motion (Branch
2/3/4) through MotionTableManager.PerformMovement; PlayAction is the
same dispatch (actions rebuild via Branch 3; modifiers are Branch 4
physics-only combine_motion — the AP-73 turn-blend mechanism now runs
for real). CurrentStyle/CurrentMotion/CurrentSpeedMod are read-only
mirrors of MotionState (post-adjust_motion, signed mod). Lazy
initialize_state on first drive (retail lazy-create analog) keeps
undriven sequencers do-nothing.
DELETED legacy inventions (net -648 lines): the adapter fast-path
(replaced by Branch-2 fast re-speed: change_cycle_speed +
subtract/combine_motion), Fix B + IsLocomotionCycleLowByte (replaced by
remove_redundant_links on the pending queue — the retail mechanism the
2026-05-03 cdb trace pointed at), the stop-anim low-byte fallback
(replaced by get_link's reversed-key double-hop — retail's actual
backward-walk settle plays the windup link REVERSED), GetLink/BuildNode/
EnqueueMotionData + the G17 HasVelocity/HasOmega gate (add_motion sets
unconditionally), MultiplyCyclicFramerate + the G13 composite,
PlayAction's insert-before-tail machinery, SCFAST/SCFULL diagnostics.
KEPT at the adapter (register rows): the boundary adjust_motion remap +
locomotion velocity/omega synthesis (AP-75, retire R3-W6/R6), K-fix18
skip-link as post-dispatch RemoveAllLinkAnimations (AP-74, retire
R3-W4 when LeaveGround fires it).
GameWindow queue-drain wiring (the §4 G6 seam): each drained AnimDone
hook → manager.AnimationDone(true) (retail AnimDoneHook::Execute
0x00526c20 → Hook_AnimDone 0x0050fda0 chain), UseTime once per tick
(0x00517d57/0x00517d67); IMotionDoneSink bound to an
ACDREAM_DUMP_MOTION recorder (AD-36 — R3 rebinds to
MotionInterpreter.MotionDone).
Conformance: the 11-scenario pre-cutover trace suite (a6235a36) replays
green with six EXPECTED-DIFF annotations (double-hop walk-run routing,
reversed settle links, Branch-4 physics-only modifiers, Branch-1
style-change links, post-adjust mirrors) — everything unannotated is
byte-identical. Legacy suites repaired by a dedicated agent (fixtures
gain the retail-mandatory StyleDefaults + class-bit-tagged ids;
reflection state-poking replaced with real dispatch; zero category-D
regressions — the suspected cursor bug was a fixture class-bit gap),
plus two vacuously-passing tests fixed. Register: stale IA-4 deleted
(R1-P1 ported the negative-factor swap).
Full suite green: 3,458 passed (374+425+713+1946).
Closes H6, H7, H8, H9, H10-adapter, H16-wiring (r2-port-plan.md §3 Q4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd0289bea2
commit
3b9d9bb6be
6 changed files with 562 additions and 646 deletions
|
|
@ -119,19 +119,28 @@ public sealed class AnimationSequencer
|
|||
{
|
||||
// ── Public state ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Current style (stance) command.</summary>
|
||||
public uint CurrentStyle { get; private set; }
|
||||
|
||||
/// <summary>Current cyclic motion command.</summary>
|
||||
public uint CurrentMotion { get; private set; }
|
||||
/// <summary>
|
||||
/// Current style (stance) command — R2-Q4: read-only mirror of the
|
||||
/// retail <see cref="MotionState.Style"/> (MotionState OWNS
|
||||
/// style/substate/substate_mod; the adapter no longer keeps copies).
|
||||
/// </summary>
|
||||
public uint CurrentStyle => _state.Style;
|
||||
|
||||
/// <summary>
|
||||
/// Speed multiplier currently applied to the cyclic tail. Starts at 1.0
|
||||
/// and is updated by <see cref="SetCycle"/> when the same motion is
|
||||
/// re-issued with a different speed (which triggers
|
||||
/// <see cref="MultiplyCyclicFramerate"/> instead of a cycle restart).
|
||||
/// Current cyclic motion command — mirror of
|
||||
/// <see cref="MotionState.Substate"/>. NOTE (Q4): this is the
|
||||
/// POST-adjust_motion substate (WalkBackward reads back as WalkForward
|
||||
/// with a negative <see cref="CurrentSpeedMod"/>) — retail's interpreted
|
||||
/// state is post-adjustment.
|
||||
/// </summary>
|
||||
public float CurrentSpeedMod { get; private set; } = 1f;
|
||||
public uint CurrentMotion => _state.Substate;
|
||||
|
||||
/// <summary>
|
||||
/// Speed multiplier of the current substate — mirror of
|
||||
/// <see cref="MotionState.SubstateMod"/> (signed; see
|
||||
/// <see cref="CurrentMotion"/>).
|
||||
/// </summary>
|
||||
public float CurrentSpeedMod => _state.SubstateMod;
|
||||
|
||||
/// <summary>
|
||||
/// Sequence-wide velocity mirror of the core's <see cref="CSequence.Velocity"/>
|
||||
|
|
@ -221,6 +230,23 @@ public sealed class AnimationSequencer
|
|||
// accumulators.
|
||||
private readonly CSequence _core;
|
||||
|
||||
// R2-Q4: the verbatim motion-selection stack. CMotionTable resolves
|
||||
// (style, substate, speed) requests (Q2); MotionState owns
|
||||
// style/substate/mod + the modifier/action chains (Q1);
|
||||
// MotionTableManager owns the pending-animation queue + the
|
||||
// tick-countdown completion machinery (Q3). SetCycle/PlayAction are
|
||||
// thin shims over MotionTableManager.PerformMovement.
|
||||
private readonly CMotionTable _table;
|
||||
private readonly MotionState _state;
|
||||
private readonly MotionTableManager _manager;
|
||||
|
||||
// Retail lazy-create analog (MovementManager::get_minterp →
|
||||
// enter_default_state, r3-motioninterp-decomp §6g): the manager's
|
||||
// initialize_state runs on the FIRST SetCycle/PlayAction, not at
|
||||
// construction, so a sequencer that is never driven stays do-nothing
|
||||
// (RenderBootstrap invariant).
|
||||
private bool _initialized;
|
||||
|
||||
// Hooks pending dispatch. Accumulated during Advance (via the
|
||||
// AdapterHookQueue seam below); drained via ConsumePendingHooks.
|
||||
private readonly List<AnimationHook> _pendingHooks = new();
|
||||
|
|
@ -229,14 +255,6 @@ public sealed class AnimationSequencer
|
|||
// (retail CSequence::update(Frame*), 0x00525b80) — the old adapter
|
||||
// accumulator fields are gone with ConsumeRootMotionDelta.
|
||||
|
||||
// ── Diagnostics (Commit A 2026-05-03) ───────────────────────────────────
|
||||
// Throttle clock for the [SCFAST] / [SCFULL] / [SCNULLFALLBACK] log lines
|
||||
// emitted from SetCycle. Gated on env var ACDREAM_REMOTE_VEL_DIAG=1; reads
|
||||
// the env var inline rather than caching so a launch can be re-toggled
|
||||
// without restarting. 0.5s per sequencer instance keeps logs readable
|
||||
// while still capturing meaningful state changes.
|
||||
private double _lastSetCycleDiagTime;
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -259,6 +277,41 @@ public sealed class AnimationSequencer
|
|||
_loader = loader;
|
||||
_core = new CSequence(loader);
|
||||
_core.HookObj = new AdapterHookQueue(this);
|
||||
_table = new CMotionTable(motionTable);
|
||||
_state = new MotionState();
|
||||
_manager = new MotionTableManager(_table, _state, _core, new ForwardingMotionDoneSink(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q4: the entity's <see cref="MotionTableManager"/> — the caller's
|
||||
/// per-tick drive surface (<see cref="MotionTableManager.AnimationDone"/>
|
||||
/// per drained AnimDone hook, <see cref="MotionTableManager.UseTime"/>
|
||||
/// once per tick).
|
||||
/// </summary>
|
||||
public MotionTableManager Manager => _manager;
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q4 seam: where <see cref="IMotionDoneSink.MotionDone"/> lands.
|
||||
/// Null → dropped (diagnostic recorder bound by the host under
|
||||
/// ACDREAM_DUMP_MOTION); R3 binds MotionInterpreter.MotionDone here
|
||||
/// (register row: MotionDone observed-not-consumed until R3).
|
||||
/// </summary>
|
||||
public Action<uint, bool>? MotionDoneTarget { get; set; }
|
||||
|
||||
private sealed class ForwardingMotionDoneSink : IMotionDoneSink
|
||||
{
|
||||
private readonly AnimationSequencer _owner;
|
||||
public ForwardingMotionDoneSink(AnimationSequencer owner) => _owner = owner;
|
||||
public void MotionDone(uint motion, bool success)
|
||||
=> _owner.MotionDoneTarget?.Invoke(motion, success);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
_initialized = true;
|
||||
_manager.InitializeState();
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
|
@ -315,10 +368,16 @@ public sealed class AnimationSequencer
|
|||
/// preserve normal smooth transitions for everything else.</param>
|
||||
public void SetCycle(uint style, uint motion, float speedMod = 1f, bool skipTransitionLink = false)
|
||||
{
|
||||
// ── adjust_motion: remap left→right / backward→forward variants ───
|
||||
// ACE MotionInterp.cs:394-428. The MotionTable never stores TurnLeft,
|
||||
// SideStepLeft, or WalkBackward cycles; the client plays the mirror
|
||||
// animation with a negated speed so it runs backward.
|
||||
EnsureInitialized();
|
||||
|
||||
// ── Q4 boundary normalization (adjust_motion cyclic subset) ───────
|
||||
// Retail adjusts BEFORE the table sees the motion (CMotionInterp::
|
||||
// adjust_motion, R3 scope) — the interpreted state on the wire is
|
||||
// already post-adjustment, so remote-driven callers never pass these.
|
||||
// GameWindow's LOCAL-player path (UpdatePlayerAnimation) still passes
|
||||
// raw TurnLeft/SideStepLeft/WalkBackward, so the adapter normalizes
|
||||
// ONCE at this boundary. DELETED in R3-W6 when the local player
|
||||
// unifies onto MotionInterpreter (r3-port-plan.md J15).
|
||||
uint adjustedMotion = motion;
|
||||
float adjustedSpeed = speedMod;
|
||||
switch (motion & 0xFFFFu)
|
||||
|
|
@ -337,199 +396,35 @@ public sealed class AnimationSequencer
|
|||
break;
|
||||
}
|
||||
|
||||
// Fast-path: already playing this exact motion.
|
||||
//
|
||||
// Retail (ACE MotionTable.cs:132-139): when motion == current and
|
||||
// sign(speedMod) matches, DON'T restart the cycle — just rescale the
|
||||
// in-flight cyclic-tail's framerate via multiply_cyclic_animation_fr.
|
||||
// This keeps the run/walk loop smooth when a new UpdateMotion arrives
|
||||
// with a different ForwardSpeed (e.g. when the server broadcasts a
|
||||
// player's updated RunRate mid-step).
|
||||
//
|
||||
// **Sign-flip case (2026-05-02):** when the server sends adjust_motion'd
|
||||
// backward walk as `WalkForward + speed=-N`, motion stays 0x45000005
|
||||
// but speedMod sign flips. We MUST do a full cycle restart in that case
|
||||
// so the new (negative) framerate takes effect; otherwise the cycle
|
||||
// keeps playing forward with the old positive framerate and the
|
||||
// observer sees the player walking forward despite the negative speed.
|
||||
if (CurrentStyle == style && CurrentMotion == motion
|
||||
&& _core.FirstCyclic != null && _core.Count > 0
|
||||
&& MathF.Sign(speedMod) == MathF.Sign(CurrentSpeedMod))
|
||||
{
|
||||
if (MathF.Abs(speedMod - CurrentSpeedMod) > 1e-4f
|
||||
&& MathF.Abs(CurrentSpeedMod) > 1e-6f)
|
||||
{
|
||||
MultiplyCyclicFramerate(speedMod / CurrentSpeedMod);
|
||||
CurrentSpeedMod = speedMod;
|
||||
}
|
||||
// ── R2-Q4: dispatch through the verbatim motion-selection stack ───
|
||||
// Style change first (style-class ids route GetObjectSequence
|
||||
// Branch 1 — exit link + entry hop + target style's default cycle),
|
||||
// then the motion itself (Branch 2 cycle / 3 action / 4 modifier;
|
||||
// the Branch-2 fast re-speed path replaces the old adapter
|
||||
// fast-path, remove_redundant_links replaces Fix B, get_link's
|
||||
// reversed-key double-hop replaces the stop-anim fallback).
|
||||
if (style != 0 && style != _state.Style)
|
||||
_manager.PerformMovement(MotionTableMovement.Interpreted(style, 1f));
|
||||
|
||||
// D3 (Commit A 2026-05-03): SCFAST — proves whether the fast-path
|
||||
// is firing instead of the full rebuild. Throttled to 0.5s per
|
||||
// instance (re-throttled after A.1 unthrottled experiment).
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCFAST] motion=0x{motion:X8} speedMod={speedMod:F3} "
|
||||
+ $"oldSpeedMod={CurrentSpeedMod:F3} "
|
||||
+ $"qCount={_core.Count} "
|
||||
+ $"currNodeIsCyclic={ReferenceEquals(_core.CurrAnim, _core.FirstCyclic)}");
|
||||
_lastSetCycleDiagTime = nowSec;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint dispatchResult = _manager.PerformMovement(
|
||||
MotionTableMovement.Interpreted(adjustedMotion, adjustedSpeed));
|
||||
|
||||
// Resolve transition link (currentSubstate → adjustedMotion). Pass
|
||||
// both speeds — GetLink switches lookup branches based on sign.
|
||||
// CurrentSpeedMod defaults to 1.0 (positive) on a fresh sequencer,
|
||||
// so a Ready → WalkBackward transition correctly enters GetLink's
|
||||
// negative-speed (reversed-key) branch.
|
||||
// K-fix18: when the caller asked to skip the transition link
|
||||
// (instant-engage cases like Falling on jump start), force
|
||||
// linkData to null so only the cycle gets enqueued.
|
||||
MotionData? linkData = (skipTransitionLink || CurrentMotion == 0)
|
||||
? null
|
||||
: GetLink(style, CurrentMotion, CurrentSpeedMod, adjustedMotion, adjustedSpeed);
|
||||
|
||||
// Stop-anim fallback: dat-authored leg-settle / turn-stop links are
|
||||
// keyed under the FORWARD/RIGHT variant only. Stopping from
|
||||
// WalkBackward / SideStepLeft / TurnLeft hits a null linkData and
|
||||
// would visibly snap to Ready. The settle anim is direction-agnostic
|
||||
// (legs come to standing the same way regardless of which way you
|
||||
// were walking), so retry GetLink with the substate's low-byte
|
||||
// remapped to its forward/right peer.
|
||||
if (linkData is null && !skipTransitionLink && CurrentMotion != 0)
|
||||
{
|
||||
uint substateLow = CurrentMotion & 0xFFu;
|
||||
uint adjustedSubstate = substateLow switch
|
||||
{
|
||||
0x06u => (CurrentMotion & 0xFFFFFF00u) | 0x05u, // WalkBackward → WalkForward
|
||||
0x10u => (CurrentMotion & 0xFFFFFF00u) | 0x0Fu, // SideStepLeft → SideStepRight
|
||||
0x0Eu => (CurrentMotion & 0xFFFFFF00u) | 0x0Du, // TurnLeft → TurnRight
|
||||
_ => CurrentMotion,
|
||||
};
|
||||
if (adjustedSubstate != CurrentMotion)
|
||||
{
|
||||
linkData = GetLink(style, adjustedSubstate, CurrentSpeedMod, adjustedMotion, adjustedSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve target cycle using the ADJUSTED motion (TurnRight not TurnLeft).
|
||||
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu));
|
||||
_mtable.Cycles.TryGetValue(cycleKey, out var cycleData);
|
||||
|
||||
// ── Rebuild path (R1-P5) ───────────────────────────────────────────
|
||||
// remove_cyclic_anims (0x00524e40): drop the old cyclic tail; keep
|
||||
// any non-cyclic head that hasn't been played yet — the core snaps
|
||||
// curr_anim back to the preceding link node (or clears to empty).
|
||||
_core.RemoveCyclicAnims();
|
||||
|
||||
// K-fix18: when the caller asked for instant-engage, ALSO drain
|
||||
// any in-flight non-cyclic transition frames from the previous
|
||||
// cycle. Without this, the old RunForward → ??? link would
|
||||
// continue draining for ~100 ms before the new Falling cycle
|
||||
// starts, defeating the "skip the link" intent.
|
||||
// clear_animations (0x00524dc0): full wipe.
|
||||
// K-fix18 (2026-04-26, register row survives → R3): instant-engage
|
||||
// cases (Falling on jump start) strip the just-selected transition
|
||||
// links so the new cycle starts immediately. Post-Q4 expression:
|
||||
// the retail primitive CPhysicsObj::RemoveLinkAnimations
|
||||
// (0x0050fe20 → CSequence::remove_all_link_animations) — exactly
|
||||
// what R3-W4's LeaveGround will call, at which point this flag and
|
||||
// its GameWindow call sites are deleted. The pending-queue entry's
|
||||
// tick count intentionally stays (retail's RemoveLinkAnimations
|
||||
// does not touch the manager queue; the countdown chain absorbs it).
|
||||
if (skipTransitionLink)
|
||||
{
|
||||
_core.ClearAnimations();
|
||||
}
|
||||
|
||||
// Clear sequence-wide physics before the rebuild. Retail's
|
||||
// GetObjectSequence calls sequence.clear_physics() before each
|
||||
// add_motion chain (MotionTable.cs L100-L101, L152-L153).
|
||||
_core.ClearPhysics();
|
||||
|
||||
// Enqueue link frames (with adjusted speed for left→right remapping).
|
||||
if (linkData is { Anims.Count: > 0 })
|
||||
EnqueueMotionData(linkData, adjustedSpeed);
|
||||
|
||||
// Enqueue new cycle.
|
||||
if (cycleData is { Anims.Count: > 0 })
|
||||
{
|
||||
EnqueueMotionData(cycleData, adjustedSpeed);
|
||||
}
|
||||
else if (_core.Count == 0)
|
||||
{
|
||||
// No cycle and no link — nothing to play; reset fully.
|
||||
CurrentStyle = style;
|
||||
CurrentMotion = motion;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix B (both old AND new locomotion low-byte, #39 2026-05-06): for
|
||||
// direct cyclic-locomotion → cyclic-locomotion transitions
|
||||
// (Walk↔Run on Shift toggle, W↔S direct flip, A↔D, Forward↔Strafe),
|
||||
// land curr_anim on the new CYCLE, not on the link, and remove the
|
||||
// just-enqueued link nodes from the list entirely.
|
||||
//
|
||||
// Why: the transition link's drain time (~100–300 ms at
|
||||
// Framerate 30 × link runSpeed) gets restarted before it can
|
||||
// end if the user toggles Shift faster than that. curr_anim
|
||||
// sits on a fresh link every UM and Advance never reaches
|
||||
// the cycle. User observes "blips forward in walking
|
||||
// animation" — what they're seeing is the link's
|
||||
// interpolation pose, never the new cycle.
|
||||
//
|
||||
// Conditional on BOTH old AND new being locomotion cycles to
|
||||
// avoid regressing the cases where the link IS the right
|
||||
// animation:
|
||||
// - Idle (Ready) → any cycle: link is the wind-up pose
|
||||
// - Falling → Ready: landing animation
|
||||
// - Ready → Sitting/Crouching: pose-change links
|
||||
// - Combat substates (attack/parry/ready transitions)
|
||||
// Commit c06b6c5 (reverted in a2ae2ae) demonstrated that
|
||||
// unconditionally skipping the link breaks all of these.
|
||||
//
|
||||
// Retail reference: cdb live trace 2026-05-03 of a Walk→Run
|
||||
// direct transition logged
|
||||
// add_to_queue(45000005, looping=1) walk
|
||||
// add_to_queue(44000007, looping=1) run
|
||||
// with truncate_animation_list never firing — i.e. retail
|
||||
// appends the new cycle directly without a separate link
|
||||
// enqueue or visible link pose for cyclic→cyclic. Our
|
||||
// structural mismatch was always enqueueing link+cycle and
|
||||
// forcing curr onto the link; this fix matches retail's
|
||||
// observed semantics for the locomotion subset.
|
||||
//
|
||||
// Retail mechanism = remove_redundant_links (0x0051bf20, R2 scope,
|
||||
// gap map "Invented behaviors" note). Ported here as
|
||||
// remove_all_link_animations (the R1 core's verbatim remove-family
|
||||
// primitive) — this IS Fix B's outcome for the locomotion subset,
|
||||
// now expressed through the core instead of hand-rolled queue
|
||||
// surgery.
|
||||
bool prevIsLocomotion = IsLocomotionCycleLowByte(CurrentMotion & 0xFFu);
|
||||
bool newIsLocomotion = IsLocomotionCycleLowByte(motion & 0xFFu);
|
||||
if (prevIsLocomotion && newIsLocomotion && _core.FirstCyclic is not null)
|
||||
{
|
||||
_core.RemoveAllLinkAnimations();
|
||||
}
|
||||
|
||||
// D3 (Commit A 2026-05-03): SCFULL — counterpart to SCFAST. Fires on
|
||||
// the full-rebuild SetCycle path. Throttled to 0.5s per instance.
|
||||
// Logs prev CurrentMotion so the line shows the transition directly
|
||||
// (e.g. "Run → Ready" = cycle just got reset).
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCFULL] prev=0x{CurrentMotion:X8} -> motion=0x{motion:X8} adjustedMotion=0x{adjustedMotion:X8} "
|
||||
+ $"speedMod={speedMod:F3} "
|
||||
+ $"qCount={_core.Count} "
|
||||
+ $"currNodeIsCyclic={ReferenceEquals(_core.CurrAnim, _core.FirstCyclic)} "
|
||||
+ $"firstCyclicNull={(_core.FirstCyclic is null)}");
|
||||
_lastSetCycleDiagTime = nowSec;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentStyle = style;
|
||||
CurrentMotion = motion;
|
||||
CurrentSpeedMod = speedMod;
|
||||
// Failed dispatch (missing cycle for this style, is_allowed reject):
|
||||
// retail leaves sequence AND state untouched — no synthesis either.
|
||||
if (dispatchResult != MotionTableManagerError.Success)
|
||||
return;
|
||||
|
||||
// ── Synthesize CurrentVelocity for locomotion cycles ──────────────
|
||||
// The Humanoid motion table ships every locomotion MotionData with
|
||||
|
|
@ -637,48 +532,11 @@ public sealed class AnimationSequencer
|
|||
private const float RunAnimSpeed = 4.0f;
|
||||
private const float SidestepAnimSpeed = 1.25f;
|
||||
|
||||
/// <summary>
|
||||
/// Scale every cyclic node's framerate by <paramref name="factor"/>,
|
||||
/// mirroring retail's <c>multiply_cyclic_animation_fr</c> (0x00524940):
|
||||
/// walks <c>first_cyclic</c> through the tail of the list and scales
|
||||
/// each node's framerate. The non-cyclic head (link frames) is
|
||||
/// untouched — those drain at their original framerate, which matches
|
||||
/// retail: the sequencer "catches up" the transition before applying
|
||||
/// the new run speed.
|
||||
///
|
||||
/// <para>
|
||||
/// Called from <see cref="SetCycle"/> when the same (style, motion) pair
|
||||
/// is re-issued with a different speedMod — for instance, when a remote
|
||||
/// player's ForwardSpeed changes mid-run. Does NOT restart the animation,
|
||||
/// so footsteps keep planting where they are.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// R1-P5 / gap-map G13: the core's <c>MultiplyCyclicAnimationFramerate</c>
|
||||
/// touches ONLY node framerates (retail-verbatim). The velocity/omega
|
||||
/// rescale below is the ADAPTER-level stand-in for retail's
|
||||
/// <c>change_cycle_speed</c> composite (<c>subtract_motion(oldSpeed)</c>
|
||||
/// + <c>combine_motion(newSpeed)</c>, algebraically equal to scaling by
|
||||
/// newSpeed/oldSpeed) — an R2 mechanism this adapter keeps inline until
|
||||
/// R2 routes speed changes through the real subtract/combine_motion
|
||||
/// pair. See gap map G13 for the register-row rationale.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="factor">Framerate multiplier (newSpeed / oldSpeed).</param>
|
||||
public void MultiplyCyclicFramerate(float factor)
|
||||
{
|
||||
if (_core.FirstCyclic == null) return;
|
||||
if (factor < 0f || float.IsNaN(factor) || float.IsInfinity(factor))
|
||||
return;
|
||||
|
||||
_core.MultiplyCyclicAnimationFramerate(factor);
|
||||
|
||||
// R2-composite stand-in (G13): retail's change_cycle_speed rescales
|
||||
// velocity/omega via subtract_motion(old)+combine_motion(new), which
|
||||
// is algebraically newSpeed/oldSpeed — exactly this factor.
|
||||
_core.SetVelocity(_core.Velocity * factor);
|
||||
_core.SetOmega(_core.Omega * factor);
|
||||
}
|
||||
// R2-Q4: MultiplyCyclicFramerate DELETED (zero external callers). The
|
||||
// gap-map-G13 composite stand-in (framerate scale + velocity/omega
|
||||
// rescale) is retired — same-motion re-speeds now route through the
|
||||
// verbatim CMotionTable fast re-speed path (change_cycle_speed +
|
||||
// subtract_motion(old) + combine_motion(new), decomp §5).
|
||||
|
||||
/// <summary>
|
||||
/// Advance the animation by <paramref name="dt"/> seconds and return the
|
||||
|
|
@ -751,150 +609,48 @@ public sealed class AnimationSequencer
|
|||
// contract — for R6's per-tick wiring.
|
||||
|
||||
/// <summary>
|
||||
/// Play a one-shot action/modifier motion (Jump, emote, attack, etc.)
|
||||
/// on top of the current cycle. The action frames are inserted in the
|
||||
/// list immediately before the looping cyclic tail; they drain once
|
||||
/// and then the cycle resumes naturally.
|
||||
/// Play a one-shot action / modifier motion (Jump, emote, attack, etc.)
|
||||
/// on top of the current cycle.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail semantics: actions and modifiers live in
|
||||
/// <see cref="MotionTable.Modifiers"/> (a separate dict from
|
||||
/// <see cref="MotionTable.Cycles"/>) keyed by
|
||||
/// <c>(style << 16) | (motion & 0xFFFFFF)</c>. A motion like
|
||||
/// <c>Jump = 0x2500003b</c> is a Modifier (class byte 0x25) not a
|
||||
/// SubState — feeding it to <see cref="SetCycle"/> silently fails the
|
||||
/// cycle lookup. Routing through <c>PlayAction</c> instead resolves
|
||||
/// from the Modifiers table and interleaves the action frames with
|
||||
/// the ongoing cyclic motion.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// If no entry is found in the Modifiers table for the requested
|
||||
/// motion, this is a no-op.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// R1-P5: inserts directly on the core's internal list via
|
||||
/// <see cref="CSequence.AnimList"/> / <see cref="CSequence.FirstCyclicNode"/> /
|
||||
/// <see cref="CSequence.CurrAnimNode"/> (internal members visible within
|
||||
/// this assembly). Preserves the exact pre-cutover semantics: action
|
||||
/// nodes insert ahead of the cyclic tail and play once; if the cursor
|
||||
/// was sitting on the cyclic tail (or nothing was playing), it jumps to
|
||||
/// the first inserted action node so the action plays immediately.
|
||||
/// R2-Q4: a thin shim over
|
||||
/// <see cref="MotionTableManager.PerformMovement"/> — action-class ids
|
||||
/// (0x10000000) route <c>GetObjectSequence</c> Branch 3 (rebuild:
|
||||
/// substate→action link + base cycle re-added, the action tracked on the
|
||||
/// MotionState action FIFO and popped by the manager's countdown);
|
||||
/// modifier-class ids (0x20000000) route Branch 4 (PHYSICS-ONLY
|
||||
/// <c>combine_motion</c> — no animation frames; the pre-Q4
|
||||
/// insert-before-tail modifier-anim mechanism was an acdream invention,
|
||||
/// deleted). Unknown ids fail the dispatch and are a no-op, matching the
|
||||
/// pre-Q4 contract.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="motionCommand">Raw MotionCommand (e.g. 0x2500003b for Jump).</param>
|
||||
/// <param name="speedMod">Speed multiplier for the action's framerate.</param>
|
||||
public void PlayAction(uint motionCommand, float speedMod = 1f)
|
||||
{
|
||||
// Resolve motion data. The lookup depends on the command's mask class:
|
||||
//
|
||||
// - Action (mask 0x10): stored in the Links dict as the transition
|
||||
// FROM currentSubstate TO the action motion. Matches ACE
|
||||
// MotionTable.GetObjectSequence @ line 189-207 (CommandMask.Action).
|
||||
// - Modifier (mask 0x20): stored in the Modifiers dict, keyed by
|
||||
// (style<<16) | (motion&0xFFFFFF) (or unstyled key). Matches ACE
|
||||
// @ line 234-242 (CommandMask.Modifier).
|
||||
//
|
||||
// Jump (0x2500003B) has BOTH bits set (0x20|0x04|0x01) but ACE treats
|
||||
// it via the Modifier path. FallDown (0x10000050) / Jumpup (0x1000004B)
|
||||
// are pure Actions (mask 0x10) and live in Links.
|
||||
//
|
||||
// We try Links first (via GetLink, which reproduces ACE's get_link
|
||||
// fallback chain). If that fails and the motion is a Modifier, fall
|
||||
// through to the Modifiers dict.
|
||||
const uint ActionMask = 0x10000000u;
|
||||
const uint ModifierMask = 0x20000000u;
|
||||
|
||||
MotionData? data = null;
|
||||
if ((motionCommand & ActionMask) != 0 && CurrentMotion != 0)
|
||||
{
|
||||
// Action: look up the transition link from current substate → action.
|
||||
// Action overlays always play forward (positive speeds) — the
|
||||
// action speed mod is the caller-supplied modifier, not part of
|
||||
// the substate cycle's direction.
|
||||
data = GetLink(CurrentStyle, CurrentMotion, /*substateSpeed:*/ 1f, motionCommand, /*speed:*/ 1f);
|
||||
}
|
||||
if (data is null && (motionCommand & ModifierMask) != 0)
|
||||
{
|
||||
uint styleKey = CurrentStyle << 16;
|
||||
int keyStyled = (int)(styleKey | (motionCommand & 0xFFFFFFu));
|
||||
int keyPlain = (int)(motionCommand & 0xFFFFFFu);
|
||||
if (!_mtable.Modifiers.TryGetValue(keyStyled, out data))
|
||||
_mtable.Modifiers.TryGetValue(keyPlain, out data);
|
||||
}
|
||||
|
||||
if (data is null || data.Anims.Count == 0)
|
||||
return;
|
||||
|
||||
// Build AnimSequenceNodes from the action's AnimData list. All
|
||||
// non-looping — they drain once, then the list falls through to
|
||||
// first_cyclic. Velocity/omega gate matches EnqueueMotionData (G17).
|
||||
Vector3 vel = data.Flags.HasFlag(MotionDataFlags.HasVelocity)
|
||||
? data.Velocity * speedMod : Vector3.Zero;
|
||||
Vector3 omg = data.Flags.HasFlag(MotionDataFlags.HasOmega)
|
||||
? data.Omega * speedMod : Vector3.Zero;
|
||||
|
||||
var newNodes = new List<AnimSequenceNode>(data.Anims.Count);
|
||||
foreach (var ad in data.Anims)
|
||||
{
|
||||
var node = BuildNode(ad, speedMod);
|
||||
if (node.HasAnim) newNodes.Add(node);
|
||||
}
|
||||
if (newNodes.Count == 0) return;
|
||||
|
||||
var animList = _core.AnimList;
|
||||
var firstCyclicNode = _core.FirstCyclicNode;
|
||||
|
||||
// Insert before the cyclic tail (so the action plays, then cycle
|
||||
// resumes). If there's no cyclic tail yet, append at the end.
|
||||
LinkedListNode<AnimSequenceNode>? firstInserted = null;
|
||||
if (firstCyclicNode != null)
|
||||
{
|
||||
foreach (var n in newNodes)
|
||||
{
|
||||
var inserted = animList.AddBefore(firstCyclicNode, n);
|
||||
firstInserted ??= inserted;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var n in newNodes)
|
||||
{
|
||||
var inserted = animList.AddLast(n);
|
||||
firstInserted ??= inserted;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Flags.HasFlag(MotionDataFlags.HasVelocity))
|
||||
_core.SetVelocity(vel);
|
||||
if (data.Flags.HasFlag(MotionDataFlags.HasOmega))
|
||||
_core.SetOmega(omg);
|
||||
|
||||
// If we're currently on the cyclic tail (or nothing was playing),
|
||||
// jump the cursor back to the first newly-inserted action node so
|
||||
// the action plays immediately instead of after the next cycle wrap.
|
||||
var currNode = _core.CurrAnimNode;
|
||||
bool cursorOnCyclic = currNode != null && ReferenceEquals(currNode, firstCyclicNode);
|
||||
if (cursorOnCyclic || currNode == null)
|
||||
{
|
||||
_core.CurrAnimNode = firstInserted;
|
||||
if (firstInserted != null)
|
||||
_core.FrameNumber = firstInserted.Value.GetStartingFrame();
|
||||
}
|
||||
EnsureInitialized();
|
||||
_manager.PerformMovement(MotionTableMovement.Interpreted(motionCommand, speedMod));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the sequencer to an unplaying state without clearing the
|
||||
/// motion table reference.
|
||||
/// motion table reference. R2-Q4: drains the pending-animation queue
|
||||
/// (exit-world semantics — each entry fires MotionDone(success:false))
|
||||
/// and zeroes the MotionState so the next SetCycle re-runs
|
||||
/// initialize_state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_manager.HandleExitWorld();
|
||||
_core.Clear();
|
||||
_pendingHooks.Clear();
|
||||
CurrentStyle = 0;
|
||||
CurrentMotion = 0;
|
||||
CurrentSpeedMod = 1f;
|
||||
_state.Style = 0;
|
||||
_state.Substate = 0;
|
||||
_state.SubstateMod = 1f;
|
||||
_state.ClearModifiers();
|
||||
_state.ClearActions();
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────────────────
|
||||
|
|
@ -925,150 +681,13 @@ public sealed class AnimationSequencer
|
|||
private static readonly AnimationDoneHook AnimationDoneSentinel =
|
||||
new() { Direction = AnimationHookDir.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Look up the transition MotionData for going from <paramref name="substate"/>
|
||||
/// (current state, played at <paramref name="substateSpeed"/>) to
|
||||
/// <paramref name="motion"/> (new state, played at <paramref name="speed"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// Port of ACE's MotionTable.get_link (MotionTable.cs:395-426). The lookup
|
||||
/// path differs by sign of the speeds — the retail/ACE mechanism is two
|
||||
/// distinct branches:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Both speeds positive</b> (forward → forward, normal case):
|
||||
/// Look up Links[(style<<16) | substate][motion] — the link FROM
|
||||
/// substate TO motion. Played forward.</item>
|
||||
/// <item><b>Either speed negative</b> (any direction reversal —
|
||||
/// WalkBackward, SideStepLeft, TurnLeft): Look up the REVERSED key
|
||||
/// Links[(style<<16) | motion][substate] — the link FROM motion TO
|
||||
/// substate. Played in reverse, this anim visually transitions
|
||||
/// substate → motion's pose, then the cycle continues from where it
|
||||
/// left off. Without this branch, Ready→WalkBackward would queue the
|
||||
/// "start walking forward" link played in reverse, which strands the
|
||||
/// cursor at the wrong cycle frame and causes the user-visible
|
||||
/// "left leg twitches forward two times" glitch on the X key.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// DatReaderWriter encodes Links as Dictionary<int, MotionCommandData>
|
||||
/// where MotionCommandData.MotionData is Dictionary<int, MotionData>.
|
||||
/// </summary>
|
||||
private MotionData? GetLink(uint style, uint substate, float substateSpeed, uint motion, float speed)
|
||||
{
|
||||
if (speed < 0f || substateSpeed < 0f)
|
||||
{
|
||||
// Reversed-direction path: link FROM motion TO substate.
|
||||
int reversedKey = (int)((style << 16) | (motion & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(reversedKey, out var revLink)
|
||||
&& revLink.MotionData.TryGetValue((int)substate, out var revResult))
|
||||
{
|
||||
return revResult;
|
||||
}
|
||||
// R2-Q4: GetLink DELETED - re-homed verbatim as CMotionTable.GetLink
|
||||
// (Q2, field-validated reversed-key branch preserved; Q0-pins A1).
|
||||
|
||||
// Style-defaults fallback per ACE MotionTable.cs:405-409.
|
||||
if (_mtable.StyleDefaults.TryGetValue(
|
||||
(DatReaderWriter.Enums.MotionCommand)style, out var defaultMotion))
|
||||
{
|
||||
int subKey = (int)((style << 16) | (substate & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(subKey, out var subLink)
|
||||
&& subLink.MotionData.TryGetValue((int)defaultMotion, out var subResult))
|
||||
{
|
||||
return subResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Forward-direction path: link FROM substate TO motion (the original
|
||||
// implementation pre-K-fix6).
|
||||
int outerKey1 = (int)((style << 16) | (substate & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(outerKey1, out var cmd1)
|
||||
&& cmd1.MotionData.TryGetValue((int)motion, out var result1))
|
||||
{
|
||||
return result1;
|
||||
}
|
||||
|
||||
// Fallback: style-level catch-all (ACE line 419-422).
|
||||
int outerKey2 = (int)(style << 16);
|
||||
if (_mtable.Links.TryGetValue(outerKey2, out var cmd2)
|
||||
&& cmd2.MotionData.TryGetValue((int)motion, out var result2))
|
||||
{
|
||||
return result2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build one <see cref="AnimSequenceNode"/> from an <see cref="AnimData"/>,
|
||||
/// speed-scaled retail-style (only Framerate scales — retail
|
||||
/// <c>AnimData::operator*</c>, 0x00525d00). Returns a node whose
|
||||
/// <see cref="AnimSequenceNode.HasAnim"/> may be false (unresolved dat
|
||||
/// id); callers filter those out.
|
||||
/// </summary>
|
||||
private AnimSequenceNode BuildNode(AnimData ad, float speedMod)
|
||||
{
|
||||
var scaled = new AnimData
|
||||
{
|
||||
AnimId = ad.AnimId,
|
||||
LowFrame = ad.LowFrame,
|
||||
HighFrame = ad.HighFrame,
|
||||
Framerate = ad.Framerate * speedMod,
|
||||
};
|
||||
return new AnimSequenceNode(scaled, _loader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append all AnimData entries from <paramref name="motionData"/> to the
|
||||
/// core's animation list (retail free function <c>add_motion</c>,
|
||||
/// 0x005224b0). Each AnimData becomes one <see cref="AnimSequenceNode"/>
|
||||
/// via <see cref="CSequence.AppendAnimation"/>, which slides
|
||||
/// <c>first_cyclic</c> to the just-appended node on EVERY call (G10) —
|
||||
/// so a multi-anim MotionData's loop membership is naturally "only the
|
||||
/// LAST AnimData loops" once a subsequent MotionData is appended, and a
|
||||
/// single MotionData with N AnimData entries makes the WHOLE MotionData
|
||||
/// the cyclic tail as of its last entry.
|
||||
/// </summary>
|
||||
/// <param name="motionData">The MotionData to enqueue.</param>
|
||||
/// <param name="speedMod">Speed multiplier for AnimData framerate scaling.</param>
|
||||
/// <remarks>
|
||||
/// The <c>isLooping</c> parameter from the pre-cutover signature is
|
||||
/// deleted — retail has no per-call loop flag (G10); loop membership is
|
||||
/// purely a function of append order via <c>first_cyclic</c>.
|
||||
/// </remarks>
|
||||
private void EnqueueMotionData(MotionData motionData, float speedMod)
|
||||
{
|
||||
// Sequence-wide velocity/omega: retail add_motion (0x005224b0) calls
|
||||
// set_velocity/set_omega UNCONDITIONALLY (G17 core semantics — a
|
||||
// MotionData without HasVelocity carries a zero Velocity field, so
|
||||
// "unconditional replace" and "replace with zero" are the same
|
||||
// retail call). The adapter keeps today's HasVelocity/HasOmega GATE
|
||||
// (G17 adapter split) so a transient zero-velocity modifier doesn't
|
||||
// stomp a running cycle's velocity — this is the documented R2
|
||||
// stand-in until modifiers route through combine_motion instead of
|
||||
// add_motion. See gap map G17.
|
||||
if (motionData.Flags.HasFlag(MotionDataFlags.HasVelocity))
|
||||
_core.SetVelocity(motionData.Velocity * speedMod);
|
||||
if (motionData.Flags.HasFlag(MotionDataFlags.HasOmega))
|
||||
_core.SetOmega(motionData.Omega * speedMod);
|
||||
|
||||
foreach (var ad in motionData.Anims)
|
||||
{
|
||||
// Speed-scale (retail AnimData::operator*, 0x00525d00 — ONLY
|
||||
// framerate scales) then hand the scaled AnimData straight to
|
||||
// append_animation; the core resolves + clamps via
|
||||
// AnimSequenceNode.SetAnimationId and silently discards nodes
|
||||
// whose dat id doesn't resolve (mirrors BuildNode's HasAnim
|
||||
// filter without constructing a throwaway node here).
|
||||
_core.AppendAnimation(new AnimData
|
||||
{
|
||||
AnimId = ad.AnimId,
|
||||
LowFrame = ad.LowFrame,
|
||||
HighFrame = ad.HighFrame,
|
||||
Framerate = ad.Framerate * speedMod,
|
||||
});
|
||||
}
|
||||
}
|
||||
// R2-Q4: BuildNode + EnqueueMotionData DELETED - MotionData appends are
|
||||
// owned by CMotionTable.AddMotion (add_motion 0x005224b0, unconditional
|
||||
// velocity/omega set = G17 core semantics; the adapter's HasVelocity/
|
||||
// HasOmega gate is retired with them).
|
||||
|
||||
/// <summary>
|
||||
/// Build the per-part blended transform from the current animation frame.
|
||||
|
|
@ -1166,19 +785,10 @@ public sealed class AnimationSequencer
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the given motion-low-byte names a locomotion cycle —
|
||||
/// WalkForward (0x05), WalkBackward (0x06), RunForward (0x07),
|
||||
/// SideStepRight (0x0F), or SideStepLeft (0x10).
|
||||
/// Used by <see cref="SetCycle"/> to recognise cyclic→cyclic
|
||||
/// direct transitions and bypass the transition link in that case
|
||||
/// (retail's observed add_to_queue semantics).
|
||||
/// </summary>
|
||||
private static bool IsLocomotionCycleLowByte(uint lowByte)
|
||||
{
|
||||
return lowByte == 0x05u || lowByte == 0x06u || lowByte == 0x07u
|
||||
|| lowByte == 0x0Fu || lowByte == 0x10u;
|
||||
}
|
||||
// R2-Q4: IsLocomotionCycleLowByte DELETED with Fix B - the cyclic-to-
|
||||
// cyclic link-skip is now retail's remove_redundant_links (0x0051bf20,
|
||||
// MotionTableManager.RemoveRedundantLinks) operating on the pending
|
||||
// queue, not a locomotion-subset special case in the adapter.
|
||||
|
||||
/// <summary>
|
||||
/// Quaternion slerp matching the retail client's <c>FUN_005360d0</c>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue