feat(vfx): bind effects to live animated poses

This commit is contained in:
Erik 2026-07-14 10:56:01 +02:00
parent 96ddfdf175
commit 542dcfc384
41 changed files with 3246 additions and 741 deletions

View file

@ -0,0 +1,89 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Vfx;
using DatReaderWriter.Types;
namespace AcDream.App.Rendering.Vfx;
/// <summary>
/// Defers animation-hook delivery until every entity and equipped child has
/// published its final pose for the frame.
/// </summary>
/// <remarks>
/// Retail stages hooks during sequence advance, then the same object update
/// commits the final part frame and runs <c>CPhysicsObj::UpdateChild</c>
/// (<c>0x00512D50</c>) before particles advance or anything draws. Capturing
/// here preserves that observable order and avoids firing a hand or weapon
/// effect against the previous animation frame.
/// </remarks>
public sealed class AnimationHookFrameQueue
{
private readonly AnimationHookRouter _router;
private readonly IEntityEffectPoseSource _poses;
private readonly List<Entry> _entries = new();
public AnimationHookFrameQueue(
AnimationHookRouter router,
IEntityEffectPoseSource poses)
{
_router = router ?? throw new ArgumentNullException(nameof(router));
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
}
public int Count => _entries.Count;
public void Capture(uint ownerLocalId, AnimationSequencer sequencer)
{
ArgumentNullException.ThrowIfNull(sequencer);
Capture(ownerLocalId, sequencer, sequencer.ConsumePendingHooks());
}
internal void Capture(
uint ownerLocalId,
AnimationSequencer sequencer,
IReadOnlyList<AnimationHook> hooks)
{
ArgumentNullException.ThrowIfNull(sequencer);
ArgumentNullException.ThrowIfNull(hooks);
_entries.Add(new Entry(
ownerLocalId,
sequencer,
hooks));
}
public void Drain()
{
for (int i = 0; i < _entries.Count; i++)
{
Entry entry = _entries[i];
bool hasRootPose = _poses.TryGetRootPose(
entry.OwnerLocalId,
out Matrix4x4 rootWorld);
Vector3 worldPosition = hasRootPose
? rootWorld.Translation
: Vector3.Zero;
for (int hi = 0; hi < entry.Hooks.Count; hi++)
{
AnimationHook? hook = entry.Hooks[hi];
if (hook is null)
continue;
if (hasRootPose)
_router.OnHook(entry.OwnerLocalId, worldPosition, hook);
if (hook is AnimationDoneHook)
entry.Sequencer.Manager.AnimationDone(success: true);
}
// Retail sweeps zero-tick motion entries even on frames without a
// hook. It remains paired with every sequencer advanced this frame.
entry.Sequencer.Manager.UseTime();
}
_entries.Clear();
}
public void Clear() => _entries.Clear();
private readonly record struct Entry(
uint OwnerLocalId,
AnimationSequencer Sequencer,
IReadOnlyList<AnimationHook> Hooks);
}