feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
509
src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs
Normal file
509
src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
using System.Numerics;
|
||||
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 AnimatedEntity = AcDream.App.Rendering.GameWindow.AnimatedEntity;
|
||||
using RemoteMotion = AcDream.App.Rendering.GameWindow.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 Func<LiveEntityRuntime?> _liveEntities;
|
||||
private readonly Func<uint> _localPlayerGuid;
|
||||
private readonly RemotePhysicsUpdater _remotePhysics;
|
||||
private readonly LiveEntityOrdinaryPhysicsUpdater _ordinaryPhysics;
|
||||
private readonly Func<ProjectileController?> _projectiles;
|
||||
private readonly Action<WorldEntity> _publishRootPose;
|
||||
private readonly Action<uint, AnimationSequencer> _captureAnimationHooks;
|
||||
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(
|
||||
Func<LiveEntityRuntime?> liveEntities,
|
||||
Func<uint> localPlayerGuid,
|
||||
RemotePhysicsUpdater remotePhysics,
|
||||
LiveEntityOrdinaryPhysicsUpdater ordinaryPhysics,
|
||||
Func<ProjectileController?> projectiles,
|
||||
Action<WorldEntity> publishRootPose,
|
||||
Action<uint, AnimationSequencer> captureAnimationHooks)
|
||||
{
|
||||
_liveEntities = liveEntities
|
||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_localPlayerGuid = localPlayerGuid
|
||||
?? throw new ArgumentNullException(nameof(localPlayerGuid));
|
||||
_remotePhysics = remotePhysics
|
||||
?? throw new ArgumentNullException(nameof(remotePhysics));
|
||||
_ordinaryPhysics = ordinaryPhysics
|
||||
?? throw new ArgumentNullException(nameof(ordinaryPhysics));
|
||||
_projectiles = projectiles
|
||||
?? throw new ArgumentNullException(nameof(projectiles));
|
||||
_publishRootPose = publishRootPose
|
||||
?? throw new ArgumentNullException(nameof(publishRootPose));
|
||||
_captureAnimationHooks = captureAnimationHooks
|
||||
?? throw new ArgumentNullException(nameof(captureAnimationHooks));
|
||||
}
|
||||
|
||||
/// <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<uint, AnimatedEntity>? prepareAnimation = null)
|
||||
{
|
||||
_schedules.Clear();
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null)
|
||||
return _schedules;
|
||||
|
||||
runtime.CopySpatialRootObjectRecordsTo(_rootSnapshot);
|
||||
foreach (LiveEntityRecord record in _rootSnapshot)
|
||||
{
|
||||
if (!runtime.IsCurrentSpatialRootObject(record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AnimatedEntity? animation = record.AnimationRuntime as AnimatedEntity;
|
||||
RemoteMotion? remote = record.RemoteMotionRuntime as RemoteMotion;
|
||||
ProjectileController.Runtime? projectile =
|
||||
record.ProjectileRuntime as ProjectileController.Runtime;
|
||||
ulong objectClockEpoch = record.ObjectClockEpoch;
|
||||
|
||||
if (animation is not null)
|
||||
prepareAnimation?.Invoke(record.ServerGuid, 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))
|
||||
{
|
||||
_schedules[entity.Id] = schedule;
|
||||
}
|
||||
}
|
||||
|
||||
return _schedules;
|
||||
}
|
||||
|
||||
private LiveEntityAnimationSchedule AdvanceRecord(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
AnimatedEntity? 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 == _localPlayerGuid())
|
||||
{
|
||||
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.
|
||||
_publishRootPose(entity);
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
remote,
|
||||
projectile,
|
||||
objectClockEpoch))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return new LiveEntityAnimationSchedule(
|
||||
prepared ?? (composeHidden ? 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,
|
||||
_captureAnimationHooks,
|
||||
sequencer,
|
||||
runtime,
|
||||
record,
|
||||
objectClockEpoch))
|
||||
{
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sequencer is not null)
|
||||
{
|
||||
_captureAnimationHooks(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 = 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,
|
||||
_captureAnimationHooks,
|
||||
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,
|
||||
_captureAnimationHooks))
|
||||
{
|
||||
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)
|
||||
_captureAnimationHooks(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;
|
||||
}
|
||||
|
||||
_publishRootPose(entity);
|
||||
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
||||
return default;
|
||||
|
||||
if (hidden)
|
||||
{
|
||||
return new LiveEntityAnimationSchedule(
|
||||
sequencer?.SampleCurrentPose(),
|
||||
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,
|
||||
AnimatedEntity? 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue