fix(core): resolve AnimData HighFrame=-1 sentinel before filtering cycles
Diagnostic spawn dump revealed the player character arrives with `low=0 high=-1 framerate=30.00 partFrames=33`. The -1 is ACViewer's "play the whole animation" sentinel (see references/ACViewer/.../Physics/Animation/AnimSequenceNode.cs:96-113 set_animation_id). My Phase 6.1 code used the raw int values, so the downstream registration filter evaluated `HighFrame(-1) > LowFrame(0)` as false and threw away every animated entity whose AnimData used the sentinel — which, from the live dump, appears to be basically all of them. MotionResolver.GetIdleCycle now does the same four-step clamp ACViewer does: -1 HighFrame → NumFrames-1, clamp LowFrame and HighFrame to NumFrames-1, and collapse to LowFrame if LowFrame > HighFrame. The IdleCycle carried up to GameWindow is always in terms of real frame indices the playback loop can step through. Static poses (framerate==0 or single frame after resolution) still skip registration correctly. 168 tests green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5253c7bfe8
commit
49c1a9d29e
2 changed files with 50 additions and 1 deletions
|
|
@ -946,6 +946,23 @@ public sealed class GameWindow : IDisposable
|
||||||
else if (idleCycle.Animation.PartFrames.Count <= 1)
|
else if (idleCycle.Animation.PartFrames.Count <= 1)
|
||||||
_liveAnimRejectPartFrames++;
|
_liveAnimRejectPartFrames++;
|
||||||
|
|
||||||
|
// Per-entity dump for the first N spawns so we can see actual
|
||||||
|
// AnimData values for live creatures and compare against retail.
|
||||||
|
if (_liveSpawnReceived <= 30)
|
||||||
|
{
|
||||||
|
if (idleCycle is not null)
|
||||||
|
Console.WriteLine($"live: SPAWN[{_liveSpawnReceived}] name='{spawn.Name ?? "?"}' " +
|
||||||
|
$"setup=0x{spawn.SetupTableId:X8} mt=0x{(spawn.MotionTableId ?? 0):X8} " +
|
||||||
|
$"cycle: anim=0x{(uint)idleCycle.Animation.Id:X8} " +
|
||||||
|
$"low={idleCycle.LowFrame} high={idleCycle.HighFrame} " +
|
||||||
|
$"framerate={idleCycle.Framerate:F2} partFrames={idleCycle.Animation.PartFrames.Count} " +
|
||||||
|
$"numParts={idleCycle.Animation.NumParts}");
|
||||||
|
else
|
||||||
|
Console.WriteLine($"live: SPAWN[{_liveSpawnReceived}] name='{spawn.Name ?? "?"}' " +
|
||||||
|
$"setup=0x{spawn.SetupTableId:X8} mt=0x{(spawn.MotionTableId ?? 0):X8} " +
|
||||||
|
$"cycle=null (no motion table / resolver failed)");
|
||||||
|
}
|
||||||
|
|
||||||
if (idleCycle is not null && idleCycle.Framerate != 0f
|
if (idleCycle is not null && idleCycle.Framerate != 0f
|
||||||
&& idleCycle.HighFrame > idleCycle.LowFrame
|
&& idleCycle.HighFrame > idleCycle.LowFrame
|
||||||
&& idleCycle.Animation.PartFrames.Count > 1)
|
&& idleCycle.Animation.PartFrames.Count > 1)
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,39 @@ public static class MotionResolver
|
||||||
var resolved = ResolveIdleCycleInternal(setup, dats, motionTableIdOverride, stanceOverride, commandOverride);
|
var resolved = ResolveIdleCycleInternal(setup, dats, motionTableIdOverride, stanceOverride, commandOverride);
|
||||||
if (resolved is null) return null;
|
if (resolved is null) return null;
|
||||||
var (anim, ad) = resolved.Value;
|
var (anim, ad) = resolved.Value;
|
||||||
return new IdleCycle(anim, ad.LowFrame, ad.HighFrame, ad.Framerate);
|
|
||||||
|
// Sentinel resolution — matches ACViewer's AnimSequenceNode.set_animation_id
|
||||||
|
// (references/ACViewer/.../Physics/Animation/AnimSequenceNode.cs:96-113).
|
||||||
|
//
|
||||||
|
// AnimData in the dats encodes a few sentinel values that the raw int
|
||||||
|
// fields can't represent directly:
|
||||||
|
// * HighFrame == -1 → "play the whole animation" (use NumFrames - 1)
|
||||||
|
// * HighFrame > NumFrames - 1 → clamp (defensive for malformed data)
|
||||||
|
// * LowFrame >= NumFrames → clamp to NumFrames - 1
|
||||||
|
// * LowFrame > HighFrame after clamping → collapse to a single frame
|
||||||
|
//
|
||||||
|
// Our Phase 6.1 code naively used the raw LowFrame/HighFrame and then
|
||||||
|
// rejected the cycle if `HighFrame <= LowFrame`, which silently threw
|
||||||
|
// away every animated creature whose AnimData used the -1 sentinel —
|
||||||
|
// i.e. almost all of them. Resolving here makes the IdleCycle carry
|
||||||
|
// the real frame range, so downstream playback/filtering sees a span
|
||||||
|
// it can advance through.
|
||||||
|
int numFrames = anim.PartFrames.Count;
|
||||||
|
int lowFrame = ad.LowFrame;
|
||||||
|
int highFrame = ad.HighFrame;
|
||||||
|
|
||||||
|
if (highFrame < 0)
|
||||||
|
highFrame = numFrames - 1;
|
||||||
|
if (lowFrame >= numFrames)
|
||||||
|
lowFrame = numFrames - 1;
|
||||||
|
if (highFrame >= numFrames)
|
||||||
|
highFrame = numFrames - 1;
|
||||||
|
if (lowFrame < 0)
|
||||||
|
lowFrame = 0;
|
||||||
|
if (lowFrame > highFrame)
|
||||||
|
highFrame = lowFrame;
|
||||||
|
|
||||||
|
return new IdleCycle(anim, lowFrame, highFrame, ad.Framerate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AnimationFrame? GetIdleFrame(
|
public static AnimationFrame? GetIdleFrame(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue