using AcDream.Core.Net.Messages; using AcDream.Core.Physics; namespace AcDream.App.World; /// /// 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(); 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 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; } _lastAcceptedByChild[relation.ChildGuid] = relation; _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(); } public void RemoveObject(uint guid) { _stagedByChild.Remove(guid); _recoveryByChild.Remove(guid); _lastAcceptedByChild.Remove(guid); _unresolvedByChild.Remove(guid); RemoveParentReferences(_stagedByChild, guid); RemoveParentReferences(_recoveryByChild, guid); RemoveParentReferences(_lastAcceptedByChild, 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) { FilterChildCandidates( guid, relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent); _stagedByChild.Remove(guid); _recoveryByChild.Remove(guid); _lastAcceptedByChild.Remove(guid); RemoveParentReferences(_stagedByChild, guid); RemoveParentReferences(_recoveryByChild, guid); RemoveParentReferences(_lastAcceptedByChild, 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) { FilterChildCandidates( guid, relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent); _stagedByChild.Remove(guid); _recoveryByChild.Remove(guid); _lastAcceptedByChild.Remove(guid); RemoveParentReferences(_stagedByChild, guid); RemoveParentReferences(_recoveryByChild, guid); RemoveParentReferences(_lastAcceptedByChild, 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); _lastAcceptedByChild.Remove(childGuid); } public void RemoveChild(uint childGuid) { _stagedByChild.Remove(childGuid); _recoveryByChild.Remove(childGuid); _lastAcceptedByChild.Remove(childGuid); _unresolvedByChild.Remove(childGuid); } public void Clear() { _unresolvedByChild.Clear(); _stagedByChild.Clear(); _recoveryByChild.Clear(); _lastAcceptedByChild.Clear(); } 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; } } 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, }