acdream/src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs

543 lines
20 KiB
C#

using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Physics;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
using DatReaderWriter.Types;
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
namespace AcDream.App.Rendering;
/// <summary>
/// Owns retail's ordinary live-object workset. <c>CPhysics::UseTime</c>
/// (0x00509950) walks the object table, not the render-animation table; an
/// animation, MovementManager, projectile body, or effect owner is therefore
/// an optional component of one canonical <see cref="LiveEntityRecord"/>.
/// </summary>
internal sealed class LiveEntityAnimationScheduler
{
private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _localPlayer;
private readonly RemotePhysicsUpdater _remotePhysics;
private readonly LiveEntityOrdinaryPhysicsUpdater _ordinaryPhysics;
private readonly ProjectileController _projectiles;
private readonly IEntityRootPosePublisher _rootPoses;
private readonly IAnimationHookCaptureSink _animationHooks;
private readonly List<LiveEntityRecord> _rootSnapshot = new();
private readonly Dictionary<uint, LiveEntityAnimationSchedule> _schedules = new();
private readonly Frame _rootFrameScratch = new();
private readonly MotionDeltaFrame _rootDeltaScratch = new();
public LiveEntityAnimationScheduler(
LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource localPlayer,
RemotePhysicsUpdater remotePhysics,
LiveEntityOrdinaryPhysicsUpdater ordinaryPhysics,
ProjectileController projectiles,
IEntityRootPosePublisher rootPoses,
IAnimationHookCaptureSink animationHooks)
{
_liveEntities = liveEntities
?? throw new ArgumentNullException(nameof(liveEntities));
_localPlayer = localPlayer
?? throw new ArgumentNullException(nameof(localPlayer));
_remotePhysics = remotePhysics
?? throw new ArgumentNullException(nameof(remotePhysics));
_ordinaryPhysics = ordinaryPhysics
?? throw new ArgumentNullException(nameof(ordinaryPhysics));
_projectiles = projectiles ?? throw new ArgumentNullException(nameof(projectiles));
_rootPoses = rootPoses ?? throw new ArgumentNullException(nameof(rootPoses));
_animationHooks = animationHooks ?? throw new ArgumentNullException(nameof(animationHooks));
}
/// <summary>
/// Advances a stable snapshot of the complete ordinary-object table and
/// returns pose work only for animation owners that survived every
/// callback. The returned dictionary is reused and valid until the next
/// call.
/// </summary>
public IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Tick(
float elapsedSeconds,
Vector3? playerPosition,
bool localHiddenPartPoseDirty,
int liveCenterX,
int liveCenterY,
Action<LiveEntityRecord, LiveEntityAnimationState>? prepareAnimation = null)
{
_schedules.Clear();
LiveEntityRuntime runtime = _liveEntities;
runtime.CopySpatialRootObjectRecordsTo(_rootSnapshot);
foreach (LiveEntityRecord record in _rootSnapshot)
{
if (!runtime.IsCurrentSpatialRootObject(record)
|| record.WorldEntity is not { } entity)
{
continue;
}
LiveEntityAnimationState? animation = record.AnimationRuntime as LiveEntityAnimationState;
RemoteMotion? remote = record.RemoteMotionRuntime as RemoteMotion;
ProjectileController.Runtime? projectile =
record.ProjectileRuntime as ProjectileController.Runtime;
ulong objectClockEpoch = record.ObjectClockEpoch;
if (!IsCurrent(
runtime,
record,
entity,
animation,
remote,
projectile,
objectClockEpoch))
continue;
if (animation is not null)
{
prepareAnimation?.Invoke(record, animation);
if (!IsCurrent(
runtime,
record,
entity,
animation,
remote,
projectile,
objectClockEpoch))
{
continue;
}
}
LiveEntityAnimationSchedule schedule = AdvanceRecord(
runtime,
record,
entity,
animation,
remote,
projectile,
elapsedSeconds,
playerPosition,
localHiddenPartPoseDirty,
liveCenterX,
liveCenterY,
objectClockEpoch);
if (animation is not null
&& schedule.ComposeParts
&& IsCurrent(
runtime,
record,
entity,
animation,
remote,
projectile,
objectClockEpoch))
{
IReadOnlyList<PartTransform>? ownedFrames = schedule.SequenceFrames is { } produced
? animation.CaptureScheduleFrames(produced)
: null;
_schedules[entity.Id] = schedule with
{
SequenceFrames = ownedFrames,
Record = record,
Entity = entity,
Animation = animation,
ObjectClockEpoch = objectClockEpoch,
ProjectionMutationVersion = record.ProjectionMutationVersion,
PresentationRevision = animation.PresentationRevision,
};
}
}
return _schedules;
}
private LiveEntityAnimationSchedule AdvanceRecord(
LiveEntityRuntime runtime,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState? animation,
RemoteMotion? remote,
ProjectileController.Runtime? projectile,
float elapsedSeconds,
Vector3? playerPosition,
bool localHiddenPartPoseDirty,
int liveCenterX,
int liveCenterY,
ulong objectClockEpoch)
{
uint serverGuid = record.ServerGuid;
AnimationSequencer? sequencer = animation?.Sequencer;
PhysicsStateFlags state = record.FinalPhysicsState;
bool hidden = (state & PhysicsStateFlags.Hidden) != 0;
// PlayerMovementController owns this exact record clock and advances
// its PartArray before local collision. Consume that prepared pose;
// never tick the same clock or sequence a second time here.
if (serverGuid == _localPlayer.ServerGuid)
{
IReadOnlyList<PartTransform>? prepared =
animation?.PreparedSequenceFrames;
bool advanced = animation?.SequenceAdvancedBeforeAnimationPass == true;
if (animation is not null)
{
animation.PreparedSequenceFrames = null;
animation.SequenceAdvancedBeforeAnimationPass = false;
}
bool composeHidden = hidden && localHiddenPartPoseDirty;
if (!IsCurrent(
runtime,
record,
entity,
animation,
remote,
projectile,
objectClockEpoch))
{
return default;
}
// Local projection interpolates the visible root on render frames
// that are smaller than an admitted object quantum. Publish that
// current root independently of PartArray recomposition so local
// attached effects and lights never trail the rendered character.
_rootPoses.UpdateRoot(entity);
if (!IsCurrent(
runtime,
record,
entity,
animation,
remote,
projectile,
objectClockEpoch))
{
return default;
}
return new LiveEntityAnimationSchedule(
prepared ?? (composeHidden && sequencer is not null
? animation?.CaptureSequenceFrames(sequencer.SampleCurrentPose())
: null),
0f,
ComposeParts: advanced || composeHidden);
}
RetailObjectActivityResult activity = RetailObjectActivityGate.Evaluate(
record.ObjectClock,
remote?.Body ?? projectile?.Body ?? record.PhysicsBody,
runtime.GetRootObjectClockDisposition(serverGuid)
is RetailObjectClockDisposition.Advance,
record.HasPartArray,
(state & PhysicsStateFlags.Static) != 0,
entity.Position,
playerPosition,
elapsedSeconds);
if (activity is not RetailObjectActivityResult.Active)
return default;
RetailObjectQuantumBatch batch = record.ObjectClock.Advance(elapsedSeconds);
if (batch.Count == 0)
return default;
IReadOnlyList<PartTransform>? frames = null;
float legacyElapsed = 0f;
bool completed = true;
ProjectileController projectileController = _projectiles;
bool projectileHandlesMovement = projectile is not null
&& projectileController?.HandlesMovement(serverGuid) == true;
float objectScale = animation?.Scale
?? record.Snapshot.Physics?.Scale
?? record.Snapshot.ObjScale
?? entity.Scale;
for (int qi = 0; qi < batch.Count; qi++)
{
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
float quantum = batch.GetQuantum(qi);
if (hidden)
{
if (remote is not null)
{
if (!_remotePhysics.TickHidden(
remote,
entity,
quantum,
sequencer?.Manager,
_animationHooks.Capture,
sequencer,
runtime,
record,
objectClockEpoch))
{
completed = false;
break;
}
}
else
{
if (sequencer is not null)
{
_animationHooks.Capture(entity.Id, sequencer);
if (!IsCurrent(
runtime,
record,
entity,
animation,
remote,
projectile,
objectClockEpoch))
{
completed = false;
break;
}
}
RunManagerTail(remote, sequencer?.Manager);
}
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
continue;
}
Frame rootFrame = animation?.RootMotionScratch ?? _rootFrameScratch;
rootFrame.Origin = Vector3.Zero;
rootFrame.Orientation = Quaternion.Identity;
if (sequencer is not null)
frames = animation?.CaptureSequenceFrames(
sequencer.Advance(quantum, rootFrame))
?? sequencer.Advance(quantum, rootFrame);
else if (animation is not null)
legacyElapsed += quantum;
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
if (remote is not null && !projectileHandlesMovement)
{
if (animation is not null && quantum > 0f)
{
float rootMotionSpeed = rootFrame.Origin.Length()
* objectScale / quantum;
remote.MaxRootMotionSpeedSinceLastUP = MathF.Max(
remote.MaxRootMotionSpeedSinceLastUP,
rootMotionSpeed);
}
MotionDeltaFrame rootDelta = animation?.RootMotionDeltaScratch
?? _rootDeltaScratch;
rootDelta.Origin = rootFrame.Origin;
rootDelta.Orientation = rootFrame.Orientation;
if (!_remotePhysics.Tick(
remote,
entity,
objectScale,
sequencer,
animation,
quantum,
rootDelta,
liveCenterX,
liveCenterY,
_animationHooks.Capture,
runtime,
record,
objectClockEpoch))
{
completed = false;
break;
}
}
else
{
bool ordinaryBodyHandlesMovement = projectile is null
&& record.PhysicsBody is not null;
if (ordinaryBodyHandlesMovement)
{
if (!_ordinaryPhysics.Tick(
runtime,
record,
entity,
rootFrame,
objectScale,
quantum,
liveCenterX,
liveCenterY,
objectClockEpoch,
sequencer,
_animationHooks.Capture))
{
completed = false;
break;
}
}
else
{
ApplyRootFrame(record, entity, rootFrame, objectScale);
}
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
ProjectileController.QuantumStep projectileStep = default;
bool beganProjectile = projectileHandlesMovement
&& projectileController?.TryBeginQuantum(
record,
quantum,
out projectileStep) == true;
if (!ordinaryBodyHandlesMovement && sequencer is not null)
_animationHooks.Capture(entity.Id, sequencer);
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
if (beganProjectile
&& !projectileController!.CompleteQuantum(
projectileStep,
liveCenterX,
liveCenterY))
{
completed = false;
break;
}
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
RunManagerTail(remote, sequencer?.Manager);
}
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
completed = false;
break;
}
}
if (!completed
|| !IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
{
return default;
}
_rootPoses.UpdateRoot(entity);
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
return default;
if (hidden)
{
return new LiveEntityAnimationSchedule(
sequencer is not null && animation is not null
? animation.CaptureSequenceFrames(sequencer.SampleCurrentPose())
: null,
0f,
ComposeParts: animation is not null);
}
return new LiveEntityAnimationSchedule(
frames,
legacyElapsed,
ComposeParts: animation is not null);
}
private static void ApplyRootFrame(
LiveEntityRecord record,
WorldEntity entity,
Frame rootFrame,
float objectScale)
{
PhysicsBody? body = record.PhysicsBody;
Vector3 position = body?.Position ?? entity.Position;
Quaternion orientation = body?.Orientation ?? entity.Rotation;
Vector3 localOrigin = body?.OnWalkable == true
? rootFrame.Origin * objectScale
: Vector3.Zero;
if (localOrigin != Vector3.Zero)
position += Vector3.Transform(localOrigin, orientation);
if (!rootFrame.Orientation.IsIdentity)
{
orientation = FrameOps.SetRotate(
position,
orientation,
orientation * rootFrame.Orientation);
}
if (body is not null)
{
body.Position = position;
body.Orientation = orientation;
// Projectile integration follows this compose. Manager-less
// ordinary bodies use LiveEntityOrdinaryPhysicsUpdater so their
// complete Frame travels through Transition/SetPositionInternal.
}
entity.SetPosition(position);
entity.Rotation = orientation;
}
private static bool IsCurrent(
LiveEntityRuntime runtime,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState? animation,
RemoteMotion? remote,
ProjectileController.Runtime? projectile,
ulong objectClockEpoch) =>
runtime.IsCurrentSpatialRootObject(record)
&& record.ObjectClockEpoch == objectClockEpoch
&& ReferenceEquals(record.WorldEntity, entity)
&& ReferenceEquals(record.AnimationRuntime, animation)
&& ReferenceEquals(record.RemoteMotionRuntime, remote)
&& ReferenceEquals(record.ProjectileRuntime, projectile)
&& (animation is null || runtime.IsCurrentSpatialAnimation(record, animation));
private static void RunManagerTail(
RemoteMotion? remote,
MotionTableManager? partArray)
{
if (remote is not null)
{
RetailObjectManagerTail.Run(
remote.Host?.TargetManager,
remote.Movement,
partArray,
remote.Host?.PositionManager);
}
else
{
partArray?.UseTime();
}
}
}
internal readonly record struct LiveEntityAnimationSchedule(
IReadOnlyList<PartTransform>? SequenceFrames,
float LegacyAdvanceSeconds,
bool ComposeParts,
LiveEntityRecord? Record = null,
WorldEntity? Entity = null,
LiveEntityAnimationState? Animation = null,
ulong ObjectClockEpoch = 0UL,
ulong ProjectionMutationVersion = 0UL,
ulong PresentationRevision = 0UL);