using System.Numerics; using AcDream.App.Rendering.Vfx; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.App.Tests.Rendering.Vfx; public sealed class EntityEffectPoseRegistryTests { [Fact] public void Publish_PreservesIndexedParts_AndUpdateRootDoesNotReplaceThem() { var poses = new EntityEffectPoseRegistry(); WorldEntity entity = Entity(7u, new Vector3(1, 2, 3)); poses.Publish(entity, new[] { Matrix4x4.Identity, Matrix4x4.CreateTranslation(0, 0, 4), }); entity.SetPosition(new Vector3(10, 20, 30)); Assert.True(poses.UpdateRoot(entity)); Assert.True(poses.TryGetRootPose(7u, out Matrix4x4 root)); Assert.Equal(new Vector3(10, 20, 30), root.Translation); Assert.True(poses.TryGetPartPose(7u, 1, out Matrix4x4 part)); Assert.Equal(new Vector3(0, 0, 4), part.Translation); Assert.False(poses.TryGetPartPose(7u, -1, out _)); } [Fact] public void PublishMeshRefs_ReplacesPartSnapshotImmediately() { var poses = new EntityEffectPoseRegistry(); WorldEntity entity = Entity(8u, Vector3.Zero); poses.Publish(entity, new[] { Matrix4x4.Identity }); entity.MeshRefs = new[] { new MeshRef(0x01000001u, Matrix4x4.CreateTranslation(1, 2, 3)), new MeshRef(0x01000002u, Matrix4x4.CreateTranslation(4, 5, 6)), }; poses.PublishMeshRefs(entity); Assert.True(poses.TryGetPartPoses(8u, out var parts)); Assert.Equal(2, parts.Count); Assert.Equal(new Vector3(4, 5, 6), parts[1].Translation); } [Fact] public void Publish_MissingMiddlePartRetainsIndicesButCannotBeResolved() { var poses = new EntityEffectPoseRegistry(); WorldEntity entity = Entity(81u, Vector3.Zero); poses.Publish( entity, [ Matrix4x4.CreateTranslation(1, 0, 0), Matrix4x4.CreateTranslation(2, 0, 0), Matrix4x4.CreateTranslation(3, 0, 0), ], [true, false, true]); Assert.False(poses.TryGetPartPose(81u, 1, out _)); Assert.True(poses.TryGetPartPose(81u, 2, out Matrix4x4 third)); Assert.Equal(3f, third.Translation.X); } [Fact] public void PublishMeshRefs_PrefersStableIndexedEntityPosesOverCompactedDrawables() { var poses = new EntityEffectPoseRegistry(); WorldEntity entity = Entity(82u, Vector3.Zero); entity.MeshRefs = [ new MeshRef(1u, Matrix4x4.CreateTranslation(1, 0, 0)), new MeshRef(3u, Matrix4x4.CreateTranslation(3, 0, 0)), ]; entity.SetIndexedPartPoses( [ Matrix4x4.CreateTranslation(1, 0, 0), Matrix4x4.CreateTranslation(2, 0, 0), Matrix4x4.CreateTranslation(3, 0, 0), ], [true, false, true]); poses.PublishMeshRefs(entity); Assert.False(poses.TryGetPartPose(82u, 1, out _)); Assert.True(poses.TryGetPartPose(82u, 2, out Matrix4x4 third)); Assert.Equal(3f, third.Translation.X); } [Fact] public void AnimationHookQueue_DrainsAgainstPosePublishedAfterCapture() { var poses = new EntityEffectPoseRegistry(); WorldEntity entity = Entity(9u, Vector3.Zero); poses.Publish(entity, Array.Empty()); var router = new AnimationHookRouter(); var sink = new RecordingSink(); router.Register(sink); var queue = new AnimationHookFrameQueue(router, poses); var sequencer = new AnimationSequencer( new Setup(), new MotionTable(), new NullAnimationLoader()); queue.Capture(9u, sequencer, new AnimationHook[] { new SoundHook() }); entity.SetPosition(new Vector3(4, 5, 6)); poses.UpdateRoot(entity); queue.Drain(); Assert.Single(sink.Calls); Assert.Equal(new Vector3(4, 5, 6), sink.Calls[0].Position); Assert.Equal(0, queue.Count); } [Fact] public void AnimationHookQueue_CompletesMotionStateEvenWhenPoseWasRemoved() { var poses = new EntityEffectPoseRegistry(); var queue = new AnimationHookFrameQueue(new AnimationHookRouter(), poses); var sequencer = new AnimationSequencer( new Setup(), new MotionTable(), new NullAnimationLoader()); sequencer.Manager.AddToQueue(0x10000001u, ticks: 1u); queue.Capture( 11u, sequencer, new AnimationHook[] { new AnimationDoneHook() }); queue.Drain(); Assert.Empty(sequencer.Manager.PendingAnimations); Assert.Equal(0, queue.Count); } private static WorldEntity Entity(uint id, Vector3 position) => new() { Id = id, ServerGuid = id, SourceGfxObjOrSetupId = 0x02000001u, Position = position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = 0x01010001u, }; private sealed class RecordingSink : IAnimationHookSink { public List<(uint Id, Vector3 Position)> Calls { get; } = new(); public void OnHook(uint entityId, Vector3 entityWorldPosition, AnimationHook hook) => Calls.Add((entityId, entityWorldPosition)); } private sealed class NullAnimationLoader : IAnimationLoader { public Animation? LoadAnimation(uint animationId) => null; } }