using AcDream.App.Streaming; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; 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; } } /// Remote motion state owned by a live object. public interface ILiveEntityRemoteMotionRuntime { PhysicsBody Body { get; } } /// /// 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); } /// 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); } /// 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, or newer INSTANCE_TS ends it. /// public sealed class LiveEntityRecord { internal LiveEntityRecord(WorldSession.EntitySpawn snapshot) { ServerGuid = snapshot.Guid; Snapshot = snapshot; RefreshDerivedState(); } 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; } public PhysicsBody? PhysicsBody { get; internal set; } public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; } public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal 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; } public bool WorldSpawnPublished { get; internal set; } public LiveEntityProjectionKind ProjectionKind { get; internal set; } /// /// 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; FinalPhysicsState = (PhysicsStateFlags)RawPhysicsState; } } 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 Action? _tearDownRuntimeComponents; private readonly InboundPhysicsStateController _inbound = new(); private readonly Dictionary _recordsByGuid = 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(); private uint _rebucketingGuid; private uint _nextLocalEntityId; public LiveEntityRuntime( GpuWorldState spatial, ILiveEntityResourceLifecycle resources, Action? tearDownRuntimeComponents = null, uint firstLocalEntityId = FirstLiveEntityId) { _spatial = spatial ?? throw new ArgumentNullException(nameof(spatial)); _resources = resources ?? throw new ArgumentNullException(nameof(resources)); _tearDownRuntimeComponents = tearDownRuntimeComponents; 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 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 projections are excluded from radar, picking, status, and target /// candidate consumers. /// public IReadOnlyDictionary WorldEntities => _visibleWorldEntitiesByGuid; public IReadOnlyDictionary Snapshots => _inbound.Snapshots; public IReadOnlyDictionary AnimationRuntimes => _animationsByLocalId; public IReadOnlyDictionary RemoteMotionRuntimes => _remoteMotionByGuid; 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) { InboundCreateResult result = _inbound.AcceptCreate(incoming); if (result.Disposition is CreateObjectTimestampDisposition.StaleGeneration) return new LiveEntityRegistrationResult(result, null, false, false); if (result.Disposition is CreateObjectTimestampDisposition.ExistingGeneration) { if (_recordsByGuid.TryGetValue(incoming.Guid, out LiveEntityRecord? retained)) { retained.Snapshot = result.Snapshot; retained.RefreshDerivedState(); return new LiveEntityRegistrationResult(result, retained, 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); Exception? tearDownFailure = null; if (old is not null) { try { TearDownRecord(old); } catch (Exception error) { tearDownFailure = error; } } if (result.Disposition is CreateObjectTimestampDisposition.NewGeneration) ParentAttachments.EndGeneration(incoming.Guid, result.Snapshot.InstanceSequence); 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 (!_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; try { _resources.Register(entity); record.ResourcesRegistered = true; } catch (Exception registrationError) { // Registration is an atomic logical-lifetime boundary. Give // composite owners a symmetric rollback opportunity, then // remove the identity even if cleanup itself fails. try { _resources.Unregister(entity); } catch (Exception rollbackError) { throw new AggregateException( "Live entity resource registration and rollback both failed.", registrationError, rollbackError); } finally { _guidByLocalId.Remove(localId); record.WorldEntity = null; record.ResourcesRegistered = false; } throw; } } record.ProjectionKind = projectionKind; RebucketLiveEntity(serverGuid, fullCellId); return record.WorldEntity; } /// 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; // 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; _rebucketingGuid = serverGuid; try { _spatial.RebucketLiveEntity(entity, spatialCellOrLandblockId); } finally { _rebucketingGuid = 0; } bool visible = _spatial.IsLiveEntityVisible(serverGuid); record.IsSpatiallyVisible = visible; if (record.ProjectionKind is LiveEntityProjectionKind.World) _materializedWorldEntitiesByGuid[serverGuid] = entity; else _materializedWorldEntitiesByGuid.Remove(serverGuid); if (visible && record.ProjectionKind is LiveEntityProjectionKind.World) _visibleWorldEntitiesByGuid[serverGuid] = entity; else _visibleWorldEntitiesByGuid.Remove(serverGuid); if ((spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu) record.FullCellId = spatialCellOrLandblockId; record.CanonicalLandblockId = spatialCellOrLandblockId == 0 ? 0u : (spatialCellOrLandblockId & 0xFFFF0000u) | 0xFFFFu; if (!wasProjected || wasVisible != visible) ProjectionVisibilityChanged?.Invoke(record, visible); 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; _spatial.RemoveLiveEntityProjection(serverGuid); _materializedWorldEntitiesByGuid.Remove(serverGuid); _visibleWorldEntitiesByGuid.Remove(serverGuid); record.IsSpatiallyProjected = false; record.IsSpatiallyVisible = false; return true; } public bool UnregisterLiveEntity( DeleteObject.Parsed delete, bool isLocalPlayer, Action? beforeTeardown = null) { if (!_inbound.TryDelete(delete, isLocalPlayer)) return false; ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence); List? failures = null; try { beforeTeardown?.Invoke(); } catch (Exception error) { (failures ??= new List()).Add(error); } if (_recordsByGuid.Remove(delete.Guid, out LiveEntityRecord? record)) { try { TearDownRecord(record); } 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; } 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; } 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; return true; } 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.ProjectileRuntime is { } projectile && !ReferenceEquals(projectile.Body, runtime.Body)) { throw new InvalidOperationException( $"Live entity 0x{serverGuid:X8} already owns a different projectile physics body."); } record.RemoteMotionRuntime = runtime; record.PhysicsBody = runtime.Body; if (runtime is ILiveEntityCanonicalCellConsumer cellConsumer) { // Capture the incarnation, not only its GUID. A callback retained // by an old MovementManager must never mutate a replacement that // later reuses the same server GUID. LiveEntityRecord incarnation = record; cellConsumer.BindCanonicalCell( read: () => _recordsByGuid.TryGetValue(serverGuid, out var current) && ReferenceEquals(current, incarnation) && ReferenceEquals(current.RemoteMotionRuntime, runtime) ? current.FullCellId : 0u, write: cellId => { if (cellId != 0 && _recordsByGuid.TryGetValue(serverGuid, out var current) && ReferenceEquals(current, incarnation) && ReferenceEquals(current.RemoteMotionRuntime, runtime)) { RebucketLiveEntity(serverGuid, cellId); } }); } _remoteMotionByGuid[serverGuid] = runtime; } 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; if (record.ProjectileRuntime is null) record.PhysicsBody = null; 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.RemoteMotionRuntime is { } remote && !ReferenceEquals(remote.Body, runtime.Body)) { throw new InvalidOperationException( $"Live entity 0x{serverGuid:X8} already owns a different remote-motion physics body."); } if (record.ProjectileRuntime is { } projectile && !ReferenceEquals(projectile.Body, runtime.Body)) { throw new InvalidOperationException( $"Live entity 0x{serverGuid:X8} already owns a different projectile physics body."); } record.ProjectileRuntime = runtime; record.PhysicsBody = runtime.Body; } 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; if (record.RemoteMotionRuntime is null) record.PhysicsBody = null; 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); 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); 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); ClearWorldCell(update.ChildGuid); } 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); ClearWorldCell(update.ChildGuid); } return applied; } 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); 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); return applied; } public bool TryApplyState(SetState.Parsed update, out WorldSession.EntitySpawn accepted) { bool applied = _inbound.TryApplyState(update, out accepted); if (applied) RefreshRecord(update.Guid, accepted); return applied; } 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 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; RefreshRecord(update.Guid, snapshot, refreshPosition: acceptedPosition); if (acceptedPosition) { ParentAttachments.EndChildProjection(update.Guid); } } return known; } public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) => _inbound.IsFreshTeleportStart(localPlayerGuid, teleportSequence); /// /// Retail CPhysicsObj::update_object (0x00515D40) runs root /// movement/animation only while the object has visible world-cell /// membership and is not Frozen. 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 bool ShouldAdvanceRootRuntime(uint serverGuid) => _recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record) && record.IsSpatiallyProjected && record.IsSpatiallyVisible && record.FullCellId != 0 && (record.FinalPhysicsState & PhysicsStateFlags.Frozen) == 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; } public void Clear() { List? failures = null; foreach (LiveEntityRecord record in _recordsByGuid.Values.ToArray()) { try { TearDownRecord(record); } catch (Exception error) { (failures ??= new List()).Add(error); } } _recordsByGuid.Clear(); _materializedWorldEntitiesByGuid.Clear(); _visibleWorldEntitiesByGuid.Clear(); _guidByLocalId.Clear(); _animationsByLocalId.Clear(); _remoteMotionByGuid.Clear(); ParentAttachments.Clear(); _inbound.Clear(); if (failures is not null) throw new AggregateException("One or more live entities failed session teardown.", failures); } 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 void ClearWorldCell(uint guid) { if (!_recordsByGuid.TryGetValue(guid, out LiveEntityRecord? record)) return; record.FullCellId = 0; record.CanonicalLandblockId = 0; } 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) { if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record) || record.WorldEntity is not { } entity) return; record.IsSpatiallyVisible = visible; if (visible && record.ProjectionKind is LiveEntityProjectionKind.World) _visibleWorldEntitiesByGuid[serverGuid] = entity; else _visibleWorldEntitiesByGuid.Remove(serverGuid); if (_rebucketingGuid != serverGuid) ProjectionVisibilityChanged?.Invoke(record, visible); } private void TearDownRecord(LiveEntityRecord record) { List? failures = null; void RunCleanup(Action cleanup) { try { cleanup(); } catch (Exception error) { (failures ??= new List()).Add(error); } } if (_tearDownRuntimeComponents is not null) RunCleanup(() => _tearDownRuntimeComponents(record)); if (record.WorldEntity is { } entity) { RunCleanup(() => _spatial.RemoveLiveEntityProjection(record.ServerGuid)); if (record.ResourcesRegistered) RunCleanup(() => _resources.Unregister(entity)); _guidByLocalId.Remove(entity.Id); _materializedWorldEntitiesByGuid.Remove(record.ServerGuid); _visibleWorldEntitiesByGuid.Remove(record.ServerGuid); _animationsByLocalId.Remove(entity.Id); } _remoteMotionByGuid.Remove(record.ServerGuid); record.AnimationRuntime = null; record.RemoteMotionRuntime = null; record.PhysicsBody = null; record.ProjectileRuntime = null; record.EffectProfile = null; record.ResourcesRegistered = false; record.IsSpatiallyProjected = false; record.IsSpatiallyVisible = false; record.WorldSpawnPublished = false; record.WorldEntity = null; if (failures is not null) throw new AggregateException( $"Live entity 0x{record.ServerGuid:X8} teardown failed.", failures); } }