Process animation completion at the retail process_hooks boundary, then run targeting, movement, PartArray completion, and PositionManager in the named UpdateObjectInternal order for local, remote, hidden, and position-less animated objects. Retire TS-42 with deterministic conformance coverage.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
224 lines
7.5 KiB
C#
224 lines
7.5 KiB
C#
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<Matrix4x4>());
|
|
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_CompletesMotionStateAtCaptureEvenWhenPoseWasRemoved()
|
|
{
|
|
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() });
|
|
|
|
Assert.Empty(sequencer.Manager.PendingAnimations);
|
|
queue.Drain();
|
|
|
|
Assert.Empty(sequencer.Manager.PendingAnimations);
|
|
Assert.Equal(0, queue.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnimationHookQueue_DeferredDrainDoesNotOwnPartArrayUseTime()
|
|
{
|
|
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: 0u);
|
|
|
|
queue.Capture(12u, sequencer, Array.Empty<AnimationHook>());
|
|
queue.Drain();
|
|
|
|
Assert.Single(sequencer.Manager.PendingAnimations);
|
|
}
|
|
|
|
[Fact]
|
|
public void Publish_UsesOutdoorEffectCellWithoutChangingRenderParent()
|
|
{
|
|
var poses = new EntityEffectPoseRegistry();
|
|
WorldEntity entity = Entity(12u, Vector3.Zero);
|
|
entity.ParentCellId = null;
|
|
entity.EffectCellId = 0xA9B4000Bu;
|
|
|
|
poses.Publish(entity, Array.Empty<Matrix4x4>());
|
|
|
|
Assert.True(poses.TryGetCellId(entity.Id, out uint cellId));
|
|
Assert.Equal(0xA9B4000Bu, cellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChangeNotifications_SuppressEquivalentStaticPublishes()
|
|
{
|
|
var poses = new EntityEffectPoseRegistry();
|
|
WorldEntity entity = Entity(13u, new Vector3(1, 2, 3));
|
|
var changed = new List<uint>();
|
|
poses.EffectPoseChanged += changed.Add;
|
|
Matrix4x4 part = Matrix4x4.CreateTranslation(0, 0, 1);
|
|
|
|
poses.Publish(entity, new[] { part });
|
|
poses.Publish(entity, new[] { part });
|
|
Assert.True(poses.UpdateRoot(entity));
|
|
Assert.Equal(new[] { 13u }, changed);
|
|
|
|
entity.SetPosition(new Vector3(4, 5, 6));
|
|
Assert.True(poses.UpdateRoot(entity));
|
|
Assert.Equal(new[] { 13u, 13u }, changed);
|
|
|
|
poses.Publish(entity, new[] { Matrix4x4.CreateTranslation(0, 0, 2) });
|
|
Assert.Equal(new[] { 13u, 13u, 13u }, changed);
|
|
|
|
Assert.True(poses.Remove(entity.Id));
|
|
Assert.Equal(new[] { 13u, 13u, 13u, 13u }, changed);
|
|
}
|
|
|
|
private static WorldEntity Entity(uint id, Vector3 position) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = id,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = position,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
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;
|
|
}
|
|
}
|