using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Vfx; using DatReaderWriter.Types; namespace AcDream.App.Rendering.Vfx; /// /// Defers animation-hook delivery until every entity and equipped child has /// published its final pose for the frame. /// /// /// Retail stages hooks during sequence advance, then the same object update /// commits the final part frame and runs CPhysicsObj::UpdateChild /// (0x00512D50) 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. /// public sealed class AnimationHookFrameQueue { private readonly AnimationHookRouter _router; private readonly IEntityEffectPoseSource _poses; private readonly IEntityEffectPoseLifetimeSource? _poseLifetimes; private readonly List _entries = new(); public AnimationHookFrameQueue( AnimationHookRouter router, IEntityEffectPoseSource poses) { _router = router ?? throw new ArgumentNullException(nameof(router)); _poses = poses ?? throw new ArgumentNullException(nameof(poses)); _poseLifetimes = poses as IEntityEffectPoseLifetimeSource; } 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 hooks) { ArgumentNullException.ThrowIfNull(sequencer); ArgumentNullException.ThrowIfNull(hooks); // Capture incarnation identity before AnimationDone: its semantic // callback can synchronously delete this owner and reuse the local ID. // Reading the version afterwards would mislabel the old hook batch as // belonging to the replacement. ulong ownerLifetimeVersion = _poseLifetimes?.GetPoseOwnerLifetimeVersion(ownerLocalId) ?? 0UL; // Retail CPhysicsObj::process_hooks (0x00511550) executes AnimDone // inside UpdatePositionInternal, before the transition and every // UpdateObjectInternal manager. Complete the semantic motion state at // capture time so that this object's later Target/Movement/PartArray // tail sees the new queue in the same quantum. The hook remains in the // deferred entry so presentation sinks still observe the complete // authored hook stream after the final part poses are published. for (int i = 0; i < hooks.Count; i++) { // MotionDone can synchronously delete this owner and reuse the // local ID. Do not advance the displaced sequencer for later // AnimDone hooks from the old catch-up batch. if (_poseLifetimes is not null && _poseLifetimes.GetPoseOwnerLifetimeVersion( ownerLocalId) != ownerLifetimeVersion) { break; } if (hooks[i] is AnimationDoneHook) sequencer.Manager.AnimationDone(success: true); } if (hooks.Count == 0) return; _entries.Add(new Entry( ownerLocalId, ownerLifetimeVersion, hooks)); } public void Drain() { for (int i = 0; i < _entries.Count; i++) { Entry entry = _entries[i]; if (_poseLifetimes is not null && _poseLifetimes.GetPoseOwnerLifetimeVersion( entry.OwnerLocalId) != entry.OwnerLifetimeVersion) { continue; } for (int hi = 0; hi < entry.Hooks.Count; hi++) { // A prior hook sink can tear down and replace this local ID. // Revalidate per hook so the remainder of the old PES/animation // batch can never spill into the replacement incarnation. if (_poseLifetimes is not null && _poseLifetimes.GetPoseOwnerLifetimeVersion( entry.OwnerLocalId) != entry.OwnerLifetimeVersion) { break; } AnimationHook? hook = entry.Hooks[hi]; if (hook is null) continue; if (_poses.TryGetRootPose( entry.OwnerLocalId, out Matrix4x4 rootWorld)) { _router.OnHook( entry.OwnerLocalId, rootWorld.Translation, hook); } } } _entries.Clear(); } public void Clear() => _entries.Clear(); private readonly record struct Entry( uint OwnerLocalId, ulong OwnerLifetimeVersion, IReadOnlyList Hooks); }