fix(anim): jump animation via Falling SubState + kept PlayAction infra for emotes
The actual retail behavior for jump animations is a plain SubState swap —
NOT an Action overlay as I'd initially guessed. MotionCommand.Falling
(0x40000015) is a SubState cycle whose motion-table entries handle the
whole jump lifecycle:
- Links[(stance, RunForward)][Falling] = leap-into-air link
- Cycles[(stance, Falling)] = airborne cycle (loops)
- Links[(stance, Falling)][Ready/...] = landing link back to normal
Empirical verification from the diagnostic dump:
Links[0x003D0007] has 3 inner entries:
inner key: 0x41000003 (Ready)
inner key: 0x45000005 (WalkForward)
inner key: 0x40000015 (Falling) ← jackpot
SetCycle() already handles SubState + Links + Cycles resolution correctly,
so the whole fix is three lines:
if (!result.IsOnGround)
animCommand = MotionCommand.Falling;
What's in this commit:
- Added MotionCommand.Falling (0x40000015) constant + comments explaining
the retail jump-is-a-SubState flow
- GameWindow.UpdatePlayerAnimation swaps to Falling when airborne (the
cleanest possible implementation — motion table does all the work)
- Kept AnimationSequencer.PlayAction infrastructure (ported via Links
fallback + Modifiers fallback). Not needed for jump, but perfectly
valid for emotes like /wave, /bow (found in the same Links dict as
inner keys 0x13000080-0x13000083) and eventual combat attacks
- Kept MotionCommand.Jump / Jumpup / FallDown constants (unused for now
but useful reference if non-humanoid motion tables use them)
- Removed all diagnostic logging
What was learned (for future motion work):
- Retail's MotionTable.Cycles dict holds SubState loops (Ready, Walk,
Run, Falling, Crouch, etc.) by (style<<16) | (motion & 0xFFFFFF)
- MotionTable.Links dict holds TRANSITIONS between motions: the OUTER
key is the (style, fromMotion) combo; the INNER key is the TARGET
motion. The stored MotionData IS the link animation played during
the transition. This is what ACE's get_link traverses.
- MotionTable.Modifiers dict holds overlay motions (mask 0x20) — rare
for humanoids, only 8 TurnRight/SideStepRight stance variants
- Actions (mask 0x10) in retail ALSO go through Links — they're
transition animations FROM current substate, not overlays. Use
PlayAction (now correctly routed to Links dict) for them.
Jump animation now works retail-faithfully for running + jumping off.
Standing-jump behavior depends on whether the player's motion table
has a Ready→Falling link; SetCycle's fallback chain should handle it
via the style-level catch-all if the direct link is absent.
470 tests pass. Build clean.
This commit is contained in:
parent
08ea2c0af8
commit
6bce9b8019
3 changed files with 70 additions and 48 deletions
|
|
@ -3037,46 +3037,30 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
if (_dats is null) return;
|
if (_dats is null) return;
|
||||||
|
|
||||||
// ── Action-motion events (jump / land) ─────────────────────────────
|
// ── Airborne SubState (Falling) ────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Retail does NOT animate jumps — confirmed via ACE's HandleActionJump
|
// Retail models the jump-animation as a SubState swap to
|
||||||
// (Player.cs:914-915) which explicitly clears PendingMotions and
|
// MotionCommand.Falling (0x40000015) while airborne, NOT as an
|
||||||
// sets IsAnimating=false during the jump. The character keeps
|
// Action overlay. Empirically verified: Links[(NonCombat,RunForward)]
|
||||||
// whatever cycle it was on and the physics body arcs through the air.
|
// has 3 transitions including 0x40000015 Falling. The SubState cycle
|
||||||
// Humanoid Setup MotionTables have NO entry for Jump (0x2500003B)
|
// for Falling lives in Cycles[(style, Falling)] and loops while
|
||||||
// or FallDown (0x10000050) in the Modifiers dict — verified empirically
|
// airborne. On land, we transition back to whatever SubState the
|
||||||
// (only 8 TurnRight stance-variants + SideStepRight).
|
// motion input implies (Ready / WalkForward / RunForward).
|
||||||
//
|
//
|
||||||
// We still call PlayAction here as a no-op safety hatch: if a future
|
// Implementation: force animCommand = Falling when airborne; the
|
||||||
// Setup / creature DOES carry a jump/fall modifier in its MotionTable
|
// existing SetCycle pathway resolves the link + cycle correctly and
|
||||||
// (e.g. a leaping-monster) the sequencer will pick it up for free.
|
// the transition back happens naturally when airborne becomes false.
|
||||||
// For player humanoids, the lookup silently misses and nothing changes.
|
|
||||||
if (result.JumpExtent.HasValue || result.JustLanded)
|
|
||||||
{
|
|
||||||
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var actionPe)
|
|
||||||
&& _animatedEntities.TryGetValue(actionPe.Id, out var actionAe)
|
|
||||||
&& actionAe.Sequencer is not null)
|
|
||||||
{
|
|
||||||
if (result.JumpExtent.HasValue)
|
|
||||||
actionAe.Sequencer.PlayAction(AcDream.Core.Physics.MotionCommand.Jump);
|
|
||||||
if (result.JustLanded)
|
|
||||||
actionAe.Sequencer.PlayAction(AcDream.Core.Physics.MotionCommand.FallDown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the animation command: forward takes priority, then sidestep,
|
// Determine the animation command: airborne takes priority (Falling
|
||||||
// then turn, then idle (Ready 0x41000003).
|
// SubState), then forward, sidestep, turn, then idle (Ready 0x41000003).
|
||||||
//
|
//
|
||||||
// Note: AC's Jump (0x2500003b) is an Action motion (mask 0x25000000),
|
// Airborne → Falling (retail behavior; see airborne note above).
|
||||||
// NOT a SubState cycle. Feeding it to the MotionTable resolver via
|
// Otherwise: LocalAnimationCommand (RunForward when running) preferred,
|
||||||
// SetCycle produces a failed cycle lookup — which mis-renders the
|
// falling back to wire ForwardCommand (WalkForward / WalkBackward).
|
||||||
// character. Proper action playback needs a separate sequencer path
|
|
||||||
// that honors the motion table's action queue; that's deferred.
|
|
||||||
// For now the player stays in whatever cycle was active when they
|
|
||||||
// jumped (usually walk/run or Ready) — animation wise it's wrong but
|
|
||||||
// at least the character doesn't implode.
|
|
||||||
uint animCommand;
|
uint animCommand;
|
||||||
if (result.LocalAnimationCommand is { } localCmd)
|
if (!result.IsOnGround)
|
||||||
|
animCommand = AcDream.Core.Physics.MotionCommand.Falling;
|
||||||
|
else if (result.LocalAnimationCommand is { } localCmd)
|
||||||
animCommand = localCmd;
|
animCommand = localCmd;
|
||||||
else if (result.ForwardCommand is { } fwd)
|
else if (result.ForwardCommand is { } fwd)
|
||||||
animCommand = fwd;
|
animCommand = fwd;
|
||||||
|
|
|
||||||
|
|
@ -440,15 +440,39 @@ public sealed class AnimationSequencer
|
||||||
/// <param name="speedMod">Speed multiplier for the action's framerate.</param>
|
/// <param name="speedMod">Speed multiplier for the action's framerate.</param>
|
||||||
public void PlayAction(uint motionCommand, float speedMod = 1f)
|
public void PlayAction(uint motionCommand, float speedMod = 1f)
|
||||||
{
|
{
|
||||||
// Resolve motion data. Modifiers use (style << 16) | (motion & 0xFFFFFF)
|
// Resolve motion data. The lookup depends on the command's mask class:
|
||||||
// as the styled key, or just (motion & 0xFFFFFF) for style-independent
|
//
|
||||||
// entries (matches ACE MotionTable.GetObjectSequence lookup order).
|
// - Action (mask 0x10): stored in the Links dict as the transition
|
||||||
uint styleKey = CurrentStyle << 16;
|
// FROM currentSubstate TO the action motion. Matches ACE
|
||||||
int keyStyled = (int)(styleKey | (motionCommand & 0xFFFFFFu));
|
// MotionTable.GetObjectSequence @ line 189-207 (CommandMask.Action).
|
||||||
int keyPlain = (int)(motionCommand & 0xFFFFFFu);
|
// - 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;
|
||||||
|
|
||||||
if (!_mtable.Modifiers.TryGetValue(keyStyled, out var data))
|
MotionData? data = null;
|
||||||
_mtable.Modifiers.TryGetValue(keyPlain, out data);
|
if ((motionCommand & ActionMask) != 0 && CurrentMotion != 0)
|
||||||
|
{
|
||||||
|
// Action: look up the transition link from current substate → action.
|
||||||
|
data = GetLink(CurrentStyle, CurrentMotion, motionCommand);
|
||||||
|
}
|
||||||
|
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)
|
if (data is null || data.Anims.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,28 @@ public static class MotionCommand
|
||||||
/// <summary>0x40000008 — Fallen (lying on ground).</summary>
|
/// <summary>0x40000008 — Fallen (lying on ground).</summary>
|
||||||
public const uint Fallen = 0x40000008u;
|
public const uint Fallen = 0x40000008u;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 0x2500003B — Jump (Modifier action; played via
|
/// 0x40000015 — Falling (SubState). The airborne cycle. Retail's
|
||||||
/// <see cref="AnimationSequencer.PlayAction"/>). NOT a SubState — it
|
/// MotionTable has Links from RunForward/Ready/WalkForward → Falling,
|
||||||
/// overlays the current cycle via the motion table's Modifiers dict.
|
/// and a Cycles entry for (style, Falling) that loops while the body
|
||||||
|
/// is in the air. Swap via <see cref="AnimationSequencer.SetCycle"/>
|
||||||
|
/// when airborne; swap back to Ready/WalkForward/RunForward on land.
|
||||||
|
/// </summary>
|
||||||
|
public const uint Falling = 0x40000015u;
|
||||||
|
/// <summary>
|
||||||
|
/// 0x2500003B — Jump (Modifier flag). NOT an animation trigger; retail
|
||||||
|
/// uses this as a state flag internally. Kept for future use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const uint Jump = 0x2500003Bu;
|
public const uint Jump = 0x2500003Bu;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 0x10000050 — FallDown (Action; the landing animation played after
|
/// 0x1000004B — Jumpup (Action). Not present in the humanoid player
|
||||||
/// a jump. Enqueued via <see cref="AnimationSequencer.PlayAction"/>).
|
/// motion table's Links dict (empirically verified). Retail uses the
|
||||||
|
/// Falling SubState for airborne animation instead.
|
||||||
|
/// </summary>
|
||||||
|
public const uint Jumpup = 0x1000004Bu;
|
||||||
|
/// <summary>
|
||||||
|
/// 0x10000050 — FallDown (Action). Same story as Jumpup; not in the
|
||||||
|
/// humanoid motion table's Links. Landing returns to Ready via the
|
||||||
|
/// regular SetCycle transition.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const uint FallDown = 0x10000050u;
|
public const uint FallDown = 0x10000050u;
|
||||||
/// <summary>0x10000057 — Dead.</summary>
|
/// <summary>0x10000057 — Dead.</summary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue