fix(animation): retain trailing PartArray poses

Port CPartArray::UpdateParts minimum-count behavior: preserve the prior visual and rigid transforms for setup parts absent from a short authored AnimFrame, including the initial hydrated rest pose.

Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 09:41:15 +02:00
parent f004ac5562
commit a2fd61684a
6 changed files with 326 additions and 59 deletions

View file

@ -253,6 +253,7 @@ public sealed class AnimationSequencer
// consume the returned IReadOnlyList<PartTransform> synchronously within
// the same loop iteration and never cache it across Advance() calls.
private readonly PartTransform[] _partTransformScratch;
private readonly PartTransformBuffer _partTransformView;
// R1-P6: root motion flows through the Advance(dt, Frame) overload
// (retail CSequence::update(Frame*), 0x00525b80) — the old adapter
@ -279,6 +280,7 @@ public sealed class AnimationSequencer
_mtable = motionTable;
_loader = loader;
_partTransformScratch = new PartTransform[_setup.Parts.Count];
_partTransformView = new PartTransformBuffer(_partTransformScratch);
_core = new CSequence(loader);
_core.HookObj = new AdapterHookQueue(this);
_table = new CMotionTable(motionTable);
@ -714,25 +716,20 @@ public sealed class AnimationSequencer
// instead of allocating a fresh PartTransform[partCount] every call.
// Sized once in the constructor to _setup.Parts.Count, which never
// changes, so partCount here always matches the buffer length.
int authoredPartCount = Math.Min(partCount, f0Parts.Count);
var result = _partTransformScratch;
for (int i = 0; i < partCount; i++)
for (int i = 0; i < authoredPartCount; i++)
{
if (i < f0Parts.Count)
{
var p0 = f0Parts[i];
var p1 = i < f1Parts.Count ? f1Parts[i] : p0;
var p0 = f0Parts[i];
var p1 = i < f1Parts.Count ? f1Parts[i] : p0;
result[i] = new PartTransform(
Vector3.Lerp(p0.Origin, p1.Origin, t),
SlerpRetailClient(p0.Orientation, p1.Orientation, t));
}
else
{
result[i] = new PartTransform(Vector3.Zero, Quaternion.Identity);
}
result[i] = new PartTransform(
Vector3.Lerp(p0.Origin, p1.Origin, t),
SlerpRetailClient(p0.Orientation, p1.Orientation, t));
}
return result;
_partTransformView.SetCount(authoredPartCount);
return _partTransformView;
}
private IReadOnlyList<PartTransform> BuildIdentityFrame(int partCount)
@ -742,7 +739,36 @@ public sealed class AnimationSequencer
var result = _partTransformScratch;
for (int i = 0; i < partCount; i++)
result[i] = new PartTransform(Vector3.Zero, Quaternion.Identity);
return result;
_partTransformView.SetCount(partCount);
return _partTransformView;
}
/// <summary>
/// Allocation-free view whose Count preserves the authored AnimFrame part
/// count. Retail UpdateParts touches only that prefix and retains every
/// trailing CPhysicsPart pose.
/// </summary>
private sealed class PartTransformBuffer : IReadOnlyList<PartTransform>
{
private readonly PartTransform[] _items;
public PartTransformBuffer(PartTransform[] items) => _items = items;
public int Count { get; private set; }
public PartTransform this[int index] => index >= 0 && index < Count
? _items[index]
: throw new ArgumentOutOfRangeException(nameof(index));
public void SetCount(int count) => Count = count;
public IEnumerator<PartTransform> GetEnumerator()
{
for (int i = 0; i < Count; i++)
yield return _items[i];
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
GetEnumerator();
}
// R2-Q4: IsLocomotionCycleLowByte DELETED with Fix B - the cyclic-to-