acdream/src/AcDream.App/World/LiveEntityRuntime.cs
Erik 91e82c3c68 fix(streaming): prepare quiesced destination entities
Separate loaded spatial residency from the world presentation gate so destination live objects acquire mesh owners behind portal space while drawing and simulation stay quiesced. Prevent unowned CPU-cache hits from recreating stale GPU staging work.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
2026-07-24 20:21:57 +02:00

2853 lines
113 KiB
C#

using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using System.Numerics;
using System.Runtime.ExceptionServices;
namespace AcDream.App.World;
/// <summary>
/// Animation state owned by a live object. The concrete App animation runtime
/// implements this seam so identity storage does not depend on a render backend.
/// </summary>
public interface ILiveEntityAnimationRuntime
{
WorldEntity Entity { get; }
uint CurrentMotion { get; }
}
/// <summary>Remote motion state owned by a live object.</summary>
public interface ILiveEntityRemoteMotionRuntime
{
PhysicsBody Body { get; }
}
/// <summary>
/// Optional seam for a remote-motion component that reads the exact
/// incarnation's canonical movement-manager host from
/// <see cref="LiveEntityRecord"/>.
/// </summary>
public interface ILiveEntityPhysicsHostConsumer
{
void BindPhysicsHost(Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> read);
}
/// <summary>
/// Atomic composition seam for a component that consumes both canonical host
/// and cell identity. Implementations validate every delegate before
/// publishing any of them, so a failed bind leaves the component reusable.
/// </summary>
public interface ILiveEntityCanonicalRuntimeConsumer
{
void BindCanonicalRuntime(
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost,
Func<uint> readCell,
Action<uint> writeCell);
}
/// <summary>
/// Optional seam for a remote-motion component that consumes the canonical
/// full-cell identity owned by <see cref="LiveEntityRecord"/>. The runtime
/// binds this for every production remote, regardless of whether projectile
/// classification arrives before or after the MovementManager.
/// </summary>
public interface ILiveEntityCanonicalCellConsumer
{
void BindCanonicalCell(Func<uint> read, Action<uint> write);
}
/// <summary>
/// Remote CPhysicsObj state required to finish a retail MoveOrTeleport
/// placement. A same-incarnation runtime replacement may wrap the same body,
/// but it must preserve this complete placement contract.
/// </summary>
public interface ILiveEntityRemotePlacementRuntime :
ILiveEntityRemoteMotionRuntime,
ILiveEntityCanonicalCellConsumer
{
uint CellId { get; set; }
bool Airborne { get; set; }
Vector3 LastServerPosition { get; set; }
double LastServerPositionTime { get; set; }
Vector3 LastShadowSyncPosition { get; set; }
Quaternion LastShadowSyncOrientation { get; set; }
void HitGround();
void LeaveGround();
}
/// <summary>Projectile physics state owned by a live object.</summary>
public interface ILiveEntityProjectileRuntime
{
PhysicsBody Body { get; }
}
/// <summary>Marker for the DAT-driven effect profile added by the effect phase.</summary>
public interface ILiveEntityEffectProfile { }
public enum LiveEntityProjectionKind
{
World,
Attached,
}
/// <summary>
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
/// Spatial bucketing is deliberately absent: registering or removing meshes,
/// scripts, and effects is a logical-lifetime operation, not a landblock move.
/// </summary>
public interface ILiveEntityResourceLifecycle
{
void Register(WorldEntity entity);
void Unregister(WorldEntity entity);
}
/// <summary>
/// Exact-incarnation App component cleanup invoked by the canonical runtime
/// after it has retired the active identity. Mesh/script resource lifetime is
/// owned separately by <see cref="ILiveEntityResourceLifecycle"/>.
/// </summary>
internal interface ILiveEntityRuntimeComponentLifecycle
{
void TearDown(LiveEntityRecord record);
}
internal sealed class NullLiveEntityRuntimeComponentLifecycle :
ILiveEntityRuntimeComponentLifecycle
{
public static NullLiveEntityRuntimeComponentLifecycle Instance { get; } = new();
private NullLiveEntityRuntimeComponentLifecycle() { }
public void TearDown(LiveEntityRecord record) { }
}
internal sealed class DelegateLiveEntityRuntimeComponentLifecycle(
Action<LiveEntityRecord> tearDown) : ILiveEntityRuntimeComponentLifecycle
{
private readonly Action<LiveEntityRecord> _tearDown =
tearDown ?? throw new ArgumentNullException(nameof(tearDown));
public void TearDown(LiveEntityRecord record) => _tearDown(record);
}
/// <summary>
/// Composition bridge for the runtime-to-hydration construction cycle. It is
/// deliberately fail-fast and single-assignment: a live session must not start
/// before the exact component owner is bound, and ownership cannot be replaced
/// during a session.
/// </summary>
internal sealed class DeferredLiveEntityRuntimeComponentLifecycle :
ILiveEntityRuntimeComponentLifecycle
{
private ILiveEntityRuntimeComponentLifecycle? _inner;
public bool IsBound => Volatile.Read(ref _inner) is not null;
public void Bind(ILiveEntityRuntimeComponentLifecycle lifecycle)
{
ArgumentNullException.ThrowIfNull(lifecycle);
if (ReferenceEquals(lifecycle, this))
throw new ArgumentException("A lifecycle bridge cannot bind to itself.", nameof(lifecycle));
if (Interlocked.CompareExchange(ref _inner, lifecycle, null) is not null)
throw new InvalidOperationException("The live-entity runtime component lifecycle is already bound.");
}
public IDisposable BindOwned(ILiveEntityRuntimeComponentLifecycle lifecycle)
{
Bind(lifecycle);
return new Binding(this, lifecycle);
}
private void Unbind(ILiveEntityRuntimeComponentLifecycle expected)
{
_ = Interlocked.CompareExchange(ref _inner, null, expected);
}
public void TearDown(LiveEntityRecord record)
{
ILiveEntityRuntimeComponentLifecycle lifecycle = Volatile.Read(ref _inner)
?? throw new InvalidOperationException(
"The live-entity runtime component lifecycle has not been bound.");
lifecycle.TearDown(record);
}
private sealed class Binding : IDisposable
{
private DeferredLiveEntityRuntimeComponentLifecycle? _owner;
private readonly ILiveEntityRuntimeComponentLifecycle _expected;
public Binding(
DeferredLiveEntityRuntimeComponentLifecycle owner,
ILiveEntityRuntimeComponentLifecycle expected)
{
_owner = owner;
_expected = expected;
}
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
}
}
/// <summary>Delegate adapter used by the App composition root.</summary>
public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle
{
private readonly Action<WorldEntity> _register;
private readonly Action<WorldEntity> _unregister;
public DelegateLiveEntityResourceLifecycle(
Action<WorldEntity> register,
Action<WorldEntity> unregister)
{
_register = register ?? throw new ArgumentNullException(nameof(register));
_unregister = unregister ?? throw new ArgumentNullException(nameof(unregister));
}
public void Register(WorldEntity entity) => _register(entity);
public void Unregister(WorldEntity entity) => _unregister(entity);
}
/// <summary>
/// The one logical record for a server object incarnation. The record survives
/// loaded/pending landblock movement and temporary loss of its render bucket.
/// Only an accepted DeleteObject, session reset, newer INSTANCE_TS, or the
/// retail 25-second leave-visibility lifecycle ends it.
/// </summary>
public sealed class LiveEntityRecord
{
private readonly Queue<RetailPhysicsStateTransition> _pendingStateTransitions = new();
internal LiveEntityRecord(WorldSession.EntitySpawn snapshot)
{
ServerGuid = snapshot.Guid;
Snapshot = snapshot;
RefreshDerivedState();
PositionAuthorityVersion = snapshot.Position is null ? 0UL : 1UL;
VectorAuthorityVersion = snapshot.Physics is null ? 0UL : 1UL;
VelocityAuthorityVersion = snapshot.Position is null
&& snapshot.Physics is null
? 0UL
: 1UL;
MovementAuthorityVersion = 1UL;
ObjDescAuthorityVersion = 1UL;
FinalPhysicsState = RetailPhysicsStateTransitions.ConstructorState;
ApplyRawPhysicsState(RawPhysicsState);
}
public uint ServerGuid { get; }
public ushort Generation => Snapshot.InstanceSequence;
public WorldSession.EntitySpawn Snapshot { get; internal set; }
public WorldEntity? WorldEntity { get; internal set; }
public uint? LocalEntityId => WorldEntity?.Id;
public uint FullCellId { get; internal set; }
public uint CanonicalLandblockId { get; internal set; }
public uint RawPhysicsState { get; internal set; }
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
/// <summary>
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner. It
/// survives animation and MovementManager attachment/replacement, spatial
/// rebucketing, and appearance rehydration, and dies with this record.
/// </summary>
public RetailObjectQuantumClock ObjectClock { get; } = new();
/// <summary>
/// Changes whenever this record leaves or re-enters the ordinary-object
/// workset. Unlike <see cref="ProjectionMutationVersion"/>, loaded-to-loaded
/// rebucketing does not change this token. A scheduler can therefore stop a
/// catch-up batch that crossed a leave/enter boundary without rejecting an
/// ordinary same-frame cell move.
/// </summary>
internal ulong ObjectClockEpoch { get; private set; }
/// <summary>
/// True after this incarnation has successfully constructed its retail
/// <c>CPartArray</c>. This is an object-lifetime fact, not an animation-
/// runtime proxy: static poses and objects without a MotionTable still
/// own a PartArray and participate in the 96-unit Active gate.
/// </summary>
public bool HasPartArray { get; internal set; }
public PhysicsBody? PhysicsBody { get; internal set; }
internal bool PhysicsBodyAcquisitionInProgress { get; set; }
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal set; }
internal bool RemoteMotionBindingInProgress { get; set; }
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; }
internal bool RequiresRemotePlacementRuntime { get; set; }
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
public bool ResourcesRegistered { get; internal set; }
public bool IsSpatiallyProjected { get; internal set; }
public bool IsSpatiallyVisible { get; internal set; }
internal ulong ProjectionMutationVersion { get; set; }
/// <summary>
/// Monotonic App operation tokens for accepted wire authority. They let a
/// handler detect a newer same-incarnation packet delivered re-entrantly
/// while an older packet is crossing presentation or spatial callbacks.
/// </summary>
internal ulong PositionAuthorityVersion { get; private set; }
internal ulong StateAuthorityVersion { get; private set; }
internal ulong VectorAuthorityVersion { get; private set; }
internal ulong VelocityAuthorityVersion { get; private set; }
internal ulong MovementAuthorityVersion { get; private set; }
internal ulong ObjDescAuthorityVersion { get; private set; }
/// <summary>
/// Advances for every accepted CreateObject affecting this incarnation,
/// including a fresher same-INSTANCE_TS retransmit. App integration uses
/// it to distinguish two transactions that intentionally share identity.
/// </summary>
internal ulong CreateIntegrationVersion { get; private set; } = 1UL;
public bool WorldSpawnPublished { get; internal set; }
/// <summary>
/// True only after the incarnation's projection, collision/animation
/// components, effect owner, state presentation, and pending-effect replay
/// have crossed the initial ready barrier. A non-null
/// <see cref="WorldEntity"/> alone is not sufficient: failures after
/// resource registration retain the same projection for exact retry.
/// </summary>
public bool InitialHydrationCompleted { get; internal set; }
/// <summary>
/// A fresher same-incarnation CreateObject interrupted projection after
/// logical ownership had already been established. The retained entity
/// must replay the newest canonical appearance/current snapshot/animation
/// before ordinary rebucketing can resume. This obligation deliberately
/// survives a failed recovery attempt.
/// </summary>
internal bool CreateProjectionSynchronizationPending { get; set; }
internal bool ProjectionHydrationInProgress { get; set; }
internal ulong ProjectionHydrationCreateVersion { get; set; }
internal bool ProjectionHydrationRetryRequested { get; set; }
/// <summary>
/// A renderer mesh transaction can be re-entered by a newer accepted
/// ObjDesc. Keep that independent projection obligation on the exact
/// incarnation until the newest visual description publishes.
/// </summary>
internal bool AppearanceProjectionSynchronizationPending { get; set; }
internal bool AppearanceHydrationInProgress { get; set; }
internal ulong AppearanceHydrationVersion { get; set; }
internal bool AppearanceHydrationRetryRequested { get; set; }
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
internal bool RuntimeComponentsTeardownCompleted { get; set; }
internal LiveEntityTeardownPlan? RuntimeComponentTeardownPlan { get; set; }
internal bool SpatialProjectionTeardownCompleted { get; set; }
internal bool TeardownInProgress { get; set; }
internal bool DeleteAcceptedForTeardown { get; set; }
internal bool TryDequeueStateTransition(out RetailPhysicsStateTransition transition) =>
_pendingStateTransitions.TryDequeue(out transition);
internal void SuspendObjectClock()
{
ObjectClock.Deactivate();
ObjectClockEpoch++;
}
internal void ResetObjectClockForEnterWorld(bool isStatic)
{
ObjectClock.ResetForEnterWorld(isStatic);
ObjectClockEpoch++;
}
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState)
{
StateAuthorityVersion++;
RawPhysicsState = rawState;
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
FinalPhysicsState,
(PhysicsStateFlags)rawState);
FinalPhysicsState = transition.FinalState;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
_pendingStateTransitions.Enqueue(transition);
return transition;
}
internal void AdvancePositionAuthority()
{
PositionAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceVectorAuthority()
{
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceMovementAuthority()
{
MovementAuthorityVersion++;
VelocityAuthorityVersion++;
}
internal void AdvanceObjDescAuthority()
{
ObjDescAuthorityVersion++;
AppearanceProjectionSynchronizationPending = true;
if (AppearanceHydrationInProgress)
AppearanceHydrationRetryRequested = true;
}
internal void AdvanceCreateAuthority()
{
PositionAuthorityVersion++;
StateAuthorityVersion++;
VectorAuthorityVersion++;
VelocityAuthorityVersion++;
MovementAuthorityVersion++;
ObjDescAuthorityVersion++;
CreateIntegrationVersion++;
}
internal void SetChildNoDraw(bool noDraw)
{
FinalPhysicsState = noDraw
? FinalPhysicsState | PhysicsStateFlags.NoDraw
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
}
/// <summary>
/// Cell used when a streamed landblock asks live objects to restore their
/// spatial projection. Once materialized, the canonical runtime cell wins;
/// the retained server snapshot is only a first-materialization source.
/// </summary>
public uint ProjectionCellId => WorldEntity is not null
? FullCellId
: Snapshot.Position?.LandblockId ?? FullCellId;
internal void RefreshDerivedState(bool refreshPosition = true)
{
if (refreshPosition && Snapshot.Position is { } position)
{
FullCellId = position.LandblockId;
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
}
RawPhysicsState = Snapshot.Physics?.RawState
?? Snapshot.PhysicsState
?? 0u;
}
}
public readonly record struct LiveEntityRegistrationResult(
InboundCreateResult Inbound,
LiveEntityRecord? Record,
bool LogicalRegistrationCreated,
bool ReplacedExistingGeneration,
Exception? PriorGenerationCleanupFailure = null);
/// <summary>
/// Update-thread owner of live server-object identity, accepted state, runtime
/// components, and logical teardown. <see cref="GpuWorldState"/> is a spatial
/// projection only; moving between loaded and pending buckets never replays a
/// create-time renderer, script, animation, or effect action.
///
/// <para>
/// Retail keeps one object per accepted INSTANCE_TS: same-generation
/// CreateObject branches mutate it, Pickup leaves world without destruction,
/// and Delete/instance replacement end it. See SmartBox CreateObject/event
/// pseudocode in <c>2026-07-13-retail-projectile-vfx-pseudocode.md</c>,
/// <c>CPhysicsObj::set_description</c> (0x00514F40),
/// <c>CPhysicsObj::change_cell</c> (0x00513390), and
/// <c>CPhysicsObj::exit_world</c> (0x00514E60).
/// </para>
/// </summary>
public sealed class LiveEntityRuntime
{
public const uint FirstLiveEntityId = 1_000_000u;
public const uint LastLiveEntityId = 0x3FFF_FFFFu;
private readonly GpuWorldState _spatial;
private readonly ILiveEntityResourceLifecycle _resources;
private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle;
private readonly InboundPhysicsStateController _inbound = new();
private readonly Dictionary<uint, LiveEntityRecord> _recordsByGuid = new();
// Records leave the active gameplay map before arbitrary teardown callbacks
// so a newer GUID generation can be accepted re-entrantly. They remain
// canonical teardown tombstones until every resource owner confirms
// unregister; failures are therefore retryable by Delete or session Clear.
private readonly Dictionary<(uint Guid, ushort Generation), LiveEntityRecord>
_teardownRecords = new();
private readonly Dictionary<uint, WorldEntity> _materializedWorldEntitiesByGuid = new();
private readonly Dictionary<uint, WorldEntity> _visibleWorldEntitiesByGuid = new();
private readonly Dictionary<uint, uint> _guidByLocalId = new();
private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _animationsByLocalId = new();
private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _remoteMotionByGuid = new();
// Logical components survive retail's 25-second leave-visibility lifetime.
// Frame loops must not scan those retained owners. These component-specific
// indexes contain only records with a loaded, non-cellless spatial
// projection. Hidden and Frozen remain members: their state controls what
// each retail update path advances after the O(active) lookup.
private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _spatialAnimationsByLocalId = new();
private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _spatialRemoteMotionByGuid = new();
private readonly Dictionary<uint, ILiveEntityProjectileRuntime> _spatialProjectilesByGuid = new();
// Retail CPhysics::UseTime walks the ordinary object table, not a render-
// animation component table. This index is the loaded/cell-backed root
// workset; component-specific indexes remain lookup accelerators only.
private readonly Dictionary<uint, LiveEntityRecord> _spatialRootObjectsByGuid = new();
private bool _isClearing;
private bool _sessionClearPendingFinalization;
private bool _isRegisteringResources;
private int _logicalTeardownDepth;
private ulong _sessionLifetimeVersion;
private readonly Dictionary<uint, ulong> _lifetimeMutationVersionByGuid = new();
private uint _rebucketingGuid;
private uint _nextLocalEntityId;
public LiveEntityRuntime(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
uint firstLocalEntityId = FirstLiveEntityId)
: this(
spatial,
resources,
NullLiveEntityRuntimeComponentLifecycle.Instance,
firstLocalEntityId)
{
}
public LiveEntityRuntime(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
Action<LiveEntityRecord> tearDownRuntimeComponents,
uint firstLocalEntityId = FirstLiveEntityId)
: this(
spatial,
resources,
new DelegateLiveEntityRuntimeComponentLifecycle(tearDownRuntimeComponents),
firstLocalEntityId)
{
}
internal LiveEntityRuntime(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
ILiveEntityRuntimeComponentLifecycle? runtimeComponentLifecycle,
uint firstLocalEntityId = FirstLiveEntityId)
{
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
_runtimeComponentLifecycle = runtimeComponentLifecycle
?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle));
if (firstLocalEntityId is < FirstLiveEntityId or > LastLiveEntityId)
throw new ArgumentOutOfRangeException(
nameof(firstLocalEntityId),
$"Live entity ids must stay in 0x{FirstLiveEntityId:X8}..0x{LastLiveEntityId:X8}.");
_nextLocalEntityId = firstLocalEntityId;
_spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged;
}
public int Count => _recordsByGuid.Count;
public int PendingTeardownCount => _teardownRecords.Count;
public int MaterializedCount => _guidByLocalId.Count;
public IReadOnlyCollection<LiveEntityRecord> Records => _recordsByGuid.Values;
/// <summary>
/// Every materialized top-level world projection, including an object
/// parked in a pending (currently unloaded) spatial bucket. Network and
/// physics mutation must use this stable view so streaming visibility can
/// never masquerade as logical destruction.
/// </summary>
public IReadOnlyDictionary<uint, WorldEntity> MaterializedWorldEntities =>
_materializedWorldEntitiesByGuid;
/// <summary>
/// Currently visible top-level world projections. Attached children and
/// pending or Hidden projections are excluded from radar, picking, status,
/// and target candidate consumers. NoDraw alone suppresses the mesh but is
/// not a retail cell-visibility transition.
/// </summary>
public IReadOnlyDictionary<uint, WorldEntity> WorldEntities =>
_visibleWorldEntitiesByGuid;
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _inbound.Snapshots;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> AnimationRuntimes => _animationsByLocalId;
public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> RemoteMotionRuntimes => _remoteMotionByGuid;
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> SpatialAnimationRuntimes =>
_spatialAnimationsByLocalId;
public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> SpatialRemoteMotionRuntimes =>
_spatialRemoteMotionByGuid;
internal IReadOnlyDictionary<uint, ILiveEntityProjectileRuntime> SpatialProjectileRuntimes =>
_spatialProjectilesByGuid;
internal IReadOnlyDictionary<uint, LiveEntityRecord> SpatialRootObjects =>
_spatialRootObjectsByGuid;
public ParentAttachmentState ParentAttachments { get; } = new();
/// <summary>
/// Raised after a materialized projection enters or leaves the currently
/// loaded spatial view. Logical resources remain owned by the record.
/// </summary>
public event Action<LiveEntityRecord, bool>? ProjectionVisibilityChanged;
public LiveEntityRegistrationResult RegisterLiveEntity(WorldSession.EntitySpawn incoming)
{
if (_isClearing || _sessionClearPendingFinalization || _isRegisteringResources)
{
throw new InvalidOperationException(
_isClearing || _sessionClearPendingFinalization
? "A live entity cannot register while the session lifetime is clearing."
: "A live entity cannot register from inside atomic resource registration.");
}
InboundCreateResult result = _inbound.AcceptCreate(incoming);
if (result.Disposition is CreateObjectTimestampDisposition.StaleGeneration)
return new LiveEntityRegistrationResult(result, null, false, false);
ulong sessionVersion = _sessionLifetimeVersion;
ulong operationVersion = AdvanceLifetimeMutation(incoming.Guid);
if (result.Disposition is CreateObjectTimestampDisposition.ExistingGeneration)
{
if (_recordsByGuid.TryGetValue(incoming.Guid, out LiveEntityRecord? retained))
{
retained.Snapshot = result.Snapshot;
retained.RefreshDerivedState();
retained.AdvanceCreateAuthority();
RefreshSpatialRuntimeIndexes(retained);
return new LiveEntityRegistrationResult(result, retained, false, false);
}
// A failed materialization rollback retains this exact incarnation
// as a teardown tombstone. Do not create a second active record
// with the same (GUID, INSTANCE_TS) while its partial resources are
// still converging; a later retransmit can recover after teardown.
if (_teardownRecords.ContainsKey(
(incoming.Guid, result.Snapshot.InstanceSequence)))
{
return new LiveEntityRegistrationResult(
result,
null,
false,
false);
}
// Defensive repair for a caller that cleared only the logical map.
// Normal session teardown clears both owners together.
var recovered = new LiveEntityRecord(result.Snapshot);
_recordsByGuid.Add(incoming.Guid, recovered);
return new LiveEntityRegistrationResult(result, recovered, true, false);
}
bool replaced = _recordsByGuid.Remove(incoming.Guid, out LiveEntityRecord? old);
if (result.Disposition is CreateObjectTimestampDisposition.NewGeneration)
ParentAttachments.EndGeneration(incoming.Guid, result.Snapshot.InstanceSequence);
Exception? tearDownFailure = null;
if (old is not null)
{
RetainTeardownRecord(old);
_logicalTeardownDepth++;
try
{
try
{
TearDownRecord(old);
ReleaseTeardownRecord(old);
}
catch (Exception error)
{
tearDownFailure = error;
}
}
finally
{
_logicalTeardownDepth--;
}
}
// Resource callbacks are arbitrary App integration code. Any nested
// create, accepted delete, or session reset advances this epoch. The
// outer packet is then superseded even when the nested action left no
// record/snapshot (delete/reset); reinstalling it would resurrect an
// incarnation after an accepted terminal event.
if (_sessionLifetimeVersion != sessionVersion
|| CurrentLifetimeMutation(incoming.Guid) != operationVersion)
{
if (tearDownFailure is not null)
{
throw new AggregateException(
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its incoming replacement was superseded.",
tearDownFailure);
}
return new LiveEntityRegistrationResult(
SupersededCreateResult(),
_recordsByGuid.GetValueOrDefault(incoming.Guid),
false,
replaced,
tearDownFailure);
}
var record = new LiveEntityRecord(result.Snapshot);
_recordsByGuid.Add(incoming.Guid, record);
return new LiveEntityRegistrationResult(
result,
record,
true,
replaced,
tearDownFailure);
}
/// <summary>
/// Completes logical registration when a renderable projection can be
/// hydrated. The factory is called at most once for the incarnation.
/// </summary>
public WorldEntity? MaterializeLiveEntity(
uint serverGuid,
uint fullCellId,
Func<uint, WorldEntity> factory,
LiveEntityProjectionKind projectionKind = LiveEntityProjectionKind.World)
{
ArgumentNullException.ThrowIfNull(factory);
if (_isClearing
|| _sessionClearPendingFinalization
|| _logicalTeardownDepth != 0
|| _isRegisteringResources)
{
throw new InvalidOperationException(
"A live entity cannot materialize inside an active logical-lifetime transition.");
}
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
return null;
if (record.WorldEntity is null)
{
uint localId = ReserveLocalEntityId();
WorldEntity entity = factory(localId)
?? throw new InvalidOperationException("A live-entity projection factory returned null.");
if (entity.Id != localId)
throw new InvalidOperationException(
$"Live projection id 0x{entity.Id:X8} did not match reserved id 0x{localId:X8}.");
if (entity.ServerGuid != serverGuid)
throw new InvalidOperationException(
$"Live projection guid 0x{entity.ServerGuid:X8} did not match record 0x{serverGuid:X8}.");
_guidByLocalId.Add(localId, serverGuid);
record.WorldEntity = entity;
_isRegisteringResources = true;
try
{
// Registration may span several App owners. Until the whole
// edge returns, Unregister remains a required rollback
// obligation; a failed rollback retains this identity as a
// teardown tombstone instead of making partial owners
// unreachable.
record.ResourcesRegistered = true;
try
{
_resources.Register(entity);
}
catch (Exception registrationError)
{
Exception? rollbackError = null;
try
{
_resources.Unregister(entity);
record.ResourcesRegistered = false;
}
catch (Exception error)
{
rollbackError = error;
}
if (rollbackError is null)
{
_guidByLocalId.Remove(localId);
record.WorldEntity = null;
throw;
}
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? active)
&& ReferenceEquals(active, record))
{
_recordsByGuid.Remove(serverGuid);
}
RetainTeardownRecord(record);
throw new AggregateException(
"Live entity resource registration and rollback both failed; " +
"the partial owner was retained for teardown retry.",
registrationError,
rollbackError);
}
}
finally
{
_isRegisteringResources = false;
}
}
record.ProjectionKind = projectionKind;
if (projectionKind is LiveEntityProjectionKind.World)
{
// Attached projections inherit the complete ancestor visibility
// chain. Once the same logical entity becomes a top-level world
// projection it has no render ancestor, so retained inherited
// suppression must be cleared at this ownership transition.
record.WorldEntity!.IsAncestorDrawVisible = true;
}
RefreshPresentation(record);
WorldEntity materialized = record.WorldEntity!;
if (!RebucketLiveEntity(serverGuid, fullCellId)
|| !_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, record)
|| !ReferenceEquals(current.WorldEntity, materialized))
{
// Visibility observers may replace this GUID while rebucketing.
// A failed teardown can retain the displaced entity as a retry
// tombstone; it is not the result of this materialization.
return null;
}
return materialized;
}
/// <summary>Changes spatial buckets only. No logical resource callbacks run.</summary>
public bool RebucketLiveEntity(uint serverGuid, uint spatialCellOrLandblockId)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is not { } entity)
return false;
bool wasProjected = record.IsSpatiallyProjected;
bool wasVisible = record.IsSpatiallyVisible;
bool wasOrdinaryRoot = _spatialRootObjectsByGuid.TryGetValue(
serverGuid,
out LiveEntityRecord? indexedRoot)
&& ReferenceEquals(indexedRoot, record);
ulong projectionOperation = ++record.ProjectionMutationVersion;
// GpuWorldState reports an intermediate false/true pair while moving
// between two loaded buckets. Suppress those implementation details
// and publish only the final logical visibility edge.
record.IsSpatiallyProjected = true;
Exception? spatialNotificationFailure = null;
uint priorRebucketingGuid = _rebucketingGuid;
_rebucketingGuid = serverGuid;
try
{
try
{
_spatial.RebucketLiveEntity(entity, spatialCellOrLandblockId);
}
catch (AggregateException error)
{
// GpuWorldState has already committed the bucket move and
// drained every visibility observer before reporting their
// failures. Finish this runtime transaction before surfacing
// the notification error to the caller.
spatialNotificationFailure = error;
}
}
finally
{
_rebucketingGuid = priorRebucketingGuid;
}
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
{
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
bool visible = _spatial.IsLiveEntityProjectionResident(serverGuid);
record.IsSpatiallyVisible = visible;
if (record.ProjectionKind is LiveEntityProjectionKind.World)
_materializedWorldEntitiesByGuid[serverGuid] = entity;
else
_materializedWorldEntitiesByGuid.Remove(serverGuid);
RefreshPresentation(record);
if ((spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu)
record.FullCellId = spatialCellOrLandblockId;
record.CanonicalLandblockId = spatialCellOrLandblockId == 0
? 0u
: (spatialCellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World
&& record.IsSpatiallyProjected
&& record.IsSpatiallyVisible
&& record.FullCellId != 0;
// prepare_to_enter_world (0x00511FA0) writes update_time = cur_time.
// Only a root-workset membership edge rebases the clock. Ordinary
// loaded-to-loaded movement preserves it; parented/attached objects
// take retail update_object's parent early-out and remain suspended.
if (!wasProjected && !isOrdinaryRoot)
{
record.SuspendObjectClock();
SynchronizePhysicsBodyActiveState(record);
}
else if (wasOrdinaryRoot != isOrdinaryRoot)
{
if (isOrdinaryRoot)
{
// InitObjectBegin and every leave-visibility edge are inactive
// before retail prepare_to_enter_world. Preserve that
// prerequisite for a first materialization as well.
if (!wasProjected)
record.ObjectClock.Deactivate();
record.ResetObjectClockForEnterWorld(
(record.FinalPhysicsState & PhysicsStateFlags.Static) != 0);
SynchronizePhysicsBodyActiveState(record);
}
else
{
record.SuspendObjectClock();
SynchronizePhysicsBodyActiveState(record);
}
}
RefreshSpatialRuntimeIndexes(record);
Exception? runtimeNotificationFailure = null;
if (!wasProjected || wasVisible != visible)
{
try
{
PublishProjectionVisibilityChanged(record, visible);
}
catch (Exception error)
{
runtimeNotificationFailure = error;
}
}
// Final observers are intentionally re-entrant. A callback can delete
// and recreate this GUID or start a newer rebucket on the same record;
// the outer caller must not continue with captures from that displaced
// projection operation.
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
{
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure);
return false;
}
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure);
return true;
}
/// <summary>
/// Removes only the render-bucket reference. The logical record and every
/// create-time resource remain alive for later projection/rebucketing.
/// </summary>
public bool WithdrawLiveEntityProjection(uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is null)
return false;
bool wasOrdinaryRoot = _spatialRootObjectsByGuid.TryGetValue(
serverGuid,
out LiveEntityRecord? rootRecord)
&& ReferenceEquals(rootRecord, record);
ulong objectClockEpoch = record.ObjectClockEpoch;
ulong projectionOperation = ++record.ProjectionMutationVersion;
Exception? spatialNotificationFailure = null;
try
{
_spatial.RemoveLiveEntityProjection(serverGuid);
}
catch (AggregateException error)
{
// The projection is already absent and GpuWorldState has already
// drained its observer queue. Complete canonical withdrawal below
// before reporting the notification failure.
spatialNotificationFailure = error;
}
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
{
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
// Usually GpuWorldState delivers the false edge synchronously above.
// During a reentrant visibility callback it queues that edge until the
// outer notification completes, so publish the logical withdrawal now;
// OnSpatialVisibilityChanged rejects the later duplicate against this
// committed record state.
bool spatialEdgeWasDeferred = record.IsSpatiallyVisible;
_materializedWorldEntitiesByGuid.Remove(serverGuid);
_visibleWorldEntitiesByGuid.Remove(serverGuid);
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;
// GpuWorldState normally publishes the leave edge synchronously and
// OnSpatialVisibilityChanged suspends this exact clock there. During a
// reentrant notification the edge is deferred, so commit the missing
// suspension here. One root-workset leave is one epoch edge -- never
// advance it once in the observer and again in this owner.
if (wasOrdinaryRoot && record.ObjectClockEpoch == objectClockEpoch)
record.SuspendObjectClock();
SynchronizePhysicsBodyActiveState(record);
RefreshSpatialRuntimeIndexes(record);
Exception? runtimeNotificationFailure = null;
if (spatialEdgeWasDeferred)
{
try
{
PublishProjectionVisibilityChanged(record, false);
}
catch (Exception error)
{
runtimeNotificationFailure = error;
}
}
// The manually published deferred edge is an arbitrary callback
// boundary just like Rebucket's final visibility edge. It may
// reproject this record or replace the GUID; the displaced outer
// withdrawal must not report itself as the current operation.
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
{
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure);
return false;
}
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure);
return true;
}
/// <summary>
/// Exact-incarnation withdrawal used after App leave-world callbacks. A
/// callback may accept a newer INSTANCE_TS for the same GUID; that newer
/// projection must never be withdrawn by the displaced operation.
/// </summary>
public bool WithdrawLiveEntityProjection(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
return WithdrawLiveEntityProjection(
expectedRecord,
expectedRecord.PositionAuthorityVersion,
expectedRecord.ProjectionMutationVersion);
}
/// <summary>
/// Exact-operation withdrawal used across arbitrary App callbacks. Both
/// the accepted Position authority and projection mutation token are
/// pinned so a same-incarnation re-entry cannot be removed by an older
/// parent/pickup/leave-world operation.
/// </summary>
internal bool WithdrawLiveEntityProjection(
LiveEntityRecord expectedRecord,
ulong expectedPositionAuthorityVersion,
ulong expectedProjectionMutationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
return IsCurrentRecord(expectedRecord)
&& expectedRecord.PositionAuthorityVersion == expectedPositionAuthorityVersion
&& expectedRecord.ProjectionMutationVersion == expectedProjectionMutationVersion
&& WithdrawLiveEntityProjection(expectedRecord.ServerGuid);
}
/// <summary>
/// Withdraws a still-live projection whose authoritative rollback frame
/// is outside every world cell. Logical owners remain registered.
/// </summary>
internal bool WithdrawLiveEntityProjectionToCellless(uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is null)
{
return false;
}
ClearWorldCell(serverGuid);
record.WorldEntity.ParentCellId = 0u;
return WithdrawLiveEntityProjection(serverGuid);
}
public bool UnregisterLiveEntity(
DeleteObject.Parsed delete,
bool isLocalPlayer,
Action? beforeTeardown = null)
{
if (_isRegisteringResources)
{
throw new InvalidOperationException(
"A live entity cannot unregister from inside atomic resource registration.");
}
var teardownKey = (delete.Guid, delete.InstanceSequence);
_teardownRecords.TryGetValue(teardownKey, out LiveEntityRecord? record);
bool retryingAcceptedDelete = record is not null
&& (record.DeleteAcceptedForTeardown
|| _isClearing
|| _sessionClearPendingFinalization);
if (!retryingAcceptedDelete)
{
if (!_inbound.TryDelete(delete, isLocalPlayer))
return false;
AdvanceLifetimeMutation(delete.Guid);
ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence);
// End active gameplay identity before arbitrary callbacks so a
// newer generation can be accepted re-entrantly, but retain the
// exact incarnation as a teardown tombstone until unregister has
// succeeded. A throwing resource callback can then be retried by
// the same accepted Delete instead of orphaning its owner.
LiveEntityRecord? retainedRegistrationFailure = record;
_recordsByGuid.Remove(delete.Guid, out LiveEntityRecord? activeRecord);
if (activeRecord is not null)
{
record = activeRecord;
RetainTeardownRecord(record);
}
else
{
record = retainedRegistrationFailure;
}
if (record is not null)
record.DeleteAcceptedForTeardown = true;
}
List<Exception>? failures = null;
_logicalTeardownDepth++;
try
{
if (!retryingAcceptedDelete)
{
try
{
beforeTeardown?.Invoke();
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
if (record is not null)
{
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
}
finally
{
_logicalTeardownDepth--;
}
try
{
// Persistence is GUID-scoped. End it only when the accepted
// delete left no replacement incarnation or unfinished teardown
// behind.
if (!_recordsByGuid.ContainsKey(delete.Guid)
&& !HasPendingTeardown(delete.Guid))
{
_spatial.ForgetLiveEntity(delete.Guid);
}
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
if (failures is not null)
throw new AggregateException(
$"Live entity 0x{delete.Guid:X8} deletion cleanup failed.",
failures);
return true;
}
public bool TryGetRecord(uint serverGuid, out LiveEntityRecord record) =>
_recordsByGuid.TryGetValue(serverGuid, out record!);
public bool TryGetWorldEntity(uint serverGuid, out WorldEntity entity)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.WorldEntity is { } found)
{
entity = found;
return true;
}
entity = null!;
return false;
}
/// <summary>
/// Resolves a top-level object that currently participates in picking,
/// targeting, radar, and wire-driven MoveTo/Sticky establishment.
/// Pending, attached, and Hidden projections are intentionally excluded.
/// </summary>
public bool TryGetInteractionEligibleEntity(
uint serverGuid,
out WorldEntity entity) =>
_visibleWorldEntitiesByGuid.TryGetValue(serverGuid, out entity!);
public bool TryGetInteractionEligibleRecord(
uint serverGuid,
out LiveEntityRecord record)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? found)
&& found.WorldEntity is { } entity
&& _visibleWorldEntitiesByGuid.TryGetValue(serverGuid, out WorldEntity? visible)
&& ReferenceEquals(entity, visible))
{
record = found;
return true;
}
record = null!;
return false;
}
/// <summary>
/// Resolves a render-published interaction candidate only while the same
/// logical WorldEntity incarnation still owns the server GUID. A stale
/// frame must never retarget a replacement which reused that GUID.
/// </summary>
public bool TryGetInteractionEligibleRecord(
uint serverGuid,
uint localEntityId,
out LiveEntityRecord record)
{
if (serverGuid != 0u
&& localEntityId != 0u
&& TryGetInteractionEligibleRecord(serverGuid, out LiveEntityRecord found)
&& found.WorldEntity!.Id == localEntityId)
{
record = found;
return true;
}
record = null!;
return false;
}
public bool ContainsWorldEntity(uint serverGuid) =>
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.WorldEntity is not null;
public bool TryGetServerGuid(uint localEntityId, out uint serverGuid) =>
_guidByLocalId.TryGetValue(localEntityId, out serverGuid);
public bool TryGetLocalEntityId(uint serverGuid, out uint localEntityId)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.LocalEntityId is { } found)
{
localEntityId = found;
return true;
}
localEntityId = 0;
return false;
}
public void SetAnimationRuntime(uint serverGuid, ILiveEntityAnimationRuntime runtime)
{
ArgumentNullException.ThrowIfNull(runtime);
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is not { } entity)
throw new InvalidOperationException($"Cannot bind animation before live entity 0x{serverGuid:X8} is materialized.");
if (!ReferenceEquals(runtime.Entity, entity))
throw new InvalidOperationException("Animation runtime belongs to a different WorldEntity.");
if (record.AnimationRuntime is not null)
_animationsByLocalId.Remove(entity.Id);
record.AnimationRuntime = runtime;
_animationsByLocalId[entity.Id] = runtime;
RefreshSpatialRuntimeIndexes(record);
}
public bool TryGetAnimationRuntime(uint localEntityId, out ILiveEntityAnimationRuntime runtime) =>
_animationsByLocalId.TryGetValue(localEntityId, out runtime!);
public bool ClearAnimationRuntime(uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.AnimationRuntime is null)
return false;
if (record.LocalEntityId is { } localId)
_animationsByLocalId.Remove(localId);
record.AnimationRuntime = null;
RefreshSpatialRuntimeIndexes(record);
return true;
}
/// <summary>
/// Acquires the one retail <c>CPhysicsObj</c> body for this exact live
/// incarnation. Static animation may need the body before a MovementManager
/// or projectile component exists; later components must adopt this same
/// instance instead of replaying CreateObject vector initialization.
/// </summary>
internal PhysicsBody GetOrCreatePhysicsBody(
uint serverGuid,
Func<LiveEntityRecord, PhysicsBody> factory)
{
ArgumentNullException.ThrowIfNull(factory);
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is null)
{
throw new InvalidOperationException(
$"Cannot acquire physics body before live entity 0x{serverGuid:X8} is materialized.");
}
if (record.PhysicsBody is { } retained)
return retained;
if (record.PhysicsBodyAcquisitionInProgress)
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} physics-body acquisition is already in progress.");
}
record.PhysicsBodyAcquisitionInProgress = true;
try
{
PhysicsBody candidate = factory(record)
?? throw new InvalidOperationException("Physics-body factory returned null.");
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, record)
|| current.WorldEntity is null)
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} changed incarnation during physics-body acquisition.");
}
if (record.PhysicsBody is { } concurrentlyBound)
{
if (ReferenceEquals(concurrentlyBound, candidate))
return concurrentlyBound;
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} acquired two physics bodies within one incarnation.");
}
record.PhysicsBody = candidate;
candidate.State = record.FinalPhysicsState;
SynchronizePhysicsBodyActiveState(record);
return candidate;
}
finally
{
record.PhysicsBodyAcquisitionInProgress = false;
}
}
public void SetRemoteMotionRuntime(uint serverGuid, ILiveEntityRemoteMotionRuntime runtime)
{
ArgumentNullException.ThrowIfNull(runtime);
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
throw new InvalidOperationException($"Cannot bind remote motion before live entity 0x{serverGuid:X8} exists.");
if (record.RemoteMotionBindingInProgress)
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} remote-motion binding is already in progress.");
PhysicsBody candidateBody = runtime.Body
?? throw new InvalidOperationException("A remote-motion runtime returned no physics body.");
if (ReferenceEquals(record.RemoteMotionRuntime, runtime))
{
if (!ReferenceEquals(record.PhysicsBody, candidateBody))
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} remote motion changed its canonical physics body.");
}
// Idempotent publication is important when motion and vector
// packets converge on the same runtime in one update turn. The
// component seams are incarnation-bound and must never be rebound.
candidateBody.State = record.FinalPhysicsState;
SynchronizePhysicsBodyActiveState(record);
_remoteMotionByGuid[serverGuid] = runtime;
RefreshSpatialRuntimeIndexes(record);
return;
}
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot bind remote motion during physics-body acquisition.");
if (record.PhysicsBody is { } canonicalBody
&& !ReferenceEquals(canonicalBody, candidateBody))
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
}
if (record.RequiresRemotePlacementRuntime
&& runtime is not ILiveEntityRemotePlacementRuntime)
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot discard its remote placement contract within one incarnation.");
}
// Bind every possibly-throwing exact-incarnation seam before
// publishing any canonical record/index state. An already-bound
// component from an older GUID generation must fail without poisoning
// the replacement record. The callback is arbitrary App code, so the
// exact record, authority epoch, and expected owners are revalidated
// before commit just like resource/physics-body acquisition.
LiveEntityRecord incarnation = record;
ulong sessionVersion = _sessionLifetimeVersion;
ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid);
PhysicsBody? expectedBody = record.PhysicsBody;
ILiveEntityRemoteMotionRuntime? expectedRuntime = record.RemoteMotionRuntime;
bool expectedPlacementContract = record.RequiresRemotePlacementRuntime;
bool expectedIndexPresent = _remoteMotionByGuid.TryGetValue(
serverGuid,
out ILiveEntityRemoteMotionRuntime? expectedIndexedRuntime);
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost = () =>
_recordsByGuid.TryGetValue(serverGuid, out var current)
&& ReferenceEquals(current, incarnation)
&& ReferenceEquals(current.RemoteMotionRuntime, runtime)
? current.PhysicsHost
: null;
// Reads remain bound to the exact CPhysicsObj incarnation through its
// exit_world notifications. Only writes require active ownership.
Func<uint> readCell = () => incarnation.FullCellId;
Action<uint> writeCell = cellId =>
{
if (cellId != 0
&& _recordsByGuid.TryGetValue(serverGuid, out var current)
&& ReferenceEquals(current, incarnation)
&& ReferenceEquals(current.RemoteMotionRuntime, runtime))
{
RebucketLiveEntity(serverGuid, cellId);
}
};
record.RemoteMotionBindingInProgress = true;
try
{
if (runtime is ILiveEntityCanonicalRuntimeConsumer canonicalConsumer)
{
canonicalConsumer.BindCanonicalRuntime(
readPhysicsHost,
readCell,
writeCell);
}
else
{
bool consumesHost = runtime is ILiveEntityPhysicsHostConsumer;
bool consumesCell = runtime is ILiveEntityCanonicalCellConsumer;
if (consumesHost && consumesCell)
{
throw new InvalidOperationException(
"A remote runtime that consumes both canonical host and cell identity must bind them atomically.");
}
if (runtime is ILiveEntityPhysicsHostConsumer hostConsumer)
hostConsumer.BindPhysicsHost(readPhysicsHost);
if (runtime is ILiveEntityCanonicalCellConsumer cellConsumer)
cellConsumer.BindCanonicalCell(readCell, writeCell);
}
bool indexUnchanged = _remoteMotionByGuid.TryGetValue(
serverGuid,
out ILiveEntityRemoteMotionRuntime? currentIndexedRuntime)
== expectedIndexPresent
&& (!expectedIndexPresent
|| ReferenceEquals(currentIndexedRuntime, expectedIndexedRuntime));
if (_sessionLifetimeVersion != sessionVersion
|| CurrentLifetimeMutation(serverGuid) != lifetimeVersion
|| !_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, incarnation)
|| !ReferenceEquals(current.PhysicsBody, expectedBody)
|| !ReferenceEquals(current.RemoteMotionRuntime, expectedRuntime)
|| current.RequiresRemotePlacementRuntime != expectedPlacementContract
|| !indexUnchanged)
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} changed incarnation or component ownership during remote-motion binding.");
}
record.RequiresRemotePlacementRuntime |=
runtime is ILiveEntityRemotePlacementRuntime;
record.RemoteMotionRuntime = runtime;
record.PhysicsBody = candidateBody;
candidateBody.State = record.FinalPhysicsState;
SynchronizePhysicsBodyActiveState(record);
_remoteMotionByGuid[serverGuid] = runtime;
RefreshSpatialRuntimeIndexes(record);
}
finally
{
record.RemoteMotionBindingInProgress = false;
}
}
public void InstallPhysicsHost(
LiveEntityRecord expectedRecord,
AcDream.Core.Physics.Motion.IPhysicsObjHost host)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
ArgumentNullException.ThrowIfNull(host);
uint serverGuid = expectedRecord.ServerGuid;
if (host.Id != serverGuid)
throw new ArgumentException("A physics host must match its live entity GUID.", nameof(host));
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| !ReferenceEquals(record, expectedRecord))
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} changed incarnation during physics-host installation.");
}
if (record.PhysicsHost is not null)
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} already owns its incarnation-stable physics host.");
record.PhysicsHost = host;
}
public bool TryGetPhysicsHost(
uint serverGuid,
out AcDream.Core.Physics.Motion.IPhysicsObjHost host)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.PhysicsHost is { } existing)
{
host = existing;
return true;
}
host = null!;
return false;
}
public bool TryGetRemoteMotionRuntime(uint serverGuid, out ILiveEntityRemoteMotionRuntime runtime) =>
_remoteMotionByGuid.TryGetValue(serverGuid, out runtime!);
public bool ClearRemoteMotionRuntime(uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.RemoteMotionRuntime is null)
return false;
_remoteMotionByGuid.Remove(serverGuid);
record.RemoteMotionRuntime = null;
RefreshSpatialRuntimeIndexes(record);
return true;
}
public void SetProjectileRuntime(uint serverGuid, ILiveEntityProjectileRuntime runtime)
{
ArgumentNullException.ThrowIfNull(runtime);
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is null)
{
throw new InvalidOperationException(
$"Cannot bind projectile physics before live entity 0x{serverGuid:X8} is materialized.");
}
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot bind projectile physics during physics-body acquisition.");
PhysicsBody candidateBody = runtime.Body;
if (record.PhysicsBody is { } canonicalBody
&& !ReferenceEquals(canonicalBody, candidateBody))
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
}
record.ProjectileRuntime = runtime;
record.PhysicsBody = candidateBody;
candidateBody.State = record.FinalPhysicsState;
SynchronizePhysicsBodyActiveState(record);
RefreshSpatialRuntimeIndexes(record);
}
public bool TryGetProjectileRuntime(
uint serverGuid,
out ILiveEntityProjectileRuntime runtime)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.ProjectileRuntime is { } found)
{
runtime = found;
return true;
}
runtime = null!;
return false;
}
public bool ClearProjectileRuntime(uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.ProjectileRuntime is null)
return false;
record.ProjectileRuntime = null;
RefreshSpatialRuntimeIndexes(record);
return true;
}
public void SetEffectProfile(uint serverGuid, ILiveEntityEffectProfile profile)
{
ArgumentNullException.ThrowIfNull(profile);
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
throw new InvalidOperationException(
$"Cannot bind an effect profile before live entity 0x{serverGuid:X8} exists.");
record.EffectProfile = profile;
}
public bool TryGetEffectProfile(
uint serverGuid,
out ILiveEntityEffectProfile profile)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& record.EffectProfile is { } found)
{
profile = found;
return true;
}
profile = null!;
return false;
}
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
_inbound.TryGetSnapshot(guid, out spawn);
public bool TryApplyObjDesc(ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{
bool applied = _inbound.TryApplyObjDesc(update, out accepted);
if (applied)
{
RefreshRecord(update.Guid, accepted);
if (_recordsByGuid.TryGetValue(update.Guid, out LiveEntityRecord? record))
record.AdvanceObjDescAuthority();
}
return applied;
}
public bool TryApplyPickup(PickupEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{
bool applied = _inbound.TryApplyPickup(update, out accepted);
if (applied)
{
RefreshRecord(update.Guid, accepted);
if (_recordsByGuid.TryGetValue(update.Guid, out LiveEntityRecord? record))
record.AdvancePositionAuthority();
ClearWorldCell(update.Guid);
ParentAttachments.EndChildProjection(update.Guid);
}
return applied;
}
public bool TryApplyCreateParent(CreateParentUpdate update, out WorldSession.EntitySpawn accepted)
{
bool applied = _inbound.TryApplyCreateParent(update, out accepted);
if (applied)
{
RefreshRecord(update.ChildGuid, accepted);
if (_recordsByGuid.TryGetValue(update.ChildGuid, out LiveEntityRecord? record))
record.AdvancePositionAuthority();
}
return applied;
}
public bool TryApplyParent(ParentEvent.Parsed update, out WorldSession.EntitySpawn accepted)
{
bool applied = _inbound.TryApplyParent(update, out accepted);
if (applied)
{
RefreshRecord(update.ChildGuid, accepted);
if (_recordsByGuid.TryGetValue(update.ChildGuid, out LiveEntityRecord? record))
record.AdvancePositionAuthority();
}
return applied;
}
internal bool CommitStagedParent(
ParentAttachmentRelation relation,
out WorldSession.EntitySpawn accepted)
{
bool committed = _inbound.TryCommitParent(
relation.ChildGuid,
relation.ParentGuid,
relation.ParentLocation,
relation.PlacementId,
relation.ChildPositionSequence,
out accepted);
if (committed)
RefreshRecord(relation.ChildGuid, accepted);
return committed;
}
/// <summary>
/// Commits retail <c>CPhysicsObj::set_parent</c>'s cell-less edge only
/// after the parent accepted the child (PartArray + holding-location
/// validation). The POSITION_TS is consumed before this step, matching
/// retail; invalid parent relationships retain the old world projection.
/// </summary>
internal bool CommitAcceptedParentCellless(
LiveEntityRecord record,
ulong positionAuthorityVersion)
{
ArgumentNullException.ThrowIfNull(record);
if (!IsCurrentPositionAuthority(record, positionAuthorityVersion))
return false;
ClearWorldCell(record.ServerGuid);
return IsCurrentPositionAuthority(record, positionAuthorityVersion);
}
public bool TryApplyMotion(
WorldSession.EntityMotionUpdate update,
bool retainPayload,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
bool applied = _inbound.TryApplyMotion(update, retainPayload, out accepted, out timestamps);
if (_inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
RefreshRecord(update.Guid, snapshot);
if (applied
&& retainPayload
&& _recordsByGuid.TryGetValue(update.Guid, out LiveEntityRecord? record))
{
record.AdvanceMovementAuthority();
}
return applied;
}
public bool TryApplyVector(VectorUpdate.Parsed update, out WorldSession.EntitySpawn accepted)
{
bool applied = _inbound.TryApplyVector(update, out accepted);
if (applied)
{
RefreshRecord(update.Guid, accepted);
if (_recordsByGuid.TryGetValue(update.Guid, out LiveEntityRecord? record))
record.AdvanceVectorAuthority();
}
return applied;
}
public bool TryApplyState(SetState.Parsed update, out WorldSession.EntitySpawn accepted)
=> TryApplyState(update, out accepted, out _);
public bool TryApplyState(
SetState.Parsed update,
out WorldSession.EntitySpawn accepted,
out RetailPhysicsStateTransition transition)
{
bool applied = _inbound.TryApplyState(update, out accepted);
transition = default;
if (applied)
{
RefreshRecord(update.Guid, accepted);
if (_recordsByGuid.TryGetValue(update.Guid, out LiveEntityRecord? record))
{
transition = record.ApplyRawPhysicsState(update.PhysicsState);
RefreshPresentation(record);
}
}
return applied;
}
/// <summary>
/// Retail parent Hidden directly toggles each attached child's NoDraw bit
/// without consuming the child's STATE_TS or replacing its raw wire state.
/// </summary>
public bool SetAttachedChildNoDraw(uint childServerGuid, bool noDraw)
{
if (!_recordsByGuid.TryGetValue(childServerGuid, out LiveEntityRecord? record))
return false;
record.SetChildNoDraw(noDraw);
RefreshPresentation(record);
return true;
}
public bool TryApplyPosition(
WorldSession.EntityPositionUpdate update,
bool isLocalPlayer,
System.Numerics.Quaternion? forcePositionRotation,
System.Numerics.Vector3? currentLocalVelocity,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
bool wasCellLess = _recordsByGuid.TryGetValue(
update.Guid,
out LiveEntityRecord? beforePosition)
&& (beforePosition.FullCellId == 0
|| !beforePosition.IsSpatiallyProjected
|| !beforePosition.IsSpatiallyVisible);
bool known = _inbound.TryApplyPosition(
update,
isLocalPlayer,
forcePositionRotation,
currentLocalVelocity,
out disposition,
out accepted,
out timestamps);
if (known && _inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
{
bool acceptedPosition = disposition is not PositionTimestampDisposition.Rejected;
if (disposition is PositionTimestampDisposition.Apply)
{
timestamps = timestamps with
{
TeleportHookRequired = timestamps.TeleportAdvanced || wasCellLess,
};
}
RefreshRecord(update.Guid, snapshot, refreshPosition: acceptedPosition);
if (acceptedPosition)
{
if (_recordsByGuid.TryGetValue(
update.Guid,
out LiveEntityRecord? acceptedRecord)
&& ReferenceEquals(acceptedRecord, beforePosition))
{
acceptedRecord.AdvancePositionAuthority();
}
ParentAttachments.EndChildProjection(update.Guid);
}
}
return known;
}
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
_inbound.IsFreshTeleportStart(localPlayerGuid, teleportSequence);
/// <summary>
/// Retail <c>CPhysicsObj::update_object</c> (<c>0x00515D40</c>) runs root
/// object tick only while the object has visible world-cell membership and
/// is not Frozen. Hidden is deliberately not a broad tick stop: retail
/// skips PartArray animation and physics, but still applies the
/// PositionManager offset and advances targeting/movement/script/particle
/// owners. A pickup or parent transition keeps
/// script/effect owners but clears the cell and withdraws the root
/// projection until a fresh Position re-enters it.
/// </summary>
public RetailObjectClockDisposition GetRootObjectClockDisposition(
uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.ProjectionKind is not LiveEntityProjectionKind.World
|| !record.IsSpatiallyProjected
|| !record.IsSpatiallyVisible
|| record.FullCellId == 0)
{
return RetailObjectClockDisposition.Suspend;
}
return (record.FinalPhysicsState & PhysicsStateFlags.Frozen) != 0
? RetailObjectClockDisposition.Suspend
: RetailObjectClockDisposition.Advance;
}
public bool ShouldAdvanceRootRuntime(uint serverGuid) =>
GetRootObjectClockDisposition(serverGuid)
is RetailObjectClockDisposition.Advance;
internal bool IsCurrentPositionAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.PositionAuthorityVersion == authorityVersion;
internal bool IsCurrentStateAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.StateAuthorityVersion == authorityVersion;
internal bool IsCurrentVectorAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.VectorAuthorityVersion == authorityVersion;
internal bool IsCurrentVelocityAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.VelocityAuthorityVersion == authorityVersion;
internal bool IsCurrentMovementAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.MovementAuthorityVersion == authorityVersion;
internal bool IsCurrentObjDescAuthority(
LiveEntityRecord record,
ulong authorityVersion) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& current.ObjDescAuthorityVersion == authorityVersion;
/// <summary>
/// Copies the canonical loaded root-object workset used by retail
/// <c>CPhysics::UseTime</c>. The record reference is the incarnation token;
/// callers must revalidate it after callbacks that can delete or rebucket.
/// </summary>
internal void CopySpatialRootObjectRecordsTo(List<LiveEntityRecord> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((uint serverGuid, LiveEntityRecord indexed) in _spatialRootObjectsByGuid)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, indexed)
&& HasSpatialRuntimeProjection(current))
{
destination.Add(current);
}
}
}
internal bool IsCurrentSpatialRootObject(LiveEntityRecord record) =>
IsCurrentRecord(record)
&& _spatialRootObjectsByGuid.TryGetValue(record.ServerGuid, out var indexed)
&& ReferenceEquals(indexed, record)
&& HasSpatialRuntimeProjection(record);
internal bool IsCurrentSpatialAnimation(
LiveEntityRecord record,
ILiveEntityAnimationRuntime animation) =>
IsCurrentSpatialRootObject(record)
&& ReferenceEquals(record.AnimationRuntime, animation)
&& record.WorldEntity is { } entity
&& _spatialAnimationsByLocalId.TryGetValue(entity.Id, out var indexed)
&& ReferenceEquals(indexed, animation);
/// <summary>
/// Copies the currently spatial animation owners through the concrete
/// dictionary enumerator. This keeps per-frame traversal allocation-free;
/// exposing the workset as <see cref="IReadOnlyDictionary{TKey,TValue}"/>
/// would box its enumerator at a hot call site.
/// </summary>
internal void CopySpatialAnimationRuntimesTo<TAnimation>(
List<KeyValuePair<uint, TAnimation>> destination)
where TAnimation : class, ILiveEntityAnimationRuntime
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((uint localId, ILiveEntityAnimationRuntime animation)
in _spatialAnimationsByLocalId)
{
if (animation is TAnimation typed)
{
destination.Add(
new KeyValuePair<uint, TAnimation>(localId, typed));
}
}
}
internal void CopySpatialAnimationLocalIdsTo(HashSet<uint> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach (uint localId in _spatialAnimationsByLocalId.Keys)
destination.Add(localId);
}
/// <summary>
/// Host side of retail <c>CPhysicsObj::set_active(1)</c> for a movement
/// manager owned by this exact incarnation. Static is a required no-op.
/// </summary>
internal bool TryActivateOrdinaryObject(
uint serverGuid,
ILiveEntityRemoteMotionRuntime runtime)
{
ArgumentNullException.ThrowIfNull(runtime);
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| !ReferenceEquals(record.RemoteMotionRuntime, runtime)
|| (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0)
{
return false;
}
record.ObjectClock.Activate();
runtime.Body.TransientState |= TransientStateFlags.Active;
return true;
}
/// <summary>
/// Commits retail <c>CPhysicsObj::set_velocity</c> (0x005113F0) to the
/// canonical body for this exact incarnation. The body and the retained
/// object clock are one CPhysicsObj: a non-Static inactive-to-active edge
/// rebases both together, while Static preserves its prior Active state.
/// </summary>
internal bool TryCommitAuthoritativeVelocity(
LiveEntityRecord record,
PhysicsBody body,
Vector3 velocity,
double currentTime) =>
TryCommitAuthoritativeVectorCore(
record,
body,
velocity,
angularVelocity: null,
currentTime);
/// <summary>
/// Commits the paired retail VectorUpdate writes
/// (<c>set_velocity</c>, then <c>set_omega</c>) to one canonical body.
/// Validation is atomic: malformed wire vectors cannot partially mutate
/// the live object.
/// </summary>
internal bool TryCommitAuthoritativeVector(
LiveEntityRecord record,
PhysicsBody body,
Vector3 velocity,
Vector3 angularVelocity,
double currentTime) =>
TryCommitAuthoritativeVectorCore(
record,
body,
velocity,
angularVelocity,
currentTime);
private bool TryCommitAuthoritativeVectorCore(
LiveEntityRecord record,
PhysicsBody body,
Vector3 velocity,
Vector3? angularVelocity,
double currentTime)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(body);
if (!IsFinite(velocity)
|| (angularVelocity is { } omega && !IsFinite(omega))
|| !double.IsFinite(currentTime)
|| !_recordsByGuid.TryGetValue(
record.ServerGuid,
out LiveEntityRecord? current)
|| !ReferenceEquals(current, record)
|| !ReferenceEquals(current.PhysicsBody, body))
{
return false;
}
bool wasBodyActive =
(body.TransientState & TransientStateFlags.Active) != 0;
body.set_velocity(velocity);
if ((current.FinalPhysicsState & PhysicsStateFlags.Static) != 0)
{
// PhysicsBody models set_velocity's leaf write and therefore sets
// Active unconditionally. Retail CPhysicsObj::set_active(1) is a
// no-op for Static, so restore the exact prior bit here.
if (!wasBodyActive)
body.TransientState &= ~TransientStateFlags.Active;
}
else
{
bool clockReactivated = current.ObjectClock.Activate();
// PhysicsBody.LastUpdateTime remains only the compatibility clock
// for older absolute-time helpers; the record clock is canonical.
if (clockReactivated || !wasBodyActive)
body.LastUpdateTime = currentTime;
}
if (angularVelocity is { } acceptedOmega)
body.Omega = acceptedOmega;
return true;
}
/// <summary>
/// Copies the currently spatial remote-motion owners into caller-owned
/// reusable storage. The record reference is the incarnation token: a
/// delete and GUID reuse cannot make an older snapshot address the newer
/// object.
/// </summary>
internal void CopySpatialRemoteMotionRecordsTo(List<LiveEntityRecord> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((uint serverGuid, ILiveEntityRemoteMotionRuntime runtime) in _spatialRemoteMotionByGuid)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& ReferenceEquals(record.RemoteMotionRuntime, runtime)
&& HasSpatialRuntimeProjection(record))
{
destination.Add(record);
}
}
}
internal bool IsCurrentSpatialRemoteMotion(
LiveEntityRecord record,
ILiveEntityRemoteMotionRuntime runtime) =>
IsCurrentRecord(record)
&& ReferenceEquals(record.RemoteMotionRuntime, runtime)
&& _spatialRemoteMotionByGuid.TryGetValue(record.ServerGuid, out var indexed)
&& ReferenceEquals(indexed, runtime)
&& HasSpatialRuntimeProjection(record);
/// <summary>
/// Copies only projectile owners that currently have a loaded world-cell
/// projection. Pending projectiles retain their logical runtime but impose
/// no per-frame scan cost.
/// </summary>
internal void CopySpatialProjectileRecordsTo(List<LiveEntityRecord> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((uint serverGuid, ILiveEntityProjectileRuntime runtime) in _spatialProjectilesByGuid)
{
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& ReferenceEquals(record.ProjectileRuntime, runtime)
&& HasSpatialRuntimeProjection(record))
{
destination.Add(record);
}
}
}
internal bool IsCurrentSpatialProjectile(
LiveEntityRecord record,
ILiveEntityProjectileRuntime runtime) =>
IsCurrentRecord(record)
&& ReferenceEquals(record.ProjectileRuntime, runtime)
&& _spatialProjectilesByGuid.TryGetValue(record.ServerGuid, out var indexed)
&& ReferenceEquals(indexed, runtime)
&& HasSpatialRuntimeProjection(record);
public bool IsHidden(uint serverGuid) =>
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
&& (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
/// <summary>
/// Claims the one logical top-level spawn notification for this object
/// incarnation. Spatial re-entry must restore presentation without replaying
/// plugin/event registration. An attached-only child claims nothing until it
/// first becomes a top-level world projection.
/// </summary>
public bool TryMarkWorldSpawnPublished(uint serverGuid)
{
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is null
|| record.ProjectionKind is not LiveEntityProjectionKind.World
|| record.WorldSpawnPublished)
return false;
record.WorldSpawnPublished = true;
return true;
}
internal bool TryMarkInitialHydrationCompleted(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentRecord(expectedRecord)
|| expectedRecord.CreateIntegrationVersion != expectedCreateIntegrationVersion
|| expectedRecord.WorldEntity is null
|| !expectedRecord.ResourcesRegistered)
{
return false;
}
expectedRecord.InitialHydrationCompleted = true;
return true;
}
internal bool TryMarkCreateProjectionSynchronizationPending(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
expectedRecord.CreateProjectionSynchronizationPending = true;
return true;
}
internal bool TryCompleteCreateProjectionSynchronization(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
expectedRecord.CreateProjectionSynchronizationPending = false;
return true;
}
internal bool TryBeginProjectionHydration(
LiveEntityRecord expectedRecord,
ulong? expectedCreateIntegrationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!IsCurrentRecord(expectedRecord)
|| (expectedCreateIntegrationVersion is { } expected
&& expectedRecord.CreateIntegrationVersion != expected))
{
return false;
}
if (expectedRecord.ProjectionHydrationInProgress)
{
if (expectedRecord.CreateIntegrationVersion
!= expectedRecord.ProjectionHydrationCreateVersion)
{
expectedRecord.ProjectionHydrationRetryRequested = true;
}
return false;
}
expectedRecord.ProjectionHydrationInProgress = true;
expectedRecord.ProjectionHydrationCreateVersion =
expectedRecord.CreateIntegrationVersion;
expectedRecord.ProjectionHydrationRetryRequested = false;
return true;
}
internal bool EndProjectionHydration(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
bool retry = IsCurrentRecord(expectedRecord)
&& (expectedRecord.ProjectionHydrationRetryRequested
|| expectedRecord.CreateIntegrationVersion
!= expectedRecord.ProjectionHydrationCreateVersion);
if (retry)
{
// Persist the exact replay obligation at the point version drift
// is observed. The stale projection body may be unwinding through
// an exception and therefore never reach the controller's next
// retry-loop iteration.
expectedRecord.CreateProjectionSynchronizationPending = true;
}
expectedRecord.ProjectionHydrationInProgress = false;
expectedRecord.ProjectionHydrationCreateVersion = 0UL;
expectedRecord.ProjectionHydrationRetryRequested = false;
return retry;
}
internal bool TryBeginAppearanceHydration(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!expectedRecord.AppearanceProjectionSynchronizationPending
|| !IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion))
{
return false;
}
expectedRecord.AppearanceProjectionSynchronizationPending = true;
if (expectedRecord.AppearanceHydrationInProgress)
{
expectedRecord.AppearanceHydrationRetryRequested = true;
return false;
}
expectedRecord.AppearanceHydrationInProgress = true;
expectedRecord.AppearanceHydrationVersion =
expectedObjDescAuthorityVersion;
expectedRecord.AppearanceHydrationRetryRequested = false;
return true;
}
internal bool EndAppearanceHydration(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
bool retry = IsCurrentRecord(expectedRecord)
&& (expectedRecord.AppearanceHydrationRetryRequested
|| expectedRecord.ObjDescAuthorityVersion
!= expectedRecord.AppearanceHydrationVersion);
expectedRecord.AppearanceHydrationInProgress = false;
expectedRecord.AppearanceHydrationVersion = 0UL;
expectedRecord.AppearanceHydrationRetryRequested = false;
return retry;
}
internal bool TryCompleteAppearanceProjectionSynchronization(
LiveEntityRecord expectedRecord,
ulong expectedObjDescAuthorityVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
if (!expectedRecord.AppearanceProjectionSynchronizationPending
|| !IsCurrentObjDescAuthority(
expectedRecord,
expectedObjDescAuthorityVersion))
{
return false;
}
expectedRecord.AppearanceProjectionSynchronizationPending = false;
return true;
}
internal bool IsCurrentCreateIntegration(
LiveEntityRecord expectedRecord,
ulong expectedCreateIntegrationVersion) =>
IsCurrentRecord(expectedRecord)
&& expectedRecord.CreateIntegrationVersion == expectedCreateIntegrationVersion;
public void Clear()
{
if (_isClearing || _isRegisteringResources)
{
throw new InvalidOperationException(
_isClearing
? "Live entity session teardown is already in progress."
: "Live entity session teardown cannot begin inside atomic resource registration.");
}
_isClearing = true;
_sessionClearPendingFinalization = true;
_sessionLifetimeVersion++;
_lifetimeMutationVersionByGuid.Clear();
List<Exception>? failures = null;
try
{
foreach (LiveEntityRecord record in _recordsByGuid.Values.ToArray())
{
if (!_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
|| !ReferenceEquals(current, record))
continue;
_recordsByGuid.Remove(record.ServerGuid);
RetainTeardownRecord(record);
}
ParentAttachments.Clear();
_inbound.Clear();
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{
// A re-entrant Clear can observe the tombstone currently being
// unregistered by its caller. Leave it queued; the outer
// teardown's ReleaseTeardownRecord finalizes the session once
// it unwinds successfully.
if (record.TeardownInProgress)
continue;
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
CompleteSessionClearIfConverged();
}
finally
{
_isClearing = false;
}
if (failures is not null)
throw new AggregateException("One or more live entities failed session teardown.", failures);
}
/// <summary>
/// Drains teardown tombstones whose first unregister attempt failed. This
/// runs on the update thread after the callback stack that deferred the
/// removal has unwound; successful steps are not replayed.
/// </summary>
public int RetryPendingTeardowns()
{
if (_teardownRecords.Count == 0
|| _isClearing
|| _isRegisteringResources
|| _logicalTeardownDepth != 0)
return 0;
int completed = 0;
List<Exception>? failures = null;
foreach (LiveEntityRecord record in _teardownRecords.Values.ToArray())
{
if (record.TeardownInProgress)
continue;
_logicalTeardownDepth++;
try
{
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
completed++;
if (!_recordsByGuid.ContainsKey(record.ServerGuid)
&& !HasPendingTeardown(record.ServerGuid)
&& !_inbound.TryGetSnapshot(record.ServerGuid, out _))
{
_spatial.ForgetLiveEntity(record.ServerGuid);
}
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
finally
{
_logicalTeardownDepth--;
}
}
if (failures is not null)
throw new AggregateException("One or more pending live-entity teardowns failed again.", failures);
return completed;
}
private void RefreshRecord(
uint guid,
WorldSession.EntitySpawn accepted,
bool refreshPosition = false)
{
if (!_recordsByGuid.TryGetValue(guid, out LiveEntityRecord? record))
return;
record.Snapshot = accepted;
record.RefreshDerivedState(refreshPosition);
}
private static InboundCreateResult SupersededCreateResult() => new(
CreateObjectTimestampDisposition.StaleGeneration,
default,
null,
default);
private ulong AdvanceLifetimeMutation(uint serverGuid)
{
ulong next = _lifetimeMutationVersionByGuid.GetValueOrDefault(serverGuid) + 1UL;
_lifetimeMutationVersionByGuid[serverGuid] = next;
return next;
}
private ulong CurrentLifetimeMutation(uint serverGuid) =>
_lifetimeMutationVersionByGuid.GetValueOrDefault(serverGuid);
private bool IsCurrentProjectionOperation(
uint serverGuid,
LiveEntityRecord record,
ulong projectionOperation) =>
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record)
&& record.ProjectionMutationVersion == projectionOperation;
private static void ThrowAfterCommittedProjectionChange(
uint serverGuid,
Exception? spatialNotificationFailure,
Exception? runtimeNotificationFailure)
{
if (spatialNotificationFailure is not null
&& runtimeNotificationFailure is not null)
{
throw new AggregateException(
$"Projection change for live entity 0x{serverGuid:X8} committed, but spatial and runtime observers failed.",
spatialNotificationFailure,
runtimeNotificationFailure);
}
Exception? failure = spatialNotificationFailure ?? runtimeNotificationFailure;
if (failure is not null)
ExceptionDispatchInfo.Capture(failure).Throw();
}
private void ClearWorldCell(uint guid)
{
if (!_recordsByGuid.TryGetValue(guid, out LiveEntityRecord? record))
return;
bool wasOrdinaryRoot = _spatialRootObjectsByGuid.TryGetValue(
guid,
out LiveEntityRecord? indexedRoot)
&& ReferenceEquals(indexedRoot, record);
// Pickup/Parent is retail's leave-world edge. Suspend the canonical
// object clock while the record is still indexed as a root; clearing
// FullCellId first removes that evidence and makes the later projection
// withdrawal look like a duplicate non-root edge.
if (wasOrdinaryRoot)
{
record.SuspendObjectClock();
SynchronizePhysicsBodyActiveState(record);
}
record.FullCellId = 0;
record.CanonicalLandblockId = 0;
RefreshSpatialRuntimeIndexes(record);
}
internal bool IsCurrentRecord(LiveEntityRecord record) =>
_recordsByGuid.TryGetValue(record.ServerGuid, out LiveEntityRecord? current)
&& ReferenceEquals(current, record);
private static bool HasSpatialRuntimeProjection(LiveEntityRecord record) =>
record.WorldEntity is not null
&& record.ProjectionKind is LiveEntityProjectionKind.World
&& record.IsSpatiallyProjected
&& record.IsSpatiallyVisible
&& record.FullCellId != 0;
private void RefreshSpatialRuntimeIndexes(LiveEntityRecord record)
{
bool current = IsCurrentRecord(record);
bool spatial = current && HasSpatialRuntimeProjection(record);
if (spatial)
{
_spatialRootObjectsByGuid[record.ServerGuid] = record;
}
else if (current
|| (_spatialRootObjectsByGuid.TryGetValue(record.ServerGuid, out var indexedRoot)
&& ReferenceEquals(indexedRoot, record)))
{
_spatialRootObjectsByGuid.Remove(record.ServerGuid);
}
if (record.WorldEntity is { } entity)
{
if (spatial && record.AnimationRuntime is { } animation)
{
_spatialAnimationsByLocalId[entity.Id] = animation;
}
else if (current
|| (record.AnimationRuntime is { } retainedAnimation
&& _spatialAnimationsByLocalId.TryGetValue(entity.Id, out var indexedAnimation)
&& ReferenceEquals(indexedAnimation, retainedAnimation)))
{
_spatialAnimationsByLocalId.Remove(entity.Id);
}
}
if (spatial && record.RemoteMotionRuntime is { } remote)
{
_spatialRemoteMotionByGuid[record.ServerGuid] = remote;
}
else if (current
|| (record.RemoteMotionRuntime is { } retainedRemote
&& _spatialRemoteMotionByGuid.TryGetValue(record.ServerGuid, out var indexedRemote)
&& ReferenceEquals(indexedRemote, retainedRemote)))
{
_spatialRemoteMotionByGuid.Remove(record.ServerGuid);
}
if (spatial && record.ProjectileRuntime is { } projectile)
{
_spatialProjectilesByGuid[record.ServerGuid] = projectile;
}
else if (current
|| (record.ProjectileRuntime is { } retainedProjectile
&& _spatialProjectilesByGuid.TryGetValue(record.ServerGuid, out var indexedProjectile)
&& ReferenceEquals(indexedProjectile, retainedProjectile)))
{
_spatialProjectilesByGuid.Remove(record.ServerGuid);
}
}
private void RemoveSpatialRuntimeIndexes(LiveEntityRecord record)
{
if (_spatialRootObjectsByGuid.TryGetValue(record.ServerGuid, out var indexedRoot)
&& ReferenceEquals(indexedRoot, record))
{
_spatialRootObjectsByGuid.Remove(record.ServerGuid);
}
if (record.WorldEntity is { } entity
&& _spatialAnimationsByLocalId.TryGetValue(entity.Id, out var indexedAnimation)
&& ReferenceEquals(indexedAnimation, record.AnimationRuntime))
{
_spatialAnimationsByLocalId.Remove(entity.Id);
}
if (_spatialRemoteMotionByGuid.TryGetValue(record.ServerGuid, out var indexedRemote)
&& ReferenceEquals(indexedRemote, record.RemoteMotionRuntime))
{
_spatialRemoteMotionByGuid.Remove(record.ServerGuid);
}
if (_spatialProjectilesByGuid.TryGetValue(record.ServerGuid, out var indexedProjectile)
&& ReferenceEquals(indexedProjectile, record.ProjectileRuntime))
{
_spatialProjectilesByGuid.Remove(record.ServerGuid);
}
}
private uint ReserveLocalEntityId()
{
uint start = _nextLocalEntityId;
do
{
uint candidate = _nextLocalEntityId;
_nextLocalEntityId = candidate == LastLiveEntityId
? FirstLiveEntityId
: candidate + 1u;
if (!_guidByLocalId.ContainsKey(candidate))
return candidate;
}
while (_nextLocalEntityId != start);
throw new InvalidOperationException("The live entity id namespace is exhausted.");
}
private void OnSpatialVisibilityChanged(uint serverGuid, bool visible)
{
// GpuWorldState serializes re-entrant visibility callbacks. A callback
// may delete an incarnation and materialize the same server GUID before
// an older queued edge drains. Apply an edge only while it still
// matches current spatial truth; otherwise it belongs to the displaced
// projection and must not mutate the replacement generation.
if (_spatial.IsLiveEntityProjectionResident(serverGuid) != visible)
return;
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is not { } entity)
return;
bool wasVisible = record.IsSpatiallyVisible;
bool wasOrdinaryRoot = _spatialRootObjectsByGuid.TryGetValue(
serverGuid,
out LiveEntityRecord? indexedRoot)
&& ReferenceEquals(indexedRoot, record);
record.IsSpatiallyVisible = visible;
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World
&& record.IsSpatiallyProjected
&& visible
&& record.FullCellId != 0;
if (_rebucketingGuid != serverGuid && wasOrdinaryRoot != isOrdinaryRoot)
{
if (isOrdinaryRoot)
record.ResetObjectClockForEnterWorld(
(record.FinalPhysicsState & PhysicsStateFlags.Static) != 0);
else
record.SuspendObjectClock();
SynchronizePhysicsBodyActiveState(record);
}
RefreshSpatialRuntimeIndexes(record);
RefreshPresentation(record);
if (_rebucketingGuid != serverGuid && wasVisible != visible)
PublishProjectionVisibilityChanged(record, visible);
}
private static void SynchronizePhysicsBodyActiveState(
LiveEntityRecord record)
{
if (record.PhysicsBody is not { } body)
return;
if (record.ObjectClock.IsActive)
body.TransientState |= TransientStateFlags.Active;
else
body.TransientState &= ~TransientStateFlags.Active;
}
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
private void PublishProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
{
Delegate[] subscribers = ProjectionVisibilityChanged?.GetInvocationList()
?? Array.Empty<Delegate>();
List<Exception>? failures = null;
for (int i = 0; i < subscribers.Length; i++)
{
try
{
((Action<LiveEntityRecord, bool>)subscribers[i])(record, visible);
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
}
}
if (failures is not null)
{
throw new AggregateException(
$"One or more projection observers failed for live entity 0x{record.ServerGuid:X8}.",
failures);
}
}
private void RefreshPresentation(LiveEntityRecord record)
{
if (record.WorldEntity is not { } entity)
return;
PhysicsStateFlags state = record.FinalPhysicsState;
entity.IsDrawVisible =
(state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
bool interactionVisible = record.IsSpatiallyVisible
&& record.ProjectionKind is LiveEntityProjectionKind.World
&& (state & PhysicsStateFlags.Hidden) == 0;
if (interactionVisible)
_visibleWorldEntitiesByGuid[record.ServerGuid] = entity;
else
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
private static (uint Guid, ushort Generation) TeardownKey(LiveEntityRecord record) =>
(record.ServerGuid, record.Generation);
private void RetainTeardownRecord(LiveEntityRecord record)
{
var key = TeardownKey(record);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained)
&& !ReferenceEquals(retained, record))
{
throw new InvalidOperationException(
$"Live entity teardown tombstone collision for 0x{record.ServerGuid:X8} generation {record.Generation}.");
}
_teardownRecords[key] = record;
if (record.WorldEntity is { } entity
&& _visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? visible)
&& ReferenceEquals(visible, entity))
{
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
}
private void ReleaseTeardownRecord(LiveEntityRecord record)
{
var key = TeardownKey(record);
if (_teardownRecords.TryGetValue(key, out LiveEntityRecord? retained)
&& ReferenceEquals(retained, record))
{
_teardownRecords.Remove(key);
}
CompleteSessionClearIfConverged();
}
private bool HasPendingTeardown(uint serverGuid)
{
foreach ((uint Guid, ushort Generation) key in _teardownRecords.Keys)
{
if (key.Guid == serverGuid)
return true;
}
return false;
}
private void CompleteSessionClearIfConverged()
{
if (!_sessionClearPendingFinalization
|| _recordsByGuid.Count != 0
|| _teardownRecords.Count != 0)
{
return;
}
_materializedWorldEntitiesByGuid.Clear();
_visibleWorldEntitiesByGuid.Clear();
_guidByLocalId.Clear();
_animationsByLocalId.Clear();
_remoteMotionByGuid.Clear();
_spatialAnimationsByLocalId.Clear();
_spatialRemoteMotionByGuid.Clear();
_spatialProjectilesByGuid.Clear();
_spatialRootObjectsByGuid.Clear();
ParentAttachments.Clear();
_inbound.Clear();
_spatial.ClearLiveEntityLifetimeState();
_sessionClearPendingFinalization = false;
}
private void TearDownRecord(LiveEntityRecord record)
{
if (record.TeardownInProgress)
throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8} teardown is already in progress.");
record.TeardownInProgress = true;
List<Exception>? failures = null;
bool TryCleanup(Action cleanup)
{
try
{
cleanup();
return true;
}
catch (Exception error)
{
(failures ??= new List<Exception>()).Add(error);
return false;
}
}
try
{
// Remove frame-workset membership before arbitrary callbacks. The
// logical component dictionaries and WorldEntity remain intact as
// a retryable tombstone until every external cleanup acknowledges
// success.
RemoveSpatialRuntimeIndexes(record);
if (!record.RuntimeComponentsTeardownCompleted)
{
record.RuntimeComponentsTeardownCompleted =
TryCleanup(() => _runtimeComponentLifecycle.TearDown(record));
}
if (record.WorldEntity is { } entity)
{
if (!record.SpatialProjectionTeardownCompleted)
{
record.SpatialProjectionTeardownCompleted =
TryCleanup(() => _spatial.RemoveLiveEntityProjection(entity));
}
if (record.ResourcesRegistered
&& TryCleanup(() => _resources.Unregister(entity)))
{
record.ResourcesRegistered = false;
}
}
if (failures is not null)
{
throw new AggregateException(
$"Live entity 0x{record.ServerGuid:X8} teardown failed.",
failures);
}
if (record.WorldEntity is { } finalizedEntity)
{
_guidByLocalId.Remove(finalizedEntity.Id);
if (_materializedWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? materialized)
&& ReferenceEquals(materialized, finalizedEntity))
{
_materializedWorldEntitiesByGuid.Remove(record.ServerGuid);
}
if (_visibleWorldEntitiesByGuid.TryGetValue(
record.ServerGuid,
out WorldEntity? visible)
&& ReferenceEquals(visible, finalizedEntity))
{
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
}
_animationsByLocalId.Remove(finalizedEntity.Id);
}
if (_remoteMotionByGuid.TryGetValue(record.ServerGuid, out var remote)
&& ReferenceEquals(remote, record.RemoteMotionRuntime))
{
_remoteMotionByGuid.Remove(record.ServerGuid);
}
record.AnimationRuntime = null;
record.RemoteMotionRuntime = null;
record.PhysicsHost = null;
record.RequiresRemotePlacementRuntime = false;
record.PhysicsBody = null;
record.ProjectileRuntime = null;
record.EffectProfile = null;
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;
record.WorldSpawnPublished = false;
record.InitialHydrationCompleted = false;
record.CreateProjectionSynchronizationPending = false;
record.ProjectionHydrationInProgress = false;
record.ProjectionHydrationCreateVersion = 0UL;
record.ProjectionHydrationRetryRequested = false;
record.AppearanceProjectionSynchronizationPending = false;
record.AppearanceHydrationInProgress = false;
record.AppearanceHydrationVersion = 0UL;
record.AppearanceHydrationRetryRequested = false;
record.WorldEntity = null;
}
finally
{
record.TeardownInProgress = false;
}
}
}