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; } } }