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:
Erik 2026-07-21 09:37:02 +02:00
parent 9760ff5a8d
commit f004ac5562
14 changed files with 963 additions and 438 deletions

View file

@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.App.Rendering.Vfx;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
@ -17,7 +18,7 @@ namespace AcDream.App.Rendering;
/// resource lifetime. Setup default installation itself is unconditional and
/// belongs to PartArray construction; this class owns only the Static workset.
/// </summary>
internal sealed class RetailStaticAnimatingObjectScheduler
internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFrameSource
{
private const double FrameEpsilon = 0.000199999995;
private const float MaximumElapsed = 2f;
@ -162,10 +163,24 @@ internal sealed class RetailStaticAnimatingObjectScheduler
/// the single owner of mesh identity, overrides, and appearance rebinding.
/// </summary>
public bool TryTakeLivePartFrames(
uint ownerId,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState animation,
ulong objectClockEpoch,
ulong projectionMutationVersion,
out IReadOnlyList<PartTransform> frames)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(entity);
ArgumentNullException.ThrowIfNull(animation);
uint ownerId = entity.Id;
if (_owners.TryGetValue(ownerId, out Owner? owner)
&& ReferenceEquals(record.WorldEntity, entity)
&& ReferenceEquals(record.AnimationRuntime, animation)
&& record.ObjectClockEpoch == objectClockEpoch
&& record.ProjectionMutationVersion == projectionMutationVersion
&& ReferenceEquals(owner.Entity, entity)
&& ReferenceEquals(owner.Sequencer, animation.Sequencer)
&& owner.Entity.ServerGuid != 0
&& owner.PreparedLivePartFrames is { } prepared
&& IsResidentAtVersion(owner, owner.PendingResidencyVersion))
@ -182,6 +197,29 @@ internal sealed class RetailStaticAnimatingObjectScheduler
return false;
}
/// <summary>
/// Scheduler-only test seam for DAT/static timing tests which deliberately
/// do not construct a <see cref="LiveEntityRuntime"/>. Runtime integration
/// must use the exact-incarnation overload above.
/// </summary>
internal bool TryTakePreparedFramesForTest(
uint ownerId,
out IReadOnlyList<PartTransform> frames)
{
if (_owners.TryGetValue(ownerId, out Owner? owner)
&& owner.PreparedLivePartFrames is { } prepared
&& IsResidentAtVersion(owner, owner.PendingResidencyVersion))
{
owner.PreparedLivePartFrames = null;
frames = prepared;
return true;
}
if (_owners.TryGetValue(ownerId, out owner))
InvalidatePending(owner);
frames = Array.Empty<PartTransform>();
return false;
}
public void Unregister(uint ownerId) => _owners.Remove(ownerId);
public void CopyAnimatedEntityIdsTo(HashSet<uint> destination)