using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime.Physics; namespace AcDream.Runtime.Entities; /// /// The single update-thread authority for live server GUIDs, incarnations, /// accepted wire snapshots, teardown tombstones, and runtime-local identity. /// It owns no graphical state and can run without loading App or a backend. /// public sealed class RuntimeEntityDirectory { public const uint FirstLocalEntityId = 1_000_000u; public const uint LastLocalEntityId = 0x3FFF_FFFFu; private readonly InboundPhysicsStateController _inbound = new(); private readonly Dictionary _activeByGuid = new(); private readonly Dictionary<(uint Guid, ushort Incarnation), RuntimeEntityRecord> _teardownByIncarnation = new(); private readonly Dictionary _byLocalId = new(); private readonly Dictionary _lifetimeMutationByGuid = new(); private uint _nextLocalEntityId; public RuntimeEntityDirectory(uint firstLocalEntityId = FirstLocalEntityId) { if (firstLocalEntityId is < FirstLocalEntityId or > LastLocalEntityId) { throw new ArgumentOutOfRangeException( nameof(firstLocalEntityId), $"Live entity ids must stay in 0x{FirstLocalEntityId:X8}..0x{LastLocalEntityId:X8}."); } _nextLocalEntityId = firstLocalEntityId; } public int Count => _activeByGuid.Count; public int PendingTeardownCount => _teardownByIncarnation.Count; public int ClaimedLocalIdCount => _byLocalId.Count; public ulong SessionLifetimeVersion { get; private set; } public IReadOnlyCollection ActiveRecords => _activeByGuid.Values; public IReadOnlyCollection TeardownRecords => _teardownByIncarnation.Values; public IReadOnlyDictionary Snapshots => _inbound.Snapshots; public ParentAttachmentState ParentAttachments { get; } = new(); public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming) => _inbound.AcceptCreate(incoming); internal InboundCreateResult AcceptCreateDeferredSameGeneration( WorldSession.EntitySpawn incoming) => _inbound.AcceptCreateDeferredSameGeneration(incoming); public CreateObjectTimestampDisposition PreviewCreateDisposition( WorldSession.EntitySpawn incoming) => _inbound.PreviewCreateDisposition(incoming); public bool TryDelete( AcDream.Core.Net.Messages.DeleteObject.Parsed delete, bool isLocalPlayer) => _inbound.TryDelete(delete, isLocalPlayer); public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) => _inbound.TryGetSnapshot(guid, out spawn); internal bool TryGetAcceptedTimestamps( uint guid, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryGetAcceptedTimestamps(guid, out timestamps); public bool TryGetActive(uint guid, out RuntimeEntityRecord record) => _activeByGuid.TryGetValue(guid, out record!); public bool IsCurrent(RuntimeEntityRecord record) => _activeByGuid.TryGetValue(record.ServerGuid, out RuntimeEntityRecord? current) && ReferenceEquals(current, record); public bool TryGetByLocalId(uint localEntityId, out RuntimeEntityRecord record) => _byLocalId.TryGetValue(localEntityId, out record!); public bool TryGetTeardown( uint guid, ushort incarnation, out RuntimeEntityRecord record) => _teardownByIncarnation.TryGetValue((guid, incarnation), out record!); public RuntimeEntityRecord AddActive(WorldSession.EntitySpawn snapshot) { if (_activeByGuid.ContainsKey(snapshot.Guid)) { throw new InvalidOperationException( $"Live entity 0x{snapshot.Guid:X8} already has an active incarnation."); } var record = new RuntimeEntityRecord(snapshot); _activeByGuid.Add(snapshot.Guid, record); try { ClaimLocalId(record); return record; } catch { _activeByGuid.Remove(snapshot.Guid); throw; } } public bool RemoveActive(uint guid, out RuntimeEntityRecord? record) => _activeByGuid.Remove(guid, out record); public bool RemoveActive(RuntimeEntityRecord expected) { if (!IsCurrent(expected)) return false; return _activeByGuid.Remove(expected.ServerGuid); } public void RetainTeardown(RuntimeEntityRecord record) { var key = (record.ServerGuid, record.Incarnation); if (_teardownByIncarnation.TryGetValue( key, out RuntimeEntityRecord? retained) && !ReferenceEquals(retained, record)) { throw new InvalidOperationException( $"Live entity teardown tombstone collision for 0x{record.ServerGuid:X8} generation {record.Incarnation}."); } _teardownByIncarnation[key] = record; } public void ReleaseTeardown(RuntimeEntityRecord record) { var key = (record.ServerGuid, record.Incarnation); if (_teardownByIncarnation.TryGetValue( key, out RuntimeEntityRecord? retained) && ReferenceEquals(retained, record)) { _teardownByIncarnation.Remove(key); } } public bool HasPendingTeardown(uint serverGuid) { foreach ((uint Guid, ushort Incarnation) key in _teardownByIncarnation.Keys) { if (key.Guid == serverGuid) return true; } return false; } public uint ClaimLocalId(RuntimeEntityRecord record) { if (!IsKnown(record)) { throw new InvalidOperationException( "A local id can only be claimed for an active or retained incarnation."); } if (record.LocalEntityId is { } existing) return existing; uint start = _nextLocalEntityId; do { uint candidate = _nextLocalEntityId; _nextLocalEntityId = candidate == LastLocalEntityId ? FirstLocalEntityId : candidate + 1u; if (_byLocalId.ContainsKey(candidate)) continue; _byLocalId.Add(candidate, record); record.LocalEntityId = candidate; return candidate; } while (_nextLocalEntityId != start); throw new InvalidOperationException("The live entity id namespace is exhausted."); } public bool ReleaseLocalId(RuntimeEntityRecord record) { if (record.LocalEntityId is not { } localId) return false; if (_byLocalId.TryGetValue(localId, out RuntimeEntityRecord? retained) && ReferenceEquals(retained, record)) { _byLocalId.Remove(localId); } record.LocalEntityId = null; return true; } public ulong AdvanceLifetimeMutation(uint serverGuid) { ulong next = _lifetimeMutationByGuid.GetValueOrDefault(serverGuid) + 1UL; _lifetimeMutationByGuid[serverGuid] = next; return next; } public ulong CurrentLifetimeMutation(uint serverGuid) => _lifetimeMutationByGuid.GetValueOrDefault(serverGuid); public void BeginSessionClear() { SessionLifetimeVersion++; _lifetimeMutationByGuid.Clear(); ParentAttachments.Clear(); _inbound.Clear(); } public bool CompleteSessionClearIfConverged() { if (_activeByGuid.Count != 0 || _teardownByIncarnation.Count != 0) return false; _byLocalId.Clear(); ParentAttachments.Clear(); _inbound.Clear(); return true; } public void RefreshSnapshot( RuntimeEntityRecord record, WorldSession.EntitySpawn accepted, bool refreshPosition = false) { EnsureKnown(record); record.Snapshot = accepted; record.RefreshDerivedState(refreshPosition); } public void AdvanceCreateAuthority(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvanceCreateAuthority(); } public void AdvancePositionAuthority(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvancePositionAuthority(); } public void AdvanceVectorAuthority(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvanceVectorAuthority(); } public void AdvanceMovementAuthority(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvanceMovementAuthority(); } public void AdvanceMovementCommit(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvanceMovementCommit(); } public void AdvancePlacementCommit(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvancePlacementCommit(); } public void AdvanceParentCommit(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvanceParentCommit(); } public void AdvanceObjDescAuthority(RuntimeEntityRecord record) { EnsureKnown(record); record.AdvanceObjDescAuthority(); } public RetailPhysicsStateTransition ApplyRawPhysicsState( RuntimeEntityRecord record, uint rawState) { EnsureKnown(record); return record.ApplyRawPhysicsState(rawState); } public bool TryDequeueStateTransition( RuntimeEntityRecord record, out RetailPhysicsStateTransition transition) { EnsureKnown(record); return record.TryDequeueStateTransition(out transition); } public void SetChildNoDraw(RuntimeEntityRecord record, bool noDraw) { EnsureKnown(record); record.SetChildNoDraw(noDraw); } public void SuspendObjectClock(RuntimeEntityRecord record) { EnsureKnown(record); record.SuspendObjectClock(); } public void ResetObjectClockForEnterWorld(RuntimeEntityRecord record, bool isStatic) { EnsureKnown(record); record.ResetObjectClockForEnterWorld(isStatic); } public void SetFullCell( RuntimeEntityRecord record, uint fullCellId, uint canonicalLandblockId) { EnsureKnown(record); record.SetFullCell(fullCellId, canonicalLandblockId); } public void SetFinalPhysicsState( RuntimeEntityRecord record, PhysicsStateFlags state) { EnsureKnown(record); record.SetFinalPhysicsState(state); } internal bool StopMissileAfterCollision( RuntimeEntityRecord record, bool requireCurrentMissile) { EnsureKnown(record); return record.StopMissileAfterCollision(requireCurrentMissile); } public void SetHasPartArray(RuntimeEntityRecord record, bool value) { EnsureKnown(record); record.HasPartArray = value; } public void SetPhysicsBody( RuntimeEntityRecord record, AcDream.Core.Physics.PhysicsBody? body) { EnsureKnown(record); record.SetPhysicsBody(body); } public void SetPhysicsBodyAcquisitionInProgress( RuntimeEntityRecord record, bool value) { EnsureKnown(record); record.PhysicsBodyAcquisitionInProgress = value; } public void SetRemoteMotion( RuntimeEntityRecord record, IRuntimeRemoteMotion? remote) { EnsureKnown(record); record.RemoteMotion = remote; } public void SetRemoteMotionBindingInProgress( RuntimeEntityRecord record, bool value) { EnsureKnown(record); record.RemoteMotionBindingInProgress = value; } public void SetProjectile( RuntimeEntityRecord record, IRuntimeProjectile? projectile) { EnsureKnown(record); record.Projectile = projectile; } public void SetProjectileBindingInProgress( RuntimeEntityRecord record, bool value) { EnsureKnown(record); record.ProjectileBindingInProgress = value; } public void SetRequiresRemotePlacementRuntime( RuntimeEntityRecord record, bool value) { EnsureKnown(record); record.RequiresRemotePlacementRuntime = value; } public void SetPhysicsHost( RuntimeEntityRecord record, AcDream.Core.Physics.Motion.IPhysicsObjHost? host) { EnsureKnown(record); record.PhysicsHost = host; } public void SetDeleteAcceptedForTeardown( RuntimeEntityRecord record, bool value) { EnsureKnown(record); record.DeleteAcceptedForTeardown = value; } public bool TryApplyObjDesc( AcDream.Core.Net.Messages.ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted) => _inbound.TryApplyObjDesc(update, out accepted); public bool TryApplyPickup( AcDream.Core.Net.Messages.PickupEvent.Parsed update, out WorldSession.EntitySpawn accepted) => _inbound.TryApplyPickup(update, out accepted); public bool TryApplyCreateParent( CreateParentUpdate update, out WorldSession.EntitySpawn accepted) => _inbound.TryApplyCreateParent(update, out accepted); public bool TryApplyParent( AcDream.Core.Net.Messages.ParentEvent.Parsed update, out WorldSession.EntitySpawn accepted) => _inbound.TryApplyParent(update, out accepted); public bool TryCommitParent( uint childGuid, uint parentGuid, uint parentLocation, uint placementId, ushort positionSequence, out WorldSession.EntitySpawn accepted) => _inbound.TryCommitParent( childGuid, parentGuid, parentLocation, placementId, positionSequence, out accepted); public bool TryApplyMotion( WorldSession.EntityMotionUpdate update, bool retainPayload, out WorldSession.EntitySpawn accepted, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryApplyMotion(update, retainPayload, out accepted, out timestamps); public bool TryApplyVector( AcDream.Core.Net.Messages.VectorUpdate.Parsed update, out WorldSession.EntitySpawn accepted) => _inbound.TryApplyVector(update, out accepted); public bool TryApplyState( AcDream.Core.Net.Messages.SetState.Parsed update, out WorldSession.EntitySpawn accepted) => _inbound.TryApplyState(update, out accepted); 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) => _inbound.TryApplyPosition( update, isLocalPlayer, forcePositionRotation, currentLocalVelocity, out disposition, out accepted, out timestamps); internal bool TryAcceptDeferredPosition( WorldSession.EntityPositionUpdate update, bool isLocalPlayer, out PositionTimestampDisposition disposition, out AcceptedPhysicsTimestamps timestamps, out bool hasTimestampMutation) => _inbound.TryAcceptDeferredPosition( update, isLocalPlayer, out disposition, out timestamps, out hasTimestampMutation); internal bool TryAcceptDeferredObjDesc( ObjDescEvent.Parsed update, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryAcceptDeferredObjDesc(update, out timestamps); internal bool TryAcceptDeferredPickup( PickupEvent.Parsed update, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryAcceptDeferredPickup(update, out timestamps); internal bool TryAcceptDeferredCreateParent( CreateParentUpdate update, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryAcceptDeferredCreateParent(update, out timestamps); internal bool TryAcceptDeferredParent( ParentEvent.Parsed update, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryAcceptDeferredParent(update, out timestamps); internal bool TryAcceptDeferredMotion( WorldSession.EntityMotionUpdate update, out AcceptedPhysicsTimestamps timestamps, out bool hasTimestampMutation) => _inbound.TryAcceptDeferredMotion( update, out timestamps, out hasTimestampMutation); internal bool TryAcceptDeferredState( SetState.Parsed update, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryAcceptDeferredState(update, out timestamps); internal bool TryAcceptDeferredVector( VectorUpdate.Parsed update, out AcceptedPhysicsTimestamps timestamps) => _inbound.TryAcceptDeferredVector(update, out timestamps); public bool IsFreshTeleportStart(uint guid, ushort teleportSequence) => _inbound.IsFreshTeleportStart(guid, teleportSequence); private bool IsKnown(RuntimeEntityRecord record) { if (IsCurrent(record)) return true; return _teardownByIncarnation.TryGetValue( (record.ServerGuid, record.Incarnation), out RuntimeEntityRecord? retained) && ReferenceEquals(retained, record); } private void EnsureKnown(RuntimeEntityRecord record) { ArgumentNullException.ThrowIfNull(record); if (!IsKnown(record)) { throw new InvalidOperationException( $"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} is no longer owned by the directory."); } } }