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);
}
}