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
279
docs/research/acclient_animation_map.md
Normal file
279
docs/research/acclient_animation_map.md
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
# acclient.exe Animation System Function Map
|
||||||
|
|
||||||
|
Mapped from decompiled C chunks against ACE's C# animation port.
|
||||||
|
Cross-referenced with `references/ACE/Source/ACE.Server/Physics/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CPartArray (PhysicsObj+0x10 ptr)
|
||||||
|
|
||||||
|
PartArray is heap-allocated and accessed via a pointer stored at `PhysicsObj+0x10`.
|
||||||
|
The Sequence is embedded inside PartArray at `PartArray+0x08`.
|
||||||
|
|
||||||
|
### Struct Layout
|
||||||
|
|
||||||
|
| Offset | Field | Type | Notes |
|
||||||
|
|--------|-------|------|-------|
|
||||||
|
| +0x00 | State | uint | Flags (0x10000 = HasPhysicsBSP) |
|
||||||
|
| +0x04 | Owner | ptr | PhysicsObj* |
|
||||||
|
| +0x08 | Sequence | embedded | 68 bytes of Sequence struct |
|
||||||
|
| +0x50 | MotionTableManager | ptr | null if no motion table |
|
||||||
|
| +0x54 | Setup | ptr | CSetup dat object |
|
||||||
|
| +0x58 | NumParts | uint | Part count |
|
||||||
|
| +0x5c | Parts | ptr | Array of PhysicsPart* |
|
||||||
|
| +0x60 | Scale | float[3] | XYZ scale (default 1,1,1) |
|
||||||
|
| +0x70 | ShadowParts | ptr | Shadow part array |
|
||||||
|
|
||||||
|
### Functions (chunk_00510000.c)
|
||||||
|
|
||||||
|
| Address | FUN name | ACE method | Description |
|
||||||
|
|---------|----------|-----------|-------------|
|
||||||
|
| 0x510140 | FUN_00510140 | PartArray::UpdateParts | Calls SetFrame when anim flag set |
|
||||||
|
| 0x512DE0 | FUN_00512de0 | AFrame::Combine | Quaternion compose + position transform |
|
||||||
|
| 0x513730 | FUN_00513730 | PhysicsObj::UpdatePositionInternal | Advances physics: calls PartArray::Update then SetFrame |
|
||||||
|
| 0x514B90 | FUN_00514b90 | PhysicsObj::UpdateObjectInternal | Calls PartArray::Update (SetFrame) |
|
||||||
|
| 0x518790 | FUN_00518790 | PartArray::CopyShadowParts | Copies frame data to shadow parts array (stride 0x48/part) |
|
||||||
|
| 0x5188E0 | FUN_005188e0 | PartArray::Update | Thin wrapper: calls Sequence::Update (FUN_00526780) |
|
||||||
|
| 0x519B00 | FUN_00519b00 | PartArray::SetPartFrame | Per-part transform: matrix3×3 + quaternion multiply → part position |
|
||||||
|
| 0x519C20 | FUN_00519c20 | PartArray::UpdateParts | Loops over NumParts (stride 0x1c per anim frame part), calls SetPartFrame |
|
||||||
|
| 0x519E40 | FUN_00519e40 | PartArray::SetFrame | Top-level frame apply: calls UpdateParts + CopyShadowParts |
|
||||||
|
|
||||||
|
**Key pattern in FUN_00519b00** (SetPartFrame):
|
||||||
|
Computes `pos = matrix3x3 * scale + parentPos`, then calls `FUN_00535dc0` (quaternion normalize) to compose rotation. The matrix at `animFrame[partIdx]` has 16 floats:
|
||||||
|
`[0..3]`=quaternion, `[4..0xc]`=rotation matrix (computed from quat), `[0xd..0xf]`=XYZ position.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSequence (embedded at PartArray+0x08)
|
||||||
|
|
||||||
|
### Struct Layout
|
||||||
|
|
||||||
|
| Offset | Field | Type | Notes |
|
||||||
|
|--------|-------|------|-------|
|
||||||
|
| +0x00 | vtable | ptr | Points to &PTR_FUN_007c92a0 |
|
||||||
|
| +0x04 | AnimList.Head | ptr | First node in linked list |
|
||||||
|
| +0x08 | AnimList.Tail | ptr | Last node sentinel |
|
||||||
|
| +0x0c | FirstCyclic | ptr | Node before first cyclic anim |
|
||||||
|
| +0x10 | Velocity.X | float | |
|
||||||
|
| +0x14 | Velocity.Y | float | |
|
||||||
|
| +0x18 | Velocity.Z | float | |
|
||||||
|
| +0x1c | Omega.X | float | Angular velocity |
|
||||||
|
| +0x20 | Omega.Y | float | |
|
||||||
|
| +0x24 | Omega.Z | float | |
|
||||||
|
| +0x28 | HookObj | ptr | PhysicsObj for animation hooks |
|
||||||
|
| +0x30 | FrameNumber | double | Current frame (fractional) |
|
||||||
|
| +0x38 | CurrAnim | ptr | Current AnimSequenceNode* |
|
||||||
|
| +0x3c | PlacementFrame | ptr | AnimationFrame* (used when no anims) |
|
||||||
|
| +0x40 | PlacementFrameID | int | Placement ID (e.g. 0x65) |
|
||||||
|
|
||||||
|
Total size: ~0x44 = 68 bytes.
|
||||||
|
|
||||||
|
### Functions (chunk_00520000.c)
|
||||||
|
|
||||||
|
| Address | FUN name | ACE method | Description |
|
||||||
|
|---------|----------|-----------|-------------|
|
||||||
|
| 0x5254C0 | FUN_005254c0 | Sequence::CombinePhysics | Adds velocity+omega to sequence totals |
|
||||||
|
| 0x525500 | FUN_00525500 | Sequence::subtract_physics | Subtracts velocity+omega |
|
||||||
|
| 0x525540 | FUN_00525540 | Sequence::remove_all_link_animations | Removes all pre-cyclic nodes from list |
|
||||||
|
| 0x525570 | FUN_00525570 | Sequence::GetCurrAnimFrame | If CurrAnim≠null: floor(FrameNumber)→get_part_frame; else PlacementFrame |
|
||||||
|
| 0x5255B0 | FUN_005255b0 | Sequence::SetPlacementFrame | Sets PlacementFrame ptr (+0x3c) and ID (+0x40) |
|
||||||
|
| 0x5255D0 | FUN_005255d0 | Sequence::get_curr_frame_number | floor(FrameNumber) |
|
||||||
|
| 0x5255F0 | FUN_005255f0 | Sequence::Init | Zero-initializes all fields, sets vtable |
|
||||||
|
| 0x525630 | FUN_00525630 | Sequence::clear_animations | Walks linked list, releases all nodes |
|
||||||
|
| 0x5256B0 | FUN_005256b0 | Sequence::apply_physics | frame.Origin += Velocity\*quantum; frame.Rotate(Omega\*quantum) |
|
||||||
|
| 0x525740 | FUN_00525740 | Sequence::apricot | Removes consumed nodes up to CurrAnim |
|
||||||
|
| 0x525EB0 | FUN_00525eb0 | Sequence::advance_to_next_animation | Moves CurrAnim to next/prev node or wraps to FirstCyclic |
|
||||||
|
| 0x526110 | FUN_00526110 | Sequence::append_animation | Allocates AnimSequenceNode (0x1c bytes), links at tail, sets FirstCyclic and CurrAnim if first |
|
||||||
|
| 0x5261B0 | FUN_005261b0 | Sequence::Clear | Calls clear_animations + clear_physics, zeroes PlacementFrame |
|
||||||
|
| 0x5261D0 | FUN_005261d0 | Sequence::update_internal | **Core update loop**: `FrameNumber += Framerate * elapsed`; handles forward/backward; fires hooks; calls advance_to_next_animation when done |
|
||||||
|
| 0x526780 | FUN_00526780 | Sequence::Update | Wrapper: if AnimList not empty → update_internal + apricot; else apply_physics only |
|
||||||
|
|
||||||
|
**Key pattern in FUN_005261d0** (update_internal):
|
||||||
|
```c
|
||||||
|
// Forward direction (frametime > 0):
|
||||||
|
while (floor(frameNum) > lastFrame) {
|
||||||
|
apply_physics + execute_hooks(lastFrame);
|
||||||
|
lastFrame++;
|
||||||
|
}
|
||||||
|
if (floor(frameNum) > get_high_frame()) {
|
||||||
|
frameTimeElapsed = (frameNum - get_high_frame() - 1.0) / framerate;
|
||||||
|
frameNum = get_high_frame();
|
||||||
|
advance_to_next_animation(timeElapsed, ...);
|
||||||
|
recurse with remaining time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CAnimSequenceNode (linked list node, 0x1c bytes)
|
||||||
|
|
||||||
|
### Struct Layout
|
||||||
|
|
||||||
|
| Offset | Field | Type | Notes |
|
||||||
|
|--------|-------|------|-------|
|
||||||
|
| +0x00 | prev | ptr | Previous linked list node |
|
||||||
|
| +0x04 | next | ptr | Next linked list node |
|
||||||
|
| +0x08 | (sentinel) | ptr | |
|
||||||
|
| +0x0c | Anim | ptr | Animation* dat object |
|
||||||
|
| +0x10 | Framerate | float | Frames per second (default 30.0) |
|
||||||
|
| +0x14 | LowFrame | int | Start frame |
|
||||||
|
| +0x18 | HighFrame | int | End frame (-1 = use all) |
|
||||||
|
|
||||||
|
Total size: 0x1c = 28 bytes (heap allocated via `FUN_005df0f5(0x1c)`).
|
||||||
|
|
||||||
|
### Functions (chunk_00520000.c)
|
||||||
|
|
||||||
|
| Address | FUN name | ACE method | Description |
|
||||||
|
|---------|----------|-----------|-------------|
|
||||||
|
| 0x526810 | FUN_00526810 | AnimSequenceNode::get_pos_frame | Returns PosFrames[frameIdx] at ptr+frame\*0x1c |
|
||||||
|
| 0x526840 | FUN_00526840 | AnimSequenceNode::get_part_frame | Returns PartFrames[frameIdx] at ptr+frame\*0x10 |
|
||||||
|
| 0x526870 | FUN_00526870 | AnimSequenceNode::has_anim | Returns Anim != null |
|
||||||
|
| 0x526880 | FUN_00526880 | AnimSequenceNode::get_starting_frame | LowFrame if Framerate≥0, else HighFrame+1-ε |
|
||||||
|
| 0x5268B0 | FUN_005268b0 | AnimSequenceNode::get_ending_frame | HighFrame+1-ε if Framerate≥0, else LowFrame |
|
||||||
|
| 0x5267E0 | FUN_005267e0 | AnimSequenceNode::multiply_framerate | Swaps Low/High if multiplier<0; Framerate \*= multiplier |
|
||||||
|
|
||||||
|
**Animation dat object layout** (referenced from AnimSequenceNode+0x0c):
|
||||||
|
- `Anim+0x38`: PosFrames array ptr (AFrame per frame, stride 0x1c)
|
||||||
|
- `Anim+0x3c`: PartFrames array ptr (AnimationFrame per frame, stride 0x10 per part)
|
||||||
|
- `Anim+0x48`: NumFrames (int)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MotionTable / MotionTableManager (chunk_00520000.c)
|
||||||
|
|
||||||
|
### Cycle Key Construction
|
||||||
|
|
||||||
|
The cycle key is built as: `(motionCategory << 16) | (command & 0xFFFFFF)`
|
||||||
|
|
||||||
|
Confirmed at lines 2469, 2474, 2491, 2551, 2597, 2660, 2699 in chunk_00520000.c:
|
||||||
|
```c
|
||||||
|
key = param_4 & 0xffffff | param_1 << 0x10;
|
||||||
|
// or equivalently:
|
||||||
|
key = (int)*param_3 << 0x10 | (uint)param_2 & 0xffffff;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hash Map Structure (used for all MotionTable lookups)
|
||||||
|
|
||||||
|
The hash map has:
|
||||||
|
- `+0x04`: hash mask (capacity - 1)
|
||||||
|
- `+0x08`: hash shift
|
||||||
|
- `+0x0c`: bucket array ptr
|
||||||
|
- Each bucket is a linked list of nodes; each node has key at `+0x08`, value at `+0x0c`, next at `+0x04`
|
||||||
|
|
||||||
|
Hash formula: `bucketIdx = (key >> hashShift ^ key) & hashMask`
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
| Address | FUN name | ACE method | Description |
|
||||||
|
|---------|----------|-----------|-------------|
|
||||||
|
| 0x520A00 | FUN_00520a00 | MotionTableManager::Init | Initializes all MTM fields (large constructor) |
|
||||||
|
| 0x520BB0 | FUN_00520bb0 | MotionTableManager::Create | Factory: allocates 0xd8 bytes, calls Init |
|
||||||
|
| 0x521770 | FUN_00521770 | MotionTable::unpack | Deserializes MotionTable from dat stream |
|
||||||
|
| 0x521F10 | FUN_00521f10 | MotionTable::find_motion | Hash map lookup by motionID |
|
||||||
|
| 0x521F60 | FUN_00521f60 | MotionTable::destructor | Frees all MotionTable allocations |
|
||||||
|
| 0x522E30 | FUN_00522e30 | MotionTableManager::SetCurrentMotion | Sets current state node |
|
||||||
|
| 0x523000 | FUN_00523000 | MotionTable::add_animations_to_sequence | Applies velocity/omega + appends AnimData to Sequence |
|
||||||
|
| 0x5230D0 | FUN_005230d0 | MotionTable::set_physics | Sets velocity+omega on Sequence only |
|
||||||
|
| 0x523150 | FUN_00523150 | MotionTable::subtract_physics | Subtracts velocity+omega from Sequence |
|
||||||
|
| 0x5231D0 | FUN_005231d0 | MotionTable::FindObjectAndMotionData | Hash lookup returning node ptr-4 |
|
||||||
|
| 0x523210 | FUN_00523210 | MotionTable::FindObjectAndCycle | Hash lookup returning cycle data at node+0xc |
|
||||||
|
| 0x523260 | FUN_00523260 | MotionTable::CheckAlreadySet | Checks if motion already current at same speed |
|
||||||
|
| 0x5232B0 | FUN_005232b0 | MotionTable::GetObjectAnimData | Builds cycle key (`src<<16 \| dst&0xFFFFFF`), calls FindObjectAndMotionData |
|
||||||
|
| 0x523400 | FUN_00523400 | MotionTableManager::PerformMovement | **Core motion dispatch**: transitions between states, populates Sequence |
|
||||||
|
|
||||||
|
**FUN_00523400 (PerformMovement) key pattern**:
|
||||||
|
```c
|
||||||
|
// Transition key for cycle lookup:
|
||||||
|
key = (int)*param_3 << 0x10 | (uint)local_8 & 0xffffff;
|
||||||
|
anim_data = FUN_005231d0(key); // FindObjectAndMotionData
|
||||||
|
|
||||||
|
// Then:
|
||||||
|
FUN_00523000(sequence, link_anim, speed); // add link animations
|
||||||
|
FUN_00523000(sequence, from_anim, speed); // add departure animation
|
||||||
|
FUN_00523000(sequence, to_anim, speed); // add transition animation
|
||||||
|
FUN_00523000(sequence, cycle_anim, speed); // add cyclic animation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AFrame (chunk_00530000.c)
|
||||||
|
|
||||||
|
AFrame is a combined rotation+position: quaternion (4 floats) + 3×3 matrix (derived) + XYZ (3 floats).
|
||||||
|
|
||||||
|
### Struct Layout (0x1c = 28 bytes total)
|
||||||
|
|
||||||
|
| Offset | Field | Type |
|
||||||
|
|--------|-------|------|
|
||||||
|
| +0x00..+0x0c | Quaternion W,X,Y,Z | float[4] |
|
||||||
|
| +0x10..+0x30 | Matrix 3×3 (derived) | float[9] |
|
||||||
|
| +0x34..+0x3c | Origin XYZ | float[3] |
|
||||||
|
|
||||||
|
Note: In Ghidra decompiled code the struct appears as offsets in uint32s:
|
||||||
|
`[0..3]` = quaternion, `[4..0xc]` = matrix, `[0xd..0xf]` = origin.
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
| Address | FUN name | ACE method | Description |
|
||||||
|
|---------|----------|-----------|-------------|
|
||||||
|
| 0x535B30 | FUN_00535b30 | AFrame::update_matrix | Quaternion → 3×3 rotation matrix (stores in [4..0xc]) |
|
||||||
|
| 0x535C10 | FUN_00535c10 | AFrame::is_valid | Checks NaN in origin [0xd..0xf] and quaternion [0..3] |
|
||||||
|
| 0x535DC0 | FUN_00535dc0 | AFrame::normalize_quaternion | Normalizes quat: each *= 1/sqrt(dot); validates with is_valid |
|
||||||
|
| 0x535E70 | FUN_00535e70 | AFrame::pack_quaternion | Serializes quaternion [0..3] + origin [0xd..0xf] to byte stream |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PhysicsObj Animation-Relevant Offsets (chunk_00510000.c)
|
||||||
|
|
||||||
|
| Offset | Field | Notes |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| +0x10 | PartArray ptr | null = no animation; checked before every anim call |
|
||||||
|
| +0x50 | Position.Frame | AFrame (embedded); updated by FUN_00512de0 (AFrame::Combine) |
|
||||||
|
| +0xA8 | State | bit 0x4000 = no-animate flag; bit 0x1000 = invisible |
|
||||||
|
| +0xD8 | LastUpdateTime | double; used to compute dt for Sequence::Update |
|
||||||
|
| +0x114 | RunRate | float; velocity scale applied to omega after anim update |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Call Chain: Per-Frame Animation Update
|
||||||
|
|
||||||
|
```
|
||||||
|
PhysicsEngine::update (FUN_00452A10)
|
||||||
|
→ PhysicsObj::update_object (FUN_00515020)
|
||||||
|
→ PhysicsObj::UpdatePositionInternal (FUN_00513730)
|
||||||
|
→ PartArray::Update (FUN_005188E0) [if PhysicsObj+0x10 != null]
|
||||||
|
→ Sequence::Update (FUN_00526780)
|
||||||
|
→ Sequence::update_internal (FUN_005261D0)
|
||||||
|
→ AnimSequenceNode::get_part_frame (FUN_00526840)
|
||||||
|
→ Sequence::execute_hooks (via FUN_00525430)
|
||||||
|
→ Sequence::advance_to_next_animation (FUN_00525EB0)
|
||||||
|
→ AnimSequenceNode::get_starting_frame (FUN_00526880)
|
||||||
|
→ Sequence::apply_physics (FUN_005256B0)
|
||||||
|
→ Sequence::apricot (FUN_00525740)
|
||||||
|
→ AFrame::Combine (FUN_00512DE0) [combines offsetFrame into PhysicsObj.Position]
|
||||||
|
→ PartArray::SetFrame (FUN_00519E40) [if PhysicsObj+0x10 != null]
|
||||||
|
→ PartArray::UpdateParts (FUN_00519C20)
|
||||||
|
→ Sequence::GetCurrAnimFrame (FUN_00525570)
|
||||||
|
→ PartArray::SetPartFrame (FUN_00519B00) [per-part matrix*scale+quaternion]
|
||||||
|
→ PartArray::CopyShadowParts (FUN_00518790) [if shadow parts exist]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Constants and Identifiers
|
||||||
|
|
||||||
|
| Constant | Value | Meaning |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| AnimSequenceNode size | 0x1c (28) | heap alloc size |
|
||||||
|
| AnimationFrame stride (PartFrames) | 0x10 per part | 4 floats: quaternion per part |
|
||||||
|
| AFrame size | 0x1c (28) | quaternion+matrix+pos |
|
||||||
|
| AnimData stride in MotionTable nodes | 0x1c | 7 uint32s per entry |
|
||||||
|
| PosFrames stride | 0x1c per frame | AFrame per position keyframe |
|
||||||
|
| PhysicsObj.PartArray offset | +0x10 | pointer field |
|
||||||
|
| PhysicsObj.Position.Frame offset | +0x50 | AFrame embedded |
|
||||||
|
| PartArray.NumParts offset | +0x58 (relative to PartArray) | uint |
|
||||||
|
| PartArray.Scale offset | +0x60 (relative to PartArray) | float[3] |
|
||||||
|
| Cycle key formula | `(category << 16) \| (cmd & 0xFFFFFF)` | MotionTable lookup |
|
||||||
|
| Default framerate | 30.0 (0x41F00000) | AnimSequenceNode default |
|
||||||
|
| PhysicsGlobals.EPSILON | from _DAT_007c92b4 | ~1e-5 |
|
||||||
|
| No-animate state flag | 0x6500000d | Walk/Run forward (left) |
|
||||||
|
| No-animate state flag | 0x6500000f | Walk/Run backward |
|
||||||
|
|
@ -2054,47 +2054,35 @@ public sealed class GameWindow : IDisposable
|
||||||
stanceOverride: NonCombatStance,
|
stanceOverride: NonCombatStance,
|
||||||
commandOverride: cmdOverride);
|
commandOverride: cmdOverride);
|
||||||
|
|
||||||
// AC reuses right-side animations for left-side motions (played in
|
// The sequencer handles left→right remapping internally via
|
||||||
// reverse). If the left-side command has no cycle, fall back to the
|
// adjust_motion (TurnLeft→TurnRight with negative speed, etc.).
|
||||||
// right-side equivalent so the player isn't stuck in idle.
|
// Pass the ORIGINAL animCommand — SetCycle does the remapping.
|
||||||
uint resolvedCommand = animCommand; // track which command actually resolved
|
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)
|
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame)
|
||||||
{
|
{
|
||||||
ushort fallback = cmdOverride switch
|
ushort fallback = cmdOverride switch
|
||||||
{
|
{
|
||||||
0x000E => 0x000D, // TurnLeft → TurnRight
|
0x000E => 0x000D,
|
||||||
0x0010 => 0x000F, // SideStepLeft → SideStepRight
|
0x0010 => 0x000F,
|
||||||
0x0006 => 0x0005, // WalkBackward → WalkForward
|
0x0006 => 0x0005,
|
||||||
_ => (ushort)0,
|
_ => (ushort)0,
|
||||||
};
|
};
|
||||||
if (fallback != 0)
|
if (fallback != 0)
|
||||||
{
|
|
||||||
cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
|
cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
|
||||||
ae.Setup, _dats,
|
ae.Setup, _dats,
|
||||||
motionTableIdOverride: _playerMotionTableId,
|
motionTableIdOverride: _playerMotionTableId,
|
||||||
stanceOverride: NonCombatStance,
|
stanceOverride: NonCombatStance,
|
||||||
commandOverride: fallback);
|
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)
|
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
ae.Animation = cycle.Animation;
|
ae.Animation = cycle.Animation;
|
||||||
ae.LowFrame = Math.Max(0, cycle.LowFrame);
|
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>
|
/// <param name="speedMod">Speed multiplier applied to framerates (1.0 = normal).</param>
|
||||||
public void SetCycle(uint style, uint motion, float speedMod = 1f)
|
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.
|
// Fast-path: already playing this exact motion at the same speed.
|
||||||
if (CurrentStyle == style && CurrentMotion == motion
|
if (CurrentStyle == style && CurrentMotion == motion
|
||||||
&& _firstCyclic != null && _queue.Count > 0)
|
&& _firstCyclic != null && _queue.Count > 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Resolve transition link (currentSubstate → newMotion).
|
// Resolve transition link (currentSubstate → adjustedMotion).
|
||||||
MotionData? linkData = CurrentMotion != 0
|
MotionData? linkData = CurrentMotion != 0
|
||||||
? GetLink(style, CurrentMotion, motion)
|
? GetLink(style, CurrentMotion, adjustedMotion)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Resolve target cycle.
|
// Resolve target cycle using the ADJUSTED motion (TurnRight, not TurnLeft).
|
||||||
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (motion & 0xFFFFFFu));
|
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu));
|
||||||
_mtable.Cycles.TryGetValue(cycleKey, out var cycleData);
|
_mtable.Cycles.TryGetValue(cycleKey, out var cycleData);
|
||||||
|
|
||||||
// Clear the old cyclic tail; keep any non-cyclic head that hasn't
|
// Clear the old cyclic tail; keep any non-cyclic head that hasn't
|
||||||
// been played yet (ACE behaviour: non-cyclic anims drain naturally).
|
// been played yet (ACE behaviour: non-cyclic anims drain naturally).
|
||||||
ClearCyclicTail();
|
ClearCyclicTail();
|
||||||
|
|
||||||
// Enqueue link frames.
|
// Enqueue link frames (with adjusted speed for left→right remapping).
|
||||||
if (linkData is { Anims.Count: > 0 })
|
if (linkData is { Anims.Count: > 0 })
|
||||||
EnqueueMotionData(linkData, speedMod, isLooping: false);
|
EnqueueMotionData(linkData, adjustedSpeed, isLooping: false);
|
||||||
|
|
||||||
// Enqueue new cycle.
|
// Enqueue new cycle.
|
||||||
if (cycleData is { Anims.Count: > 0 })
|
if (cycleData is { Anims.Count: > 0 })
|
||||||
{
|
{
|
||||||
EnqueueMotionData(cycleData, speedMod, isLooping: true);
|
EnqueueMotionData(cycleData, adjustedSpeed, isLooping: true);
|
||||||
}
|
}
|
||||||
else if (_queue.Count == 0)
|
else if (_queue.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -366,9 +389,23 @@ public sealed class AnimationSequencer
|
||||||
if (low >= numFrames) low = numFrames - 1;
|
if (low >= numFrames) low = numFrames - 1;
|
||||||
if (high >= numFrames) high = numFrames - 1;
|
if (high >= numFrames) high = numFrames - 1;
|
||||||
if (low < 0) low = 0;
|
if (low < 0) low = 0;
|
||||||
if (low > high) high = low;
|
|
||||||
|
|
||||||
float fr = ad.Framerate * speedMod;
|
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);
|
return new AnimNode(anim, fr, low, high, isLooping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue