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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue