namespace AcDream.App.Rendering; /// /// Reusable parent-before-child traversal for retail's /// CPhysicsObj::UpdateChildrenInternal attachment graph. Failed parent /// projections suppress their descendants for the same frame; callers remove /// those projections only after traversal, so dictionary enumeration remains /// stable. /// internal sealed class AttachmentUpdateOrder { private readonly HashSet _visiting = new(); private readonly HashSet _completed = new(); private readonly HashSet _failedSet = new(); private readonly List _failed = new(); private readonly List _subtree = new(); private readonly Queue _recoveryQueue = new(); private readonly HashSet _recoveryVisited = new(); /// /// Updates the current attachment map without per-frame delegate or /// enumerator boxing. The returned list is borrowed until the next call. /// public IReadOnlyList ForEachParentFirst( Dictionary children, Func parentOf, Func update) { ArgumentNullException.ThrowIfNull(children); ArgumentNullException.ThrowIfNull(parentOf); ArgumentNullException.ThrowIfNull(update); _visiting.Clear(); _completed.Clear(); _failedSet.Clear(); _failed.Clear(); foreach (uint childId in children.Keys) Visit(childId, children, parentOf, update); return _failed; } /// /// Returns descendants before ancestors so an attachment subtree can be /// withdrawn without leaving a child rooted at a stale intermediate pose. /// The returned list is borrowed until the next call. /// public IReadOnlyList CollectSubtreePostOrder( Dictionary children, uint rootId, Func parentOf) { _subtree.Clear(); _visiting.Clear(); Collect(rootId, children, parentOf); return _subtree; } /// /// Retries every waiting child after a parent pose becomes available. /// Each successfully realized child becomes a parent candidate in turn, /// so A→B→C recovers in one parent-first drain. /// public void RealizeDescendants( uint parentId, Func> childrenWaitingForParent, Func realize) { ArgumentNullException.ThrowIfNull(childrenWaitingForParent); ArgumentNullException.ThrowIfNull(realize); _recoveryQueue.Clear(); _recoveryVisited.Clear(); _recoveryQueue.Enqueue(parentId); _recoveryVisited.Add(parentId); while (_recoveryQueue.Count > 0) { uint currentParent = _recoveryQueue.Dequeue(); IReadOnlyList waiting = childrenWaitingForParent(currentParent); for (int i = 0; i < waiting.Count; i++) { uint childId = waiting[i]; if (realize(childId) && _recoveryVisited.Add(childId)) _recoveryQueue.Enqueue(childId); } } } private void Collect( uint rootId, Dictionary children, Func parentOf) { if (!_visiting.Add(rootId)) return; foreach ((uint candidateId, TChild child) in children) { if (parentOf(child) == rootId) Collect(candidateId, children, parentOf); } _subtree.Add(rootId); } private void Visit( uint childId, Dictionary children, Func parentOf, Func update) { if (_completed.Contains(childId) || !children.TryGetValue(childId, out TChild? child)) { return; } if (!_visiting.Add(childId)) return; uint? parentId = parentOf(child); if (parentId is { } attachedParentId && children.ContainsKey(attachedParentId)) Visit(attachedParentId, children, parentOf, update); _visiting.Remove(childId); if (!_completed.Add(childId)) return; bool succeeded = parentId is not { } parent || !_failedSet.Contains(parent); if (succeeded) succeeded = update(childId); if (!succeeded && _failedSet.Add(childId)) _failed.Add(childId); } }