refactor(animation): extract live PartArray presenter
Move final live part composition, exact-incarnation schedule handoff, MotionDone binding, and presentation diagnostics out of GameWindow while preserving the retail frame order. Reject stale schedule and completion ABA at the new owner boundary. Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
9760ff5a8d
commit
f004ac5562
14 changed files with 963 additions and 438 deletions
|
|
@ -139,7 +139,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
-MathF.PI / 2f),
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var animated = new GameWindow.AnimatedEntity
|
||||
var animated = new LiveEntityAnimationState
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = new Setup(),
|
||||
|
|
@ -148,7 +148,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
HighFrame = 0,
|
||||
Framerate = 0f,
|
||||
Scale = 1f,
|
||||
PartTemplate = Array.Empty<GameWindow.AnimatedPartTemplate>(),
|
||||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
};
|
||||
var motion = new GameWindow.RemoteMotion();
|
||||
|
|
@ -205,7 +205,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Rotation = initial,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var animated = new GameWindow.AnimatedEntity
|
||||
var animated = new LiveEntityAnimationState
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = new Setup(),
|
||||
|
|
@ -214,7 +214,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
HighFrame = 0,
|
||||
Framerate = 0f,
|
||||
Scale = 1f,
|
||||
PartTemplate = Array.Empty<GameWindow.AnimatedPartTemplate>(),
|
||||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
};
|
||||
var motion = new GameWindow.RemoteMotion
|
||||
|
|
@ -260,7 +260,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
Rotation = initial,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var animated = new GameWindow.AnimatedEntity
|
||||
var animated = new LiveEntityAnimationState
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = new Setup(),
|
||||
|
|
@ -269,7 +269,7 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
HighFrame = 0,
|
||||
Framerate = 0f,
|
||||
Scale = 1f,
|
||||
PartTemplate = Array.Empty<GameWindow.AnimatedPartTemplate>(),
|
||||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
};
|
||||
var motion = new GameWindow.RemoteMotion();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public sealed class LiveAppearanceAnimationTests
|
|||
var animation = new Animation();
|
||||
var sequencer = new AnimationSequencer(setup, new MotionTable(), new NullLoader());
|
||||
var oldEntity = Entity(0x70000001u, 0x01000001u);
|
||||
var state = new GameWindow.AnimatedEntity
|
||||
var state = new LiveEntityAnimationState
|
||||
{
|
||||
Entity = oldEntity,
|
||||
Setup = setup,
|
||||
|
|
@ -24,7 +24,7 @@ public sealed class LiveAppearanceAnimationTests
|
|||
HighFrame = 9,
|
||||
Framerate = 30f,
|
||||
Scale = 1f,
|
||||
PartTemplate = [new GameWindow.AnimatedPartTemplate(0x01000001u, null, true)],
|
||||
PartTemplate = [new LiveAnimationPartTemplate(0x01000001u, null, true)],
|
||||
PartAvailability = [true],
|
||||
CurrFrame = 6.5f,
|
||||
Sequencer = sequencer,
|
||||
|
|
@ -42,8 +42,8 @@ public sealed class LiveAppearanceAnimationTests
|
|||
setup,
|
||||
1.25f,
|
||||
[
|
||||
new GameWindow.AnimatedPartTemplate(0x01000002u, null, true),
|
||||
new GameWindow.AnimatedPartTemplate(0x01000003u, null, true),
|
||||
new LiveAnimationPartTemplate(0x01000002u, null, true),
|
||||
new LiveAnimationPartTemplate(0x01000003u, null, true),
|
||||
],
|
||||
[true, true]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,295 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class LiveEntityAnimationPresenterTests
|
||||
{
|
||||
private const uint Guid = 0x70000031u;
|
||||
private const uint Cell = 0x01010001u;
|
||||
|
||||
[Fact]
|
||||
public void Present_ComposesDistinctVisualAndRigidChannels()
|
||||
{
|
||||
var fixture = Build(partCount: 2, scale: 2f);
|
||||
fixture.State.Setup.DefaultScale[0] = new Vector3(3f, 4f, 5f);
|
||||
fixture.State.PartTemplate =
|
||||
[
|
||||
new LiveAnimationPartTemplate(0x01000001u, new Dictionary<uint, uint> { [1] = 2 }, true),
|
||||
new LiveAnimationPartTemplate(0x01000002u, null, false),
|
||||
];
|
||||
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
|
||||
PartTransform[] frames =
|
||||
[
|
||||
new(new Vector3(1f, 2f, 3f), rotation),
|
||||
new(new Vector3(4f, 5f, 6f), Quaternion.Identity),
|
||||
];
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var presenter = Presenter(fixture.Live, poses, new Context());
|
||||
|
||||
presenter.Present(Schedules(fixture, frames));
|
||||
|
||||
Assert.Single(fixture.Entity.MeshRefs);
|
||||
Matrix4x4 expectedVisual = Matrix4x4.CreateScale(3f, 4f, 5f)
|
||||
* Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(1f, 2f, 3f)
|
||||
* Matrix4x4.CreateScale(2f);
|
||||
Matrix4x4 expectedRigid = Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(2f, 4f, 6f);
|
||||
Assert.Equal(expectedVisual, fixture.Entity.MeshRefs[0].PartTransform);
|
||||
Assert.Equal(expectedRigid, fixture.Entity.IndexedPartTransforms[0]);
|
||||
Assert.Equal(2, fixture.Entity.IndexedPartTransforms.Count);
|
||||
Assert.True(poses.TryGetPartPose(fixture.Entity.Id, 0, out Matrix4x4 effectPose));
|
||||
Assert.Equal(expectedRigid, effectPose);
|
||||
Assert.Equal(2u, fixture.Entity.MeshRefs[0].SurfaceOverrides![1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OldSchedule_IsRejectedForCurrentOwnerEvenWhenIndexedByItsLocalId()
|
||||
{
|
||||
var old = Build(partCount: 1);
|
||||
LiveEntityAnimationSchedule stale = Schedule(old,
|
||||
[new PartTransform(new Vector3(99f, 0f, 0f), Quaternion.Identity)]);
|
||||
Assert.True(old.Live.ClearAnimationRuntime(Guid));
|
||||
LiveEntityAnimationState replacement = State(old.Entity, partCount: 1, scale: 1f);
|
||||
old.Live.SetAnimationRuntime(Guid, replacement);
|
||||
var presenter = Presenter(old.Live, new EntityEffectPoseRegistry(), new Context());
|
||||
|
||||
presenter.Present(new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[old.Entity.Id] = stale,
|
||||
});
|
||||
|
||||
Assert.Empty(replacement.MeshRefsScratch);
|
||||
Assert.Empty(replacement.EffectPartPosesScratch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionDone_OldComponentCannotResolveReplacementByGuid()
|
||||
{
|
||||
var fixture = Build(partCount: 1, withSequencer: true);
|
||||
var context = new Context();
|
||||
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), context);
|
||||
presenter.PrepareAnimation(fixture.Record, fixture.State);
|
||||
Action<uint, bool> oldCallback = fixture.State.Sequencer!.MotionDoneTarget!;
|
||||
|
||||
Assert.True(fixture.Live.ClearAnimationRuntime(Guid));
|
||||
fixture.Live.SetAnimationRuntime(Guid, State(fixture.Entity, 1, 1f));
|
||||
oldCallback(0x10000001u, true);
|
||||
|
||||
Assert.Equal(0, context.ResolveCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectPoseCallback_NestedRuntimeSnapshotDoesNotTruncatePresentation()
|
||||
{
|
||||
var first = Build(partCount: 1, guid: Guid);
|
||||
var second = Add(first.Live, Guid + 1, partCount: 1);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.EffectPoseChanged += _ =>
|
||||
{
|
||||
var nested = new List<LiveEntityRecord>();
|
||||
first.Live.CopySpatialRootObjectRecordsTo(nested);
|
||||
Assert.Equal(2, nested.Count);
|
||||
};
|
||||
var presenter = Presenter(first.Live, poses, new Context());
|
||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[first.Entity.Id] = Schedule(first,
|
||||
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
||||
[second.Entity.Id] = Schedule(second,
|
||||
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
|
||||
};
|
||||
|
||||
presenter.Present(schedules);
|
||||
|
||||
Assert.Single(first.Entity.MeshRefs);
|
||||
Assert.Single(second.Entity.MeshRefs);
|
||||
}
|
||||
|
||||
private static LiveEntityAnimationPresenter Presenter(
|
||||
LiveEntityRuntime live,
|
||||
EntityEffectPoseRegistry poses,
|
||||
Context context) => new(
|
||||
() => live,
|
||||
() => null,
|
||||
poses,
|
||||
context,
|
||||
AnimationPresentationDiagnostics.Disabled,
|
||||
hidePartIndex: -1);
|
||||
|
||||
private static IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Schedules(
|
||||
Fixture fixture,
|
||||
IReadOnlyList<PartTransform> frames) =>
|
||||
new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[fixture.Entity.Id] = Schedule(fixture, frames),
|
||||
};
|
||||
|
||||
private static LiveEntityAnimationSchedule Schedule(
|
||||
Fixture fixture,
|
||||
IReadOnlyList<PartTransform> frames) => new(
|
||||
frames,
|
||||
0f,
|
||||
true,
|
||||
fixture.Record,
|
||||
fixture.Entity,
|
||||
fixture.State,
|
||||
fixture.Record.ObjectClockEpoch,
|
||||
fixture.Record.ProjectionMutationVersion);
|
||||
|
||||
private static Fixture Build(
|
||||
int partCount,
|
||||
float scale = 1f,
|
||||
bool withSequencer = true,
|
||||
uint guid = Guid)
|
||||
{
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0101FFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
var live = new LiveEntityRuntime(
|
||||
spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
return Add(live, guid, partCount, scale, withSequencer);
|
||||
}
|
||||
|
||||
private static Fixture Add(
|
||||
LiveEntityRuntime live,
|
||||
uint guid,
|
||||
int partCount,
|
||||
float scale = 1f,
|
||||
bool withSequencer = true)
|
||||
{
|
||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!;
|
||||
WorldEntity entity = live.MaterializeLiveEntity(
|
||||
guid,
|
||||
Cell,
|
||||
id => new WorldEntity
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = guid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
ParentCellId = Cell,
|
||||
})!;
|
||||
LiveEntityAnimationState state = State(entity, partCount, scale, withSequencer);
|
||||
entity.SetIndexedPartPoses(
|
||||
Enumerable.Repeat(Matrix4x4.Identity, partCount).ToArray(),
|
||||
Enumerable.Repeat(true, partCount).ToArray());
|
||||
record.HasPartArray = true;
|
||||
live.SetAnimationRuntime(guid, state);
|
||||
return new Fixture(live, record, entity, state);
|
||||
}
|
||||
|
||||
private static LiveEntityAnimationState State(
|
||||
WorldEntity entity,
|
||||
int partCount,
|
||||
float scale,
|
||||
bool withSequencer = false)
|
||||
{
|
||||
var setup = new Setup();
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
setup.Parts.Add(0x01000001u + (uint)i);
|
||||
setup.DefaultScale.Add(Vector3.One);
|
||||
}
|
||||
return new LiveEntityAnimationState
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = setup,
|
||||
Animation = new Animation(),
|
||||
LowFrame = 0,
|
||||
HighFrame = 0,
|
||||
Framerate = 0f,
|
||||
Scale = scale,
|
||||
PartTemplate = Enumerable.Range(0, partCount)
|
||||
.Select(i => new LiveAnimationPartTemplate(0x01000001u + (uint)i, null, true))
|
||||
.ToArray(),
|
||||
PartAvailability = Enumerable.Repeat(true, partCount).ToArray(),
|
||||
Sequencer = withSequencer
|
||||
? new AnimationSequencer(setup, new MotionTable(), new NullLoader())
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(Cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: 0u,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: null,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: null,
|
||||
Children: null,
|
||||
Scale: null,
|
||||
Friction: null,
|
||||
Elasticity: null,
|
||||
Translucency: null,
|
||||
Velocity: null,
|
||||
Acceleration: null,
|
||||
AngularVelocity: null,
|
||||
DefaultScriptType: null,
|
||||
DefaultScriptIntensity: null,
|
||||
Timestamps: timestamps);
|
||||
return new WorldSession.EntitySpawn(
|
||||
guid,
|
||||
position,
|
||||
0x02000001u,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
"presenter fixture",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
PhysicsState: 0u,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private sealed class Context : ILiveAnimationPresentationContext
|
||||
{
|
||||
public uint LocalPlayerGuid => 0x50000001u;
|
||||
public int ResolveCount { get; private set; }
|
||||
public MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record)
|
||||
{
|
||||
ResolveCount++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
|
||||
private sealed record Fixture(
|
||||
LiveEntityRuntime Live,
|
||||
LiveEntityRecord Record,
|
||||
WorldEntity Entity,
|
||||
LiveEntityAnimationState State);
|
||||
}
|
||||
|
|
@ -615,7 +615,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
}
|
||||
|
||||
private static (LiveEntityRuntime Live, LiveEntityRecord Record,
|
||||
GameWindow.AnimatedEntity Animation) BuildHiddenAnimated(uint guid)
|
||||
LiveEntityAnimationState Animation) BuildHiddenAnimated(uint guid)
|
||||
{
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
|
|
@ -640,7 +640,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
ParentCellId = Cell,
|
||||
})!;
|
||||
var setup = new Setup();
|
||||
var animation = new GameWindow.AnimatedEntity
|
||||
var animation = new LiveEntityAnimationState
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = setup,
|
||||
|
|
@ -649,7 +649,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
HighFrame = 0,
|
||||
Framerate = 0f,
|
||||
Scale = 1f,
|
||||
PartTemplate = Array.Empty<GameWindow.AnimatedPartTemplate>(),
|
||||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||
PartAvailability = Array.Empty<bool>(),
|
||||
Sequencer = new AnimationSequencer(
|
||||
setup,
|
||||
|
|
@ -662,7 +662,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
}
|
||||
|
||||
private static (LiveEntityRuntime Live, LiveEntityRecord Record,
|
||||
GameWindow.AnimatedEntity Animation) BuildVisibleAnimatedWithPosFrames(
|
||||
LiveEntityAnimationState Animation) BuildVisibleAnimatedWithPosFrames(
|
||||
uint guid,
|
||||
Quaternion? rootOrientation = null,
|
||||
Vector3? rootOrigin = null)
|
||||
|
|
@ -694,7 +694,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
var loader = new DictionaryAnimationLoader(animationId, datAnimation);
|
||||
var sequencer = new AnimationSequencer(setup, new MotionTable(), loader);
|
||||
Assert.True(sequencer.InitializeSetupDefaultAnimation(animationId));
|
||||
var animation = new GameWindow.AnimatedEntity
|
||||
var animation = new LiveEntityAnimationState
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = setup,
|
||||
|
|
@ -705,7 +705,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
Scale = 1f,
|
||||
PartTemplate = new[]
|
||||
{
|
||||
new GameWindow.AnimatedPartTemplate(
|
||||
new LiveAnimationPartTemplate(
|
||||
0x0100AA01u,
|
||||
SurfaceOverrides: null,
|
||||
IsDrawable: true),
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
scheduler.Tick(0.02f);
|
||||
scheduler.ProcessHooks();
|
||||
Assert.Equal(1, captures);
|
||||
Assert.True(scheduler.TryTakeLivePartFrames(OwnerId, out var frames));
|
||||
Assert.True(scheduler.TryTakePreparedFramesForTest(OwnerId, out var frames));
|
||||
|
||||
var reference = new AnimationSequencer(
|
||||
setup,
|
||||
|
|
@ -269,7 +269,7 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
Assert.True(sequencer.InitializeSetupDefaultAnimation(alternateAnimationId));
|
||||
scheduler.Tick(0.01f);
|
||||
|
||||
Assert.True(scheduler.TryTakeLivePartFrames(OwnerId, out var frames));
|
||||
Assert.True(scheduler.TryTakePreparedFramesForTest(OwnerId, out var frames));
|
||||
Assert.True(frames[0].Origin.X >= 20f);
|
||||
Vector3 rotatedX = Vector3.Transform(Vector3.UnitX, entity.Rotation);
|
||||
Assert.InRange(rotatedX.X, -0.001f, 0.001f);
|
||||
|
|
@ -312,7 +312,7 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
scheduler.Tick(0.1f);
|
||||
|
||||
Assert.Equal(0, rootCommits);
|
||||
Assert.True(scheduler.TryTakeLivePartFrames(OwnerId, out _));
|
||||
Assert.True(scheduler.TryTakePreparedFramesForTest(OwnerId, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -349,7 +349,7 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
scheduler.Tick(0.1f);
|
||||
scheduler.ProcessHooks();
|
||||
|
||||
Assert.False(scheduler.TryTakeLivePartFrames(OwnerId, out _));
|
||||
Assert.False(scheduler.TryTakePreparedFramesForTest(OwnerId, out _));
|
||||
Assert.Equal(0, captures);
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +422,7 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
scheduler.Tick(0.2f);
|
||||
|
||||
Assert.DoesNotContain("animation_done", order);
|
||||
Assert.True(scheduler.TryTakeLivePartFrames(OwnerId, out _));
|
||||
Assert.True(scheduler.TryTakePreparedFramesForTest(OwnerId, out _));
|
||||
order.Add("parts_published");
|
||||
order.Add("children_updated");
|
||||
scheduler.ProcessHooks();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue