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

@ -31,6 +31,7 @@ public sealed class LiveEntityPresentationController : IDisposable
private readonly Dictionary<uint, ushort> _readyGenerationByGuid = new();
private readonly Dictionary<uint, ushort> _suspendedShadowGenerationByGuid = new();
private readonly Dictionary<uint, ushort> _activePlacementGenerationByGuid = new();
private readonly HashSet<LiveEntityRecord> _drainingRecords = new();
private bool _disposed;
public LiveEntityPresentationController(
@ -69,8 +70,20 @@ public sealed class LiveEntityPresentationController : IDisposable
}
_readyGenerationByGuid[serverGuid] = record.Generation;
ApplyPendingTransitions(record);
return true;
if (!ApplyPendingTransitions(record)
|| record.WorldEntity is not { } entity
|| !IsCurrent(record, entity))
{
return false;
}
// An object can materialize into a pending landblock before collision
// registration and before this ready barrier opens. Its initial false
// visibility edge therefore had nothing to suspend. Reconcile here,
// after every create-time owner exists, so the retained registration
// cannot remain active offscreen and hydration has a restore marker.
SuspendOrdinaryShadowOutsideProjection(record, entity);
return IsCurrent(record, entity);
}
/// <summary>Drains a newly accepted SetState when this incarnation is ready.</summary>
@ -83,8 +96,7 @@ public sealed class LiveEntityPresentationController : IDisposable
return false;
}
ApplyPendingTransitions(record);
return true;
return ApplyPendingTransitions(record);
}
/// <summary>
@ -188,6 +200,7 @@ public sealed class LiveEntityPresentationController : IDisposable
{
_activePlacementGenerationByGuid.Remove(record.ServerGuid);
}
_drainingRecords.Remove(record);
}
public void Clear()
@ -195,6 +208,7 @@ public sealed class LiveEntityPresentationController : IDisposable
_readyGenerationByGuid.Clear();
_suspendedShadowGenerationByGuid.Clear();
_activePlacementGenerationByGuid.Clear();
_drainingRecords.Clear();
}
public void Dispose()
@ -206,46 +220,83 @@ public sealed class LiveEntityPresentationController : IDisposable
Clear();
}
private void ApplyPendingTransitions(LiveEntityRecord record)
private bool ApplyPendingTransitions(LiveEntityRecord record)
{
while (record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition))
// State callbacks can synchronously accept another SetState. Retail's
// update thread completes the current transition before the next FIFO
// entry; a recursive drain would interleave Visible halfway through
// Hidden and then let the older side effects win last.
if (!_drainingRecords.Add(record))
return IsCurrent(record, record.WorldEntity);
try
{
if (record.WorldEntity is not { } entity)
return;
// set_state writes the final state before any PartArray/cell side
// effect. Retained shadow registrations must see those same bits
// even while their cell rows are suspended.
_shadows.UpdatePhysicsState(entity.Id, (uint)transition.FinalState);
switch (transition.HiddenTransition)
while (record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition))
{
case RetailHiddenTransition.BecameHidden:
_playTyped(entity.Id, HiddenScriptType, 1f);
_setDirectChildrenNoDraw(record.ServerGuid, true);
_shadows.Suspend(entity.Id);
if (!IsPlacementActive(record))
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
// Retail CPhysicsObj::set_hidden @ 0x00514C60 calls
// CPartArray::HandleEnterWorld after hiding the object
// from its cell. Despite the name, this is the motion
// timeline boundary: it strips link animations and
// aborts every pending completion through
// MotionTableManager::HandleEnterWorld @ 0x0051BDD0.
_handlePartArrayEnterWorld(entity.Id);
_clearInvalidTarget(record.ServerGuid);
break;
if (record.WorldEntity is not { } entity
|| !IsCurrent(record, entity))
{
return false;
}
case RetailHiddenTransition.BecameVisible:
_playTyped(entity.Id, UnHideScriptType, 1f);
_setDirectChildrenNoDraw(record.ServerGuid, false);
// Retail invokes the same PartArray boundary before
// CObjCell::unhide_object restores cell visibility.
_handlePartArrayEnterWorld(entity.Id);
if (!IsPlacementActive(record) && RestoreShadow(record, entity))
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
break;
// set_state writes the final state before any PartArray/cell side
// effect. Retained shadow registrations must see those same bits
// even while their cell rows are suspended.
_shadows.UpdatePhysicsState(entity.Id, (uint)transition.FinalState);
switch (transition.HiddenTransition)
{
case RetailHiddenTransition.BecameHidden:
_playTyped(entity.Id, HiddenScriptType, 1f);
if (!IsCurrent(record, entity))
return false;
_setDirectChildrenNoDraw(record.ServerGuid, true);
if (!IsCurrent(record, entity))
return false;
_shadows.Suspend(entity.Id);
if (!IsPlacementActive(record))
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
// Retail CPhysicsObj::set_hidden @ 0x00514C60 calls
// CPartArray::HandleEnterWorld after hiding the object
// from its cell. Despite the name, this is the motion
// timeline boundary: it strips link animations and
// aborts every pending completion through
// MotionTableManager::HandleEnterWorld @ 0x0051BDD0.
_handlePartArrayEnterWorld(entity.Id);
if (!IsCurrent(record, entity))
return false;
_clearInvalidTarget(record.ServerGuid);
if (!IsCurrent(record, entity))
return false;
break;
case RetailHiddenTransition.BecameVisible:
_playTyped(entity.Id, UnHideScriptType, 1f);
if (!IsCurrent(record, entity))
return false;
_setDirectChildrenNoDraw(record.ServerGuid, false);
if (!IsCurrent(record, entity))
return false;
// Retail invokes the same PartArray boundary before
// CObjCell::unhide_object restores cell visibility.
_handlePartArrayEnterWorld(entity.Id);
if (!IsCurrent(record, entity))
return false;
bool restored = !IsPlacementActive(record)
&& RestoreShadow(record, entity);
if (!IsCurrent(record, entity))
return false;
if (restored)
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
break;
}
}
return IsCurrent(record, record.WorldEntity);
}
finally
{
_drainingRecords.Remove(record);
}
}
@ -273,9 +324,25 @@ public sealed class LiveEntityPresentationController : IDisposable
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
{
if (!visible
|| record.WorldEntity is not { } entity
|| (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
if (record.WorldEntity is not { } entity
|| !IsCurrent(record, entity))
{
return;
}
// exit_world removes an ordinary object's collision rows on the same
// projection edge that suspends its object clock. Keep the retained
// registration so a stationary object can be restored immediately on
// hydration; it may have no later movement quantum to repair itself.
// Projectiles and active authoritative placements own their matching
// suspend/restore transaction in their dedicated controllers.
if (!visible)
{
SuspendOrdinaryShadowOutsideProjection(record, entity);
return;
}
if ((record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
|| IsPlacementActive(record)
|| !_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort readyGeneration)
|| readyGeneration != record.Generation
@ -287,10 +354,40 @@ public sealed class LiveEntityPresentationController : IDisposable
return;
}
if (RestoreShadow(record, entity))
bool restored = RestoreShadow(record, entity);
if (!IsCurrent(record, entity))
return;
if (restored)
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
}
private void SuspendOrdinaryShadowOutsideProjection(
LiveEntityRecord record,
AcDream.Core.World.WorldEntity entity)
{
if (record.IsSpatiallyVisible
|| record.ProjectileRuntime is not null
|| IsPlacementActive(record)
|| !_readyGenerationByGuid.TryGetValue(
record.ServerGuid,
out ushort readyGeneration)
|| readyGeneration != record.Generation
|| !_shadows.Suspend(entity.Id))
{
return;
}
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
}
private bool IsCurrent(
LiveEntityRecord record,
AcDream.Core.World.WorldEntity? entity) =>
entity is not null
&& _liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
&& ReferenceEquals(current, record)
&& ReferenceEquals(current.WorldEntity, entity);
private bool IsPlacementActive(LiveEntityRecord record) =>
_activePlacementGenerationByGuid.TryGetValue(
record.ServerGuid,