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;
///
/// 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.
///
public interface ILiveEntityAnimationRuntime
{
WorldEntity Entity { get; }
uint CurrentMotion { get; }
}
/// Remote motion state owned by a live object.
public interface ILiveEntityRemoteMotionRuntime
{
PhysicsBody Body { get; }
}
///
/// Optional seam for a remote-motion component that reads the exact
/// incarnation's canonical movement-manager host from
/// .
///
public interface ILiveEntityPhysicsHostConsumer
{
void BindPhysicsHost(Func read);
}
///
/// 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.
///
public interface ILiveEntityCanonicalRuntimeConsumer
{
void BindCanonicalRuntime(
Func readPhysicsHost,
Func readCell,
Action writeCell);
}
///
/// Optional seam for a remote-motion component that consumes the canonical
/// full-cell identity owned by . The runtime
/// binds this for every production remote, regardless of whether projectile
/// classification arrives before or after the MovementManager.
///
public interface ILiveEntityCanonicalCellConsumer
{
void BindCanonicalCell(Func read, Action write);
}
///
/// 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.
///
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();
}
/// Projectile physics state owned by a live object.
public interface ILiveEntityProjectileRuntime
{
PhysicsBody Body { get; }
}
/// Marker for the DAT-driven effect profile added by the effect phase.
public interface ILiveEntityEffectProfile { }
public enum LiveEntityProjectionKind
{
World,
Attached,
}
///
/// Logical-resource seam coordinated by .
/// Spatial bucketing is deliberately absent: registering or removing meshes,
/// scripts, and effects is a logical-lifetime operation, not a landblock move.
///
public interface ILiveEntityResourceLifecycle
{
void Register(WorldEntity entity);
void Unregister(WorldEntity entity);
}
///
/// 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 .
///
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 tearDown) : ILiveEntityRuntimeComponentLifecycle
{
private readonly Action _tearDown =
tearDown ?? throw new ArgumentNullException(nameof(tearDown));
public void TearDown(LiveEntityRecord record) => _tearDown(record);
}
///
/// 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.
///
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);
}
}
/// Delegate adapter used by the App composition root.
public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle
{
private readonly Action _register;
private readonly Action _unregister;
public DelegateLiveEntityResourceLifecycle(
Action register,
Action 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);
}
///
/// 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.
///
public sealed class LiveEntityRecord
{
private readonly Queue _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; }
///
/// Incarnation-stable retail CPhysicsObj::update_time owner. It
/// survives animation and MovementManager attachment/replacement, spatial
/// rebucketing, and appearance rehydration, and dies with this record.
///
public RetailObjectQuantumClock ObjectClock { get; } = new();
///
/// Changes whenever this record leaves or re-enters the ordinary-object
/// workset. Unlike , 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.
///
internal ulong ObjectClockEpoch { get; private set; }
///
/// True after this incarnation has successfully constructed its retail
/// CPartArray. 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.
///
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; }
///
/// 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.
///
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; }
///
/// 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.
///
internal ulong CreateIntegrationVersion { get; private set; } = 1UL;
public bool WorldSpawnPublished { get; internal set; }
///
/// 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
/// alone is not sufficient: failures after
/// resource registration retain the same projection for exact retry.
///
public bool InitialHydrationCompleted { get; internal set; }
///
/// 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.
///
internal bool CreateProjectionSynchronizationPending { get; set; }
internal bool ProjectionHydrationInProgress { get; set; }
internal ulong ProjectionHydrationCreateVersion { get; set; }
internal bool ProjectionHydrationRetryRequested { get; set; }
///
/// 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.
///
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;
}
///
/// 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.
///
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);
///
/// Update-thread owner of live server-object identity, accepted state, runtime
/// components, and logical teardown. is a spatial
/// projection only; moving between loaded and pending buckets never replays a
/// create-time renderer, script, animation, or effect action.
///
///
/// 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 2026-07-13-retail-projectile-vfx-pseudocode.md,
/// CPhysicsObj::set_description (0x00514F40),
/// CPhysicsObj::change_cell (0x00513390), and
/// CPhysicsObj::exit_world (0x00514E60).
///
///
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 _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 _materializedWorldEntitiesByGuid = new();
private readonly Dictionary _visibleWorldEntitiesByGuid = new();
private readonly Dictionary _guidByLocalId = new();
private readonly Dictionary _animationsByLocalId = new();
private readonly Dictionary _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 _spatialAnimationsByLocalId = new();
private readonly Dictionary _spatialRemoteMotionByGuid = new();
private readonly Dictionary _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 _spatialRootObjectsByGuid = new();
private bool _isClearing;
private bool _sessionClearPendingFinalization;
private bool _isRegisteringResources;
private int _logicalTeardownDepth;
private ulong _sessionLifetimeVersion;
private readonly Dictionary _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 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 Records => _recordsByGuid.Values;
///
/// 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.
///
public IReadOnlyDictionary MaterializedWorldEntities =>
_materializedWorldEntitiesByGuid;
///
/// 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.
///
public IReadOnlyDictionary WorldEntities =>
_visibleWorldEntitiesByGuid;
public IReadOnlyDictionary Snapshots => _inbound.Snapshots;
public IReadOnlyDictionary AnimationRuntimes => _animationsByLocalId;
public IReadOnlyDictionary RemoteMotionRuntimes => _remoteMotionByGuid;
public IReadOnlyDictionary SpatialAnimationRuntimes =>
_spatialAnimationsByLocalId;
public IReadOnlyDictionary SpatialRemoteMotionRuntimes =>
_spatialRemoteMotionByGuid;
internal IReadOnlyDictionary SpatialProjectileRuntimes =>
_spatialProjectilesByGuid;
internal IReadOnlyDictionary SpatialRootObjects =>
_spatialRootObjectsByGuid;
public ParentAttachmentState ParentAttachments { get; } = new();
///
/// Raised after a materialized projection enters or leaves the currently
/// loaded spatial view. Logical resources remain owned by the record.
///
public event Action? 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);
}
///
/// Completes logical registration when a renderable projection can be
/// hydrated. The factory is called at most once for the incarnation.
///
public WorldEntity? MaterializeLiveEntity(
uint serverGuid,
uint fullCellId,
Func 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;
}
/// Changes spatial buckets only. No logical resource callbacks run.
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;
}
///
/// Removes only the render-bucket reference. The logical record and every
/// create-time resource remain alive for later projection/rebucketing.
///
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;
}
///
/// 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.
///
public bool WithdrawLiveEntityProjection(LiveEntityRecord expectedRecord)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
return WithdrawLiveEntityProjection(
expectedRecord,
expectedRecord.PositionAuthorityVersion,
expectedRecord.ProjectionMutationVersion);
}
///
/// 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.
///
internal bool WithdrawLiveEntityProjection(
LiveEntityRecord expectedRecord,
ulong expectedPositionAuthorityVersion,
ulong expectedProjectionMutationVersion)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
return IsCurrentRecord(expectedRecord)
&& expectedRecord.PositionAuthorityVersion == expectedPositionAuthorityVersion
&& expectedRecord.ProjectionMutationVersion == expectedProjectionMutationVersion
&& WithdrawLiveEntityProjection(expectedRecord.ServerGuid);
}
///
/// Withdraws a still-live projection whose authoritative rollback frame
/// is outside every world cell. Logical owners remain registered.
///
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? failures = null;
_logicalTeardownDepth++;
try
{
if (!retryingAcceptedDelete)
{
try
{
beforeTeardown?.Invoke();
}
catch (Exception error)
{
(failures ??= new List()).Add(error);
}
}
if (record is not null)
{
try
{
TearDownRecord(record);
ReleaseTeardownRecord(record);
}
catch (Exception error)
{
(failures ??= new List()).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()).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;
}
///
/// 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.
///
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;
}
///
/// 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.
///
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;
}
///
/// Acquires the one retail CPhysicsObj 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.
///
internal PhysicsBody GetOrCreatePhysicsBody(
uint serverGuid,
Func 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 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 readCell = () => incarnation.FullCellId;
Action 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;
}
///
/// Commits retail CPhysicsObj::set_parent'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.
///
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;
}
///
/// Retail parent Hidden directly toggles each attached child's NoDraw bit
/// without consuming the child's STATE_TS or replacing its raw wire state.
///
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);
///
/// Retail CPhysicsObj::update_object (0x00515D40) 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.
///
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;
///
/// Copies the canonical loaded root-object workset used by retail
/// CPhysics::UseTime. The record reference is the incarnation token;
/// callers must revalidate it after callbacks that can delete or rebucket.
///
internal void CopySpatialRootObjectRecordsTo(List 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);
///
/// Copies the currently spatial animation owners through the concrete
/// dictionary enumerator. This keeps per-frame traversal allocation-free;
/// exposing the workset as
/// would box its enumerator at a hot call site.
///
internal void CopySpatialAnimationRuntimesTo(
List> 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(localId, typed));
}
}
}
internal void CopySpatialAnimationLocalIdsTo(HashSet destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach (uint localId in _spatialAnimationsByLocalId.Keys)
destination.Add(localId);
}
///
/// Host side of retail CPhysicsObj::set_active(1) for a movement
/// manager owned by this exact incarnation. Static is a required no-op.
///
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;
}
///
/// Commits retail CPhysicsObj::set_velocity (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.
///
internal bool TryCommitAuthoritativeVelocity(
LiveEntityRecord record,
PhysicsBody body,
Vector3 velocity,
double currentTime) =>
TryCommitAuthoritativeVectorCore(
record,
body,
velocity,
angularVelocity: null,
currentTime);
///
/// Commits the paired retail VectorUpdate writes
/// (set_velocity, then set_omega) to one canonical body.
/// Validation is atomic: malformed wire vectors cannot partially mutate
/// the live object.
///
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;
}
///
/// 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.
///
internal void CopySpatialRemoteMotionRecordsTo(List 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);
///
/// 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.
///
internal void CopySpatialProjectileRecordsTo(List 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;
///
/// 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.
///
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? 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()).Add(error);
}
}
CompleteSessionClearIfConverged();
}
finally
{
_isClearing = false;
}
if (failures is not null)
throw new AggregateException("One or more live entities failed session teardown.", failures);
}
///
/// 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.
///
public int RetryPendingTeardowns()
{
if (_teardownRecords.Count == 0
|| _isClearing
|| _isRegisteringResources
|| _logicalTeardownDepth != 0)
return 0;
int completed = 0;
List? 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()).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();
List? failures = null;
for (int i = 0; i < subscribers.Length; i++)
{
try
{
((Action)subscribers[i])(record, visible);
}
catch (Exception error)
{
(failures ??= new List()).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? failures = null;
bool TryCleanup(Action cleanup)
{
try
{
cleanup();
return true;
}
catch (Exception error)
{
(failures ??= new List()).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;
}
}
}