fix(anim): implement adjust_motion — TurnLeft/SideStepLeft play backward
ROOT CAUSE FIX for missing left-side animations. The AC client's MotionTable has NO cycles for TurnLeft (0x000E), SideStepLeft (0x0010), or WalkBackward (0x0006). The real client calls adjust_motion() which remaps these to their right-side equivalents with NEGATIVE speed before looking up the cycle. Then multiply_framerate() swaps LowFrame↔HighFrame so the animation plays backward. Source: ACE MotionInterp.cs:394-428, decompiled FUN_005267E0. Changes: - AnimationSequencer.SetCycle: adds adjust_motion block that remaps left→right with speed *= -1 (TurnLeft, SideStepLeft) or speed *= -0.65 (WalkBackward = BackwardsFactor) - LoadAnimNode: when framerate < 0, swaps Low↔High (matching the decompiled multiply_framerate) - GameWindow.UpdatePlayerAnimation: passes original animCommand to SetCycle (sequencer handles remapping internally), keeps legacy fallback for non-sequencer entities Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e66078e57
commit
67b51a3e6f
3 changed files with 339 additions and 35 deletions
|
|
@ -2054,47 +2054,35 @@ public sealed class GameWindow : IDisposable
|
|||
stanceOverride: NonCombatStance,
|
||||
commandOverride: cmdOverride);
|
||||
|
||||
// AC reuses right-side animations for left-side motions (played in
|
||||
// reverse). If the left-side command has no cycle, fall back to the
|
||||
// right-side equivalent so the player isn't stuck in idle.
|
||||
uint resolvedCommand = animCommand; // track which command actually resolved
|
||||
// The sequencer handles left→right remapping internally via
|
||||
// adjust_motion (TurnLeft→TurnRight with negative speed, etc.).
|
||||
// Pass the ORIGINAL animCommand — SetCycle does the remapping.
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
|
||||
ae.Sequencer.SetCycle(fullStyle, animCommand);
|
||||
}
|
||||
|
||||
// Legacy path fallback: for the non-sequencer slerp path, do the
|
||||
// left→right remapping here since that path doesn't have adjust_motion.
|
||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame)
|
||||
{
|
||||
ushort fallback = cmdOverride switch
|
||||
{
|
||||
0x000E => 0x000D, // TurnLeft → TurnRight
|
||||
0x0010 => 0x000F, // SideStepLeft → SideStepRight
|
||||
0x0006 => 0x0005, // WalkBackward → WalkForward
|
||||
0x000E => 0x000D,
|
||||
0x0010 => 0x000F,
|
||||
0x0006 => 0x0005,
|
||||
_ => (ushort)0,
|
||||
};
|
||||
if (fallback != 0)
|
||||
{
|
||||
cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
|
||||
ae.Setup, _dats,
|
||||
motionTableIdOverride: _playerMotionTableId,
|
||||
stanceOverride: NonCombatStance,
|
||||
commandOverride: fallback);
|
||||
// Update resolvedCommand so the sequencer looks up the right cycle
|
||||
resolvedCommand = (animCommand & 0xFF000000u) | fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame)
|
||||
{
|
||||
Console.WriteLine($"[ANIM-DIAG] FAILED: animCmd=0x{animCommand:X8} resolved=0x{resolvedCommand:X8} cmdOverride=0x{cmdOverride:X4} cycle={cycle is not null} fr={cycle?.Framerate} lo={cycle?.LowFrame} hi={cycle?.HighFrame}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[ANIM-DIAG] OK: animCmd=0x{animCommand:X8} resolved=0x{resolvedCommand:X8} hasSeq={ae.Sequencer is not null} fr={cycle.Framerate} lo={cycle.LowFrame} hi={cycle.HighFrame}");
|
||||
|
||||
// If the entity has a sequencer, use SetCycle for transition-link-aware
|
||||
// motion switching. Pass the RESOLVED command (after left→right fallback)
|
||||
// so the sequencer's internal cycle lookup finds the same animation.
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
|
||||
ae.Sequencer.SetCycle(fullStyle, resolvedCommand);
|
||||
}
|
||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
||||
|
||||
ae.Animation = cycle.Animation;
|
||||
ae.LowFrame = Math.Max(0, cycle.LowFrame);
|
||||
|
|
|
|||
|
|
@ -173,32 +173,55 @@ public sealed class AnimationSequencer
|
|||
/// <param name="speedMod">Speed multiplier applied to framerates (1.0 = normal).</param>
|
||||
public void SetCycle(uint style, uint motion, float speedMod = 1f)
|
||||
{
|
||||
// ── adjust_motion: remap left→right variants with negative speed ───
|
||||
// The AC client's MotionTable has NO cycles for TurnLeft, SideStepLeft,
|
||||
// or WalkBackward. These are played as their right-side / forward
|
||||
// equivalents with negative framerate (animation runs backward).
|
||||
// ACE: MotionInterp.cs:394-428
|
||||
uint adjustedMotion = motion;
|
||||
float adjustedSpeed = speedMod;
|
||||
switch (motion & 0xFFFFu)
|
||||
{
|
||||
case 0x000E: // TurnLeft → TurnRight
|
||||
adjustedMotion = (motion & 0xFFFF0000u) | 0x000Du;
|
||||
adjustedSpeed *= -1f;
|
||||
break;
|
||||
case 0x0010: // SideStepLeft → SideStepRight
|
||||
adjustedMotion = (motion & 0xFFFF0000u) | 0x000Fu;
|
||||
adjustedSpeed *= -1f;
|
||||
break;
|
||||
case 0x0006: // WalkBackward → WalkForward
|
||||
adjustedMotion = (motion & 0xFFFF0000u) | 0x0005u;
|
||||
adjustedSpeed *= -0.65f; // BackwardsFactor from ACE
|
||||
break;
|
||||
}
|
||||
|
||||
// Fast-path: already playing this exact motion at the same speed.
|
||||
if (CurrentStyle == style && CurrentMotion == motion
|
||||
&& _firstCyclic != null && _queue.Count > 0)
|
||||
return;
|
||||
|
||||
// Resolve transition link (currentSubstate → newMotion).
|
||||
// Resolve transition link (currentSubstate → adjustedMotion).
|
||||
MotionData? linkData = CurrentMotion != 0
|
||||
? GetLink(style, CurrentMotion, motion)
|
||||
? GetLink(style, CurrentMotion, adjustedMotion)
|
||||
: null;
|
||||
|
||||
// Resolve target cycle.
|
||||
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (motion & 0xFFFFFFu));
|
||||
// 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);
|
||||
|
||||
// Clear the old cyclic tail; keep any non-cyclic head that hasn't
|
||||
// been played yet (ACE behaviour: non-cyclic anims drain naturally).
|
||||
ClearCyclicTail();
|
||||
|
||||
// Enqueue link frames.
|
||||
// Enqueue link frames (with adjusted speed for left→right remapping).
|
||||
if (linkData is { Anims.Count: > 0 })
|
||||
EnqueueMotionData(linkData, speedMod, isLooping: false);
|
||||
EnqueueMotionData(linkData, adjustedSpeed, isLooping: false);
|
||||
|
||||
// Enqueue new cycle.
|
||||
if (cycleData is { Anims.Count: > 0 })
|
||||
{
|
||||
EnqueueMotionData(cycleData, speedMod, isLooping: true);
|
||||
EnqueueMotionData(cycleData, adjustedSpeed, isLooping: true);
|
||||
}
|
||||
else if (_queue.Count == 0)
|
||||
{
|
||||
|
|
@ -366,9 +389,23 @@ public sealed class AnimationSequencer
|
|||
if (low >= numFrames) low = numFrames - 1;
|
||||
if (high >= numFrames) high = numFrames - 1;
|
||||
if (low < 0) low = 0;
|
||||
if (low > high) high = low;
|
||||
|
||||
float fr = ad.Framerate * speedMod;
|
||||
|
||||
// multiply_framerate: when speed is negative (TurnLeft, SideStepLeft),
|
||||
// swap Low↔High so the animation plays backward. This is exactly what
|
||||
// the decompiled FUN_005267E0 does. ACE: AnimData.GetFramerate(speed).
|
||||
// After swap, LowFrame > HighFrame — the Advance loop handles this
|
||||
// by checking negative frametime against LowFrame (the higher value).
|
||||
if (fr < 0f)
|
||||
{
|
||||
(low, high) = (high, low);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (low > high) high = low; // only clamp for positive-speed case
|
||||
}
|
||||
|
||||
return new AnimNode(anim, fr, low, high, isLooping);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue