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
|
|
@ -0,0 +1,432 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Ports retail's separate <c>CPhysics::static_animating_objects</c> workset
|
||||
/// (<c>CPhysics::UseTime</c> 0x00509950 and
|
||||
/// <c>CPhysicsObj::animate_static_object</c> 0x00513DF0). Membership is
|
||||
/// Setup/state-driven: any physics-Static object with Setup.DefaultAnimation
|
||||
/// enters, whether DAT-hydrated or server-created, and leaves with its logical
|
||||
/// 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
|
||||
{
|
||||
private const double FrameEpsilon = 0.000199999995;
|
||||
private const float MaximumElapsed = 2f;
|
||||
|
||||
private sealed class Owner
|
||||
{
|
||||
public required WorldEntity Entity;
|
||||
public required Setup Setup;
|
||||
public required AnimationSequencer? Sequencer;
|
||||
public required uint[] PartGfxIds;
|
||||
public required IReadOnlyDictionary<uint, uint>?[] SurfaceOverrides;
|
||||
public required bool[] PartAvailable;
|
||||
public PhysicsBody? Body;
|
||||
public AnimationSequencer? PendingProcessHooks;
|
||||
public ulong PendingResidencyVersion;
|
||||
public IReadOnlyList<PartTransform>? PreparedLivePartFrames;
|
||||
public double ElapsedSinceUpdate;
|
||||
public readonly Frame RootFrameScratch = new();
|
||||
public readonly List<MeshRef> MeshRefs = new();
|
||||
public readonly List<Matrix4x4> PartPoses = new();
|
||||
}
|
||||
|
||||
private readonly IAnimationLoader _animationLoader;
|
||||
private readonly Action<uint, AnimationSequencer> _captureHooks;
|
||||
private readonly Action<WorldEntity, IReadOnlyList<Matrix4x4>, IReadOnlyList<bool>>
|
||||
_publishPartPoses;
|
||||
private readonly Func<WorldEntity, bool> _isResident;
|
||||
private readonly Action<WorldEntity, PhysicsBody> _commitLiveRoot;
|
||||
private readonly Func<WorldEntity, ulong> _residencyVersion;
|
||||
private readonly Dictionary<uint, Owner> _owners = new();
|
||||
private readonly List<Owner> _snapshot = new();
|
||||
private readonly List<Owner> _hookSnapshot = new();
|
||||
|
||||
public RetailStaticAnimatingObjectScheduler(
|
||||
IAnimationLoader animationLoader,
|
||||
Action<uint, AnimationSequencer> captureHooks,
|
||||
Action<WorldEntity, IReadOnlyList<Matrix4x4>, IReadOnlyList<bool>> publishPartPoses,
|
||||
Func<WorldEntity, bool>? isResident = null,
|
||||
Action<WorldEntity, PhysicsBody>? commitLiveRoot = null,
|
||||
Func<WorldEntity, ulong>? residencyVersion = null)
|
||||
{
|
||||
_animationLoader = animationLoader
|
||||
?? throw new ArgumentNullException(nameof(animationLoader));
|
||||
_captureHooks = captureHooks
|
||||
?? throw new ArgumentNullException(nameof(captureHooks));
|
||||
_publishPartPoses = publishPartPoses
|
||||
?? throw new ArgumentNullException(nameof(publishPartPoses));
|
||||
_isResident = isResident ?? (_ => true);
|
||||
_commitLiveRoot = commitLiveRoot ?? ((_, _) => { });
|
||||
_residencyVersion = residencyVersion ?? (_ => 0UL);
|
||||
}
|
||||
|
||||
internal int Count => _owners.Count;
|
||||
|
||||
public void Register(WorldEntity entity, ScriptActivationInfo info)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
if (!info.UsesStaticAnimationWorkset
|
||||
|| info.Setup is not { } setup
|
||||
|| info.DefaultAnimationId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_owners.TryGetValue(entity.Id, out Owner? retained))
|
||||
{
|
||||
if (ReferenceEquals(retained.Entity, entity))
|
||||
return;
|
||||
throw new InvalidOperationException(
|
||||
$"DAT-static animation owner 0x{entity.Id:X8} is already registered.");
|
||||
}
|
||||
|
||||
// A live CPhysicsObj already owns its canonical PartArray through
|
||||
// AnimatedEntity. Resource registration occurs before that App owner
|
||||
// is constructed, so retain a pending workset member and bind the
|
||||
// exact sequencer later. DAT-only statics have no live owner and may
|
||||
// construct their PartArray here.
|
||||
AnimationSequencer? sequencer = null;
|
||||
if (entity.ServerGuid == 0)
|
||||
{
|
||||
sequencer = new AnimationSequencer(
|
||||
setup,
|
||||
new MotionTable(),
|
||||
_animationLoader);
|
||||
if (!sequencer.HasCurrentNode)
|
||||
return;
|
||||
}
|
||||
|
||||
int partCount = setup.Parts.Count;
|
||||
uint[] gfxIds = BuildPartGfxIds(setup, entity);
|
||||
bool[] available = new bool[partCount];
|
||||
if (info.PartAvailability is { } supplied)
|
||||
{
|
||||
for (int i = 0; i < partCount && i < supplied.Count; i++)
|
||||
available[i] = supplied[i];
|
||||
}
|
||||
var surfaces = MatchSurfaceOverrides(entity, gfxIds, available);
|
||||
_owners.Add(entity.Id, new Owner
|
||||
{
|
||||
Entity = entity,
|
||||
Setup = setup,
|
||||
Sequencer = sequencer,
|
||||
PartGfxIds = gfxIds,
|
||||
SurfaceOverrides = surfaces,
|
||||
PartAvailable = available,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes a live Physics-Static registration with the same sequencer
|
||||
/// and body owned by the canonical live entity.
|
||||
/// Retail's static workset stores a <c>CPhysicsObj*</c>; it never clones a
|
||||
/// second PartArray for this alternate scheduling path.
|
||||
/// </summary>
|
||||
public bool BindLiveOwner(
|
||||
WorldEntity entity,
|
||||
AnimationSequencer sequencer,
|
||||
PhysicsBody body)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(sequencer);
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
if (!_owners.TryGetValue(entity.Id, out Owner? owner))
|
||||
return false;
|
||||
if (!ReferenceEquals(owner.Entity, entity) || entity.ServerGuid == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Static animation owner 0x{entity.Id:X8} does not match this live incarnation.");
|
||||
}
|
||||
if (!sequencer.HasCurrentNode)
|
||||
return false;
|
||||
|
||||
owner.Sequencer = sequencer;
|
||||
owner.Body = body;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfers the current live PartArray result to GameWindow's canonical
|
||||
/// appearance composer. The scheduler owns timing; AnimatedEntity remains
|
||||
/// the single owner of mesh identity, overrides, and appearance rebinding.
|
||||
/// </summary>
|
||||
public bool TryTakeLivePartFrames(
|
||||
uint ownerId,
|
||||
out IReadOnlyList<PartTransform> frames)
|
||||
{
|
||||
if (_owners.TryGetValue(ownerId, out Owner? owner)
|
||||
&& owner.Entity.ServerGuid != 0
|
||||
&& 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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
foreach ((uint ownerId, Owner owner) in _owners)
|
||||
{
|
||||
if (owner.Sequencer is not null)
|
||||
destination.Add(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(float elapsedSeconds)
|
||||
{
|
||||
if (!float.IsFinite(elapsedSeconds)
|
||||
|| elapsedSeconds <= 0f
|
||||
|| _owners.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_snapshot.Clear();
|
||||
_snapshot.AddRange(_owners.Values);
|
||||
foreach (Owner owner in _snapshot)
|
||||
{
|
||||
if (!_owners.TryGetValue(owner.Entity.Id, out Owner? current)
|
||||
|| !ReferenceEquals(current, owner)
|
||||
|| owner.Sequencer is not { } sequencer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
owner.ElapsedSinceUpdate += elapsedSeconds;
|
||||
if (!_isResident(owner.Entity))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
// animate_static_object returns before touching update_time
|
||||
// while cell == null. A short leave-world interval therefore
|
||||
// catches up on re-entry; a long one is discarded by the
|
||||
// retail two-second guard below.
|
||||
continue;
|
||||
}
|
||||
ulong residencyVersion = _residencyVersion(owner.Entity);
|
||||
|
||||
double ownerElapsed = owner.ElapsedSinceUpdate;
|
||||
owner.ElapsedSinceUpdate = 0d;
|
||||
if (ownerElapsed <= FrameEpsilon
|
||||
|| ownerElapsed > MaximumElapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<PartTransform> frames = sequencer.Advance(
|
||||
(float)ownerElapsed);
|
||||
|
||||
if (owner.Body is { } body)
|
||||
{
|
||||
// CPhysicsObj::animate_static_object 0x00513DF0 passes the
|
||||
// stored omega vector directly to Frame::grotate after the
|
||||
// PartArray update. Unlike UpdatePhysicsInternal, this odd
|
||||
// static branch does not multiply omega by elapsed time.
|
||||
Quaternion previousOrientation = body.Orientation;
|
||||
owner.RootFrameScratch.Origin = body.Position;
|
||||
owner.RootFrameScratch.Orientation = previousOrientation;
|
||||
FrameOps.GRotate(owner.RootFrameScratch, body.Omega);
|
||||
body.SetFrameInCurrentCell(
|
||||
body.Position,
|
||||
owner.RootFrameScratch.Orientation);
|
||||
owner.Entity.Rotation = body.Orientation;
|
||||
// animate_static_object rotates the root before
|
||||
// UpdatePartsInternal/UpdateChildrenInternal. Commit every
|
||||
// root consumer (collision shadows and effect anchors) at
|
||||
// that same boundary; a rendered-only rotation leaves the
|
||||
// canonical CPhysicsObj split across two orientations.
|
||||
// The common case is a zero omega. Do not turn that no-op
|
||||
// into a per-frame ShadowObjectRegistry reflood; only a real
|
||||
// root change has canonical consumers to commit.
|
||||
if (body.Orientation != previousOrientation)
|
||||
{
|
||||
_commitLiveRoot(owner.Entity, body);
|
||||
if (!_owners.TryGetValue(owner.Entity.Id, out current)
|
||||
|| !ReferenceEquals(current, owner)
|
||||
|| !IsResidentAtVersion(owner, residencyVersion))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsResidentAtVersion(owner, residencyVersion))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (owner.Entity.ServerGuid != 0)
|
||||
{
|
||||
owner.PreparedLivePartFrames = frames;
|
||||
}
|
||||
else
|
||||
{
|
||||
Compose(owner, frames);
|
||||
_publishPartPoses(owner.Entity, owner.PartPoses, owner.PartAvailable);
|
||||
if (!_owners.TryGetValue(owner.Entity.Id, out current)
|
||||
|| !ReferenceEquals(current, owner)
|
||||
|| !IsResidentAtVersion(owner, residencyVersion))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// animate_static_object runs process_hooks only after the root,
|
||||
// parts, and attached children have their final transforms. Live
|
||||
// part/child publication occurs outside this owner, so retain the
|
||||
// exact PartArray and let GameWindow call ProcessHooks at that
|
||||
// later boundary instead of completing AnimationDone here.
|
||||
owner.PendingProcessHooks = sequencer;
|
||||
owner.PendingResidencyVersion = residencyVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the static workset's retail <c>process_hooks</c> tail after final
|
||||
/// root, part, and child pose publication. Presentation hooks remain in
|
||||
/// the shared frame queue until its normal drain.
|
||||
/// </summary>
|
||||
public void ProcessHooks()
|
||||
{
|
||||
_hookSnapshot.Clear();
|
||||
foreach (Owner owner in _owners.Values)
|
||||
{
|
||||
if (owner.PendingProcessHooks is not null)
|
||||
_hookSnapshot.Add(owner);
|
||||
}
|
||||
|
||||
foreach (Owner owner in _hookSnapshot)
|
||||
{
|
||||
if (!_owners.TryGetValue(owner.Entity.Id, out Owner? current)
|
||||
|| !ReferenceEquals(current, owner)
|
||||
|| owner.PendingProcessHooks is not { } sequencer
|
||||
|| !ReferenceEquals(owner.Sequencer, sequencer)
|
||||
|| !IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clear before the callback: hook delivery may unregister or
|
||||
// replace the owner, and a nested caller must not replay this tail.
|
||||
owner.PendingProcessHooks = null;
|
||||
_captureHooks(owner.Entity.Id, sequencer);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsResidentAtVersion(Owner owner, ulong version) =>
|
||||
_isResident(owner.Entity)
|
||||
&& _residencyVersion(owner.Entity) == version;
|
||||
|
||||
private static void InvalidatePending(Owner owner)
|
||||
{
|
||||
owner.PreparedLivePartFrames = null;
|
||||
owner.PendingProcessHooks = null;
|
||||
owner.PendingResidencyVersion = 0UL;
|
||||
}
|
||||
|
||||
private static void Compose(Owner owner, IReadOnlyList<PartTransform> frames)
|
||||
{
|
||||
owner.MeshRefs.Clear();
|
||||
owner.PartPoses.Clear();
|
||||
Matrix4x4 objectScale = owner.Entity.Scale == 1f
|
||||
? Matrix4x4.Identity
|
||||
: Matrix4x4.CreateScale(owner.Entity.Scale);
|
||||
|
||||
int partCount = owner.Setup.Parts.Count;
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
Vector3 origin = i < frames.Count ? frames[i].Origin : Vector3.Zero;
|
||||
Quaternion orientation = i < frames.Count
|
||||
? frames[i].Orientation
|
||||
: Quaternion.Identity;
|
||||
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
|
||||
? owner.Setup.DefaultScale[i]
|
||||
: Vector3.One;
|
||||
|
||||
Matrix4x4 visual =
|
||||
Matrix4x4.CreateScale(defaultScale)
|
||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(origin);
|
||||
if (owner.Entity.Scale != 1f)
|
||||
visual *= objectScale;
|
||||
|
||||
owner.PartPoses.Add(
|
||||
Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(origin * owner.Entity.Scale));
|
||||
if (!owner.PartAvailable[i])
|
||||
continue;
|
||||
|
||||
owner.MeshRefs.Add(new MeshRef(owner.PartGfxIds[i], visual)
|
||||
{
|
||||
SurfaceOverrides = owner.SurfaceOverrides[i],
|
||||
});
|
||||
}
|
||||
|
||||
owner.Entity.MeshRefs = owner.MeshRefs;
|
||||
owner.Entity.SetIndexedPartPoses(owner.PartPoses, owner.PartAvailable);
|
||||
}
|
||||
|
||||
private static uint[] BuildPartGfxIds(Setup setup, WorldEntity entity)
|
||||
{
|
||||
var result = new uint[setup.Parts.Count];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
result[i] = (uint)setup.Parts[i];
|
||||
foreach (PartOverride replacement in entity.PartOverrides)
|
||||
{
|
||||
if (replacement.PartIndex < result.Length)
|
||||
result[replacement.PartIndex] = replacement.GfxObjId;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<uint, uint>?[] MatchSurfaceOverrides(
|
||||
WorldEntity entity,
|
||||
uint[] gfxIds,
|
||||
bool[] available)
|
||||
{
|
||||
var result = new IReadOnlyDictionary<uint, uint>?[gfxIds.Length];
|
||||
var consumed = new bool[entity.MeshRefs.Count];
|
||||
for (int partIndex = 0; partIndex < gfxIds.Length; partIndex++)
|
||||
{
|
||||
for (int meshIndex = 0; meshIndex < entity.MeshRefs.Count; meshIndex++)
|
||||
{
|
||||
if (consumed[meshIndex]
|
||||
|| entity.MeshRefs[meshIndex].GfxObjId != gfxIds[partIndex])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
consumed[meshIndex] = true;
|
||||
available[partIndex] = true;
|
||||
result[partIndex] = entity.MeshRefs[meshIndex].SurfaceOverrides;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue