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:
Erik 2026-07-20 09:10:31 +02:00
parent 31a0889f08
commit f961d70023
77 changed files with 12513 additions and 1871 deletions

View 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);