using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; namespace AcDream.Runtime.Entities; /// /// Update-thread state machine for parent relations that may arrive before /// either CreateObject. Unaccepted wire events remain in arrival order; /// committed rollback history contains only relations that passed both the /// canonical retail timestamp gate and retail PartArray::add_child /// validation. /// public sealed class ParentAttachmentState { private readonly Dictionary> _unresolvedByChild = new(); private readonly Dictionary _stagedByChild = new(); private readonly Dictionary _recoveryByChild = new(); private readonly Dictionary _lastAcceptedByChild = new(); private readonly Dictionary> _committedChildrenByParent = new(); private readonly Dictionary> _deferredCreatesByParent = []; private ulong _nextDeferredCreateAdmissionId; public int UnresolvedRelationCount => _unresolvedByChild.Values.Sum(queue => queue.Count); public int StagedRelationCount => _stagedByChild.Count; public int RecoveryRelationCount => _recoveryByChild.Count; public int CommittedRelationCount => _lastAcceptedByChild.Count; internal int DeferredCreateCount => _deferredCreatesByParent.Values.Sum(queue => queue.Count); /// /// Retains the complete unaccepted CreateObject packet when its nonzero /// parent is not addressable. Retail queues the raw blob before object /// lookup and before any child timestamp is consumed; storing a decoded /// relation after AcceptCreate would partially admit the child. /// internal void EnqueueDeferredCreate( WorldSession.EntitySpawn spawn, bool isLocalPlayer) { uint parentGuid = spawn.ParentGuid ?? spawn.Physics?.Parent?.Guid ?? 0u; if (spawn.Guid == 0u || parentGuid == 0u) { throw new ArgumentException( "A deferred parent CreateObject requires nonzero child and parent GUIDs.", nameof(spawn)); } if (_nextDeferredCreateAdmissionId == ulong.MaxValue) { throw new InvalidOperationException( "The deferred parent CreateObject admission sequence is exhausted."); } if (!_deferredCreatesByParent.TryGetValue( parentGuid, out Queue? queue)) { queue = new Queue(); _deferredCreatesByParent.Add(parentGuid, queue); } ulong admissionId = _nextDeferredCreateAdmissionId + 1UL; queue.Enqueue(new DeferredParentCreate( admissionId, RuntimeInitialCreateAdmissionFreezer.Freeze(spawn), isLocalPlayer)); _nextDeferredCreateAdmissionId = admissionId; } internal bool TryPeekDeferredCreate( uint parentGuid, out DeferredParentCreate deferred) { if (!_deferredCreatesByParent.TryGetValue( parentGuid, out Queue? queue) || !queue.TryPeek(out deferred)) { deferred = default; return false; } return true; } internal bool ConsumeDeferredCreate( uint parentGuid, in DeferredParentCreate expected) { if (!_deferredCreatesByParent.TryGetValue( parentGuid, out Queue? queue) || !queue.TryPeek(out DeferredParentCreate current) || current != expected) { return false; } _ = queue.Dequeue(); if (queue.Count == 0) _deferredCreatesByParent.Remove(parentGuid); return true; } internal bool ContainsDeferredCreate( uint childGuid, ushort instanceSequence) { foreach (Queue queue in _deferredCreatesByParent.Values) { if (queue.Any(candidate => candidate.Spawn.Guid == childGuid && candidate.Spawn.InstanceSequence == instanceSequence)) { return true; } } return false; } /// /// Cancels only the raw, still-unaccepted child generation addressed by a /// terminal packet. Instance zero is a normal retail timestamp and is not /// treated as an empty sentinel. /// internal void CancelDeferredChildGeneration( uint childGuid, ushort terminalInstanceSequence) => FilterDeferredCreates( candidate => candidate.Spawn.Guid != childGuid || PhysicsTimestampGate.IsNewer( terminalInstanceSequence, candidate.Spawn.InstanceSequence)); public void AcceptCreateObjectRelation(ParentAttachmentRelation relation) { _stagedByChild[relation.ChildGuid] = relation; } public void Enqueue(ParentEvent.Parsed update) { if (!_unresolvedByChild.TryGetValue(update.ChildGuid, out Queue? queue)) { queue = new Queue(); _unresolvedByChild.Add(update.ChildGuid, queue); } queue.Enqueue(new ParentAttachmentRelation( update.ParentGuid, update.ChildGuid, update.ParentLocation, update.PlacementId, update.ParentInstanceSequence, update.ChildPositionSequence)); } /// /// Resolves every now-addressable relation for one child in wire order. /// A future parent generation remains queued; an older generation or a /// rejected child POSITION_TS is discarded. Every accepted relation /// supersedes the preceding render projection. /// public void Resolve( uint childGuid, Func isObjectKnown, Func resolveInstance, Func accept) { if (_stagedByChild.ContainsKey(childGuid)) return; if (!_unresolvedByChild.TryGetValue(childGuid, out Queue? queue)) return; int candidateCount = queue.Count; for (int i = 0; i < candidateCount; i++) { ParentAttachmentRelation relation = queue.Dequeue(); ushort? parentInstance = resolveInstance(relation.ParentGuid); if (parentInstance is null) { queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent }); continue; } if (parentInstance.Value != relation.ParentInstanceSequence) { // Current parent newer than the packet: discard the stale // relation. Packet newer than current: retain it until that // parent generation is constructed. if (PhysicsTimestampGate.IsNewer( relation.ParentInstanceSequence, parentInstance.Value)) { continue; } queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent }); continue; } if (!isObjectKnown(childGuid)) { queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Child }); continue; } var update = new ParentEvent.Parsed( relation.ParentGuid, relation.ChildGuid, relation.ParentLocation, relation.PlacementId, relation.ParentInstanceSequence, relation.ChildPositionSequence); if (!accept(update)) continue; _stagedByChild[childGuid] = relation with { WaitOwner = ParentAttachmentWaitOwner.Unknown, }; break; } if (queue.Count == 0) _unresolvedByChild.Remove(childGuid); } public bool TryGetProjection(uint childGuid, out ParentAttachmentRelation relation) => _stagedByChild.TryGetValue(childGuid, out relation) || _recoveryByChild.TryGetValue(childGuid, out relation); public bool TryGetStagedProjection( uint childGuid, out ParentAttachmentRelation relation) => _stagedByChild.TryGetValue(childGuid, out relation); public bool TryGetRecoveryProjection( uint childGuid, out ParentAttachmentRelation relation) => _recoveryByChild.TryGetValue(childGuid, out relation); public void CopyPendingProjectionChildrenTo(List destination) { ArgumentNullException.ThrowIfNull(destination); destination.Clear(); foreach (uint childGuid in _stagedByChild.Keys) destination.Add(childGuid); foreach (uint childGuid in _recoveryByChild.Keys) { if (!_stagedByChild.ContainsKey(childGuid)) destination.Add(childGuid); } } public bool IsCommitted(ParentAttachmentRelation relation) => _lastAcceptedByChild.TryGetValue( relation.ChildGuid, out ParentAttachmentRelation committed) && committed == relation; public bool HasCommittedParent(uint childGuid) => _lastAcceptedByChild.ContainsKey(childGuid); public bool IsPending( ParentAttachmentRelation relation, ParentProjectionCandidateKind kind) => (kind is ParentProjectionCandidateKind.Staged ? _stagedByChild : _recoveryByChild).TryGetValue( relation.ChildGuid, out ParentAttachmentRelation pending) && pending == relation; public void MarkProjected( ParentAttachmentRelation relation, ParentProjectionCandidateKind kind) { Dictionary source = kind is ParentProjectionCandidateKind.Staged ? _stagedByChild : _recoveryByChild; if (source.TryGetValue( relation.ChildGuid, out ParentAttachmentRelation pending) && pending == relation) { source.Remove(relation.ChildGuid); } } public bool CommitProjection(ParentAttachmentRelation relation) { if (!_stagedByChild.TryGetValue( relation.ChildGuid, out ParentAttachmentRelation staged) || staged != relation) { return false; } RemoveCommittedChild(relation.ChildGuid); _lastAcceptedByChild[relation.ChildGuid] = relation; var parent = new ParentIncarnation( relation.ParentGuid, relation.ParentInstanceSequence); if (!_committedChildrenByParent.TryGetValue( parent, out List? children)) { children = []; _committedChildrenByParent.Add(parent, children); } children.Add(relation.ChildGuid); _stagedByChild.Remove(relation.ChildGuid); _recoveryByChild[relation.ChildGuid] = relation; return true; } public void RejectProjection(ParentAttachmentRelation relation) { if (_stagedByChild.TryGetValue( relation.ChildGuid, out ParentAttachmentRelation staged) && staged == relation) { _stagedByChild.Remove(relation.ChildGuid); } } public bool RestoreLastAccepted(uint childGuid) { if (!_lastAcceptedByChild.TryGetValue(childGuid, out ParentAttachmentRelation relation)) return false; _recoveryByChild[childGuid] = relation; return true; } public IReadOnlyList ChildrenWaitingForParent(uint parentGuid) { var result = new HashSet(); foreach ((uint childGuid, ParentAttachmentRelation relation) in _stagedByChild) { if (relation.ParentGuid == parentGuid) result.Add(childGuid); } foreach ((uint childGuid, ParentAttachmentRelation relation) in _recoveryByChild) { if (relation.ParentGuid == parentGuid) result.Add(childGuid); } foreach ((uint childGuid, Queue queue) in _unresolvedByChild) { if (queue.Any(relation => relation.ParentGuid == parentGuid)) result.Add(childGuid); } return result.ToArray(); } /// /// Returns only the exact direct children currently committed to one /// parent incarnation. Lost-cell destruction follows retail's live /// CHILDLIST and must not capture staged, unresolved, or future-generation /// relations that merely reuse the same parent GUID. /// public IReadOnlyList ChildrenAttachedToParent( uint parentGuid, ushort parentInstanceSequence) { var parent = new ParentIncarnation(parentGuid, parentInstanceSequence); return _committedChildrenByParent.TryGetValue( parent, out List? children) ? children : Array.Empty(); } public void RemoveObject(uint guid) { RemoveDeferredChildCreates(guid); _stagedByChild.Remove(guid); _recoveryByChild.Remove(guid); RemoveCommittedChild(guid); _unresolvedByChild.Remove(guid); RemoveParentReferences(_stagedByChild, guid); RemoveParentReferences(_recoveryByChild, guid); RemoveCommittedParentReferences(guid); uint[] children = _unresolvedByChild.Keys.ToArray(); for (int i = 0; i < children.Length; i++) { uint childGuid = children[i]; Queue retained = new( _unresolvedByChild[childGuid].Where(relation => relation.ParentGuid != guid)); if (retained.Count == 0) _unresolvedByChild.Remove(childGuid); else _unresolvedByChild[childGuid] = retained; } } /// /// Ends one logical incarnation without discarding unresolved wire events /// that may explicitly address the replacement/future parent generation. /// public void EndGeneration(uint guid, ushort replacementGeneration) { FilterDeferredCreates(candidate => candidate.Spawn.Guid != guid || candidate.Spawn.InstanceSequence == replacementGeneration || PhysicsTimestampGate.IsNewer( replacementGeneration, candidate.Spawn.InstanceSequence)); FilterChildCandidates( guid, relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent); _stagedByChild.Remove(guid); _recoveryByChild.Remove(guid); RemoveCommittedChild(guid); RemoveParentReferences(_stagedByChild, guid); RemoveParentReferences(_recoveryByChild, guid); RemoveCommittedParentReferences(guid); FilterParentCandidates( guid, relation => relation.ParentInstanceSequence == replacementGeneration || PhysicsTimestampGate.IsNewer( replacementGeneration, relation.ParentInstanceSequence)); } /// /// Deletes one exact incarnation. Child-addressed candidates die with the /// child; candidates addressed to a strictly newer parent incarnation /// survive for retail's post-Create blob replay. /// public void DeleteGeneration(uint guid, ushort deletedGeneration) { CancelDeferredChildGeneration(guid, deletedGeneration); FilterChildCandidates( guid, relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent); _stagedByChild.Remove(guid); _recoveryByChild.Remove(guid); RemoveCommittedChild(guid); RemoveParentReferences(_stagedByChild, guid); RemoveParentReferences(_recoveryByChild, guid); RemoveCommittedParentReferences(guid); FilterParentCandidates( guid, relation => PhysicsTimestampGate.IsNewer( deletedGeneration, relation.ParentInstanceSequence)); } /// /// Clears an accepted attachment projection after Pickup or a world /// Position without destroying fresher unresolved ParentEvents. /// public void EndChildProjection(uint childGuid) { _stagedByChild.Remove(childGuid); _recoveryByChild.Remove(childGuid); RemoveCommittedChild(childGuid); } public void RemoveChild(uint childGuid) { RemoveDeferredChildCreates(childGuid); _stagedByChild.Remove(childGuid); _recoveryByChild.Remove(childGuid); RemoveCommittedChild(childGuid); _unresolvedByChild.Remove(childGuid); } public void Clear() { _deferredCreatesByParent.Clear(); _unresolvedByChild.Clear(); _stagedByChild.Clear(); _recoveryByChild.Clear(); _lastAcceptedByChild.Clear(); foreach (List children in _committedChildrenByParent.Values) children.Clear(); _committedChildrenByParent.Clear(); } private void RemoveDeferredChildCreates(uint childGuid) => FilterDeferredCreates( candidate => candidate.Spawn.Guid != childGuid); private void FilterDeferredCreates( Func retain) { uint[] parents = _deferredCreatesByParent.Keys.ToArray(); for (int index = 0; index < parents.Length; index++) { uint parentGuid = parents[index]; Queue retained = new( _deferredCreatesByParent[parentGuid].Where(retain)); if (retained.Count == 0) _deferredCreatesByParent.Remove(parentGuid); else _deferredCreatesByParent[parentGuid] = retained; } } private void RemoveCommittedChild(uint childGuid) { if (!_lastAcceptedByChild.Remove( childGuid, out ParentAttachmentRelation relation)) { return; } var parent = new ParentIncarnation( relation.ParentGuid, relation.ParentInstanceSequence); if (!_committedChildrenByParent.TryGetValue( parent, out List? children)) { return; } int childIndex = children.IndexOf(childGuid); if (childIndex >= 0) { int lastIndex = children.Count - 1; children[childIndex] = children[lastIndex]; children.RemoveAt(lastIndex); } if (children.Count == 0) _committedChildrenByParent.Remove(parent); } private void RemoveCommittedParentReferences(uint parentGuid) { uint[] children = _lastAcceptedByChild .Where(pair => pair.Value.ParentGuid == parentGuid) .Select(pair => pair.Key) .ToArray(); for (int i = 0; i < children.Length; i++) RemoveCommittedChild(children[i]); } private static void RemoveParentReferences( Dictionary relations, uint parentGuid) { uint[] children = relations .Where(pair => pair.Value.ParentGuid == parentGuid) .Select(pair => pair.Key) .ToArray(); for (int i = 0; i < children.Length; i++) relations.Remove(children[i]); } private void FilterParentCandidates( uint parentGuid, Func retain) { uint[] children = _unresolvedByChild.Keys.ToArray(); for (int i = 0; i < children.Length; i++) { uint childGuid = children[i]; Queue queue = _unresolvedByChild[childGuid]; var retained = new Queue( queue.Where(relation => relation.ParentGuid != parentGuid || retain(relation))); if (retained.Count == 0) _unresolvedByChild.Remove(childGuid); else _unresolvedByChild[childGuid] = retained; } } private void FilterChildCandidates( uint childGuid, Func retain) { if (!_unresolvedByChild.TryGetValue( childGuid, out Queue? queue)) { return; } var retained = new Queue(queue.Where(retain)); if (retained.Count == 0) _unresolvedByChild.Remove(childGuid); else _unresolvedByChild[childGuid] = retained; } private readonly record struct ParentIncarnation( uint ServerGuid, ushort InstanceSequence); } internal readonly record struct DeferredParentCreate( ulong AdmissionId, WorldSession.EntitySpawn Spawn, bool IsLocalPlayer) { internal bool IsValid => AdmissionId != 0UL && Spawn.Guid != 0u; } public readonly record struct ParentAttachmentRelation( uint ParentGuid, uint ChildGuid, uint ParentLocation, uint PlacementId, ushort ParentInstanceSequence, ushort ChildPositionSequence, ParentAttachmentWaitOwner WaitOwner = ParentAttachmentWaitOwner.Unknown); public enum ParentAttachmentWaitOwner { Unknown, Parent, Child, } public enum ParentProjectionCandidateKind { Staged, Recovery, }