namespace AcDream.App.World; /// /// Runs the independent owners that participate in one live incarnation's /// teardown without letting an earlier callback strand later state. Retail's /// object destruction drains these owners as separate lifecycle steps; the App /// composition callback must preserve that symmetry even when a plugin, /// renderer, or effect sink throws. /// internal static class LiveEntityTeardown { internal static void Run(IEnumerable cleanups) { ArgumentNullException.ThrowIfNull(cleanups); List? failures = null; foreach (Action cleanup in cleanups) { try { cleanup(); } catch (Exception error) { (failures ??= []).Add(error); } } if (failures is not null) throw new AggregateException( "One or more live-entity component teardown steps failed.", failures); } } /// /// A stable teardown plan whose successful steps are committed individually. /// The plan itself stays on the live-record tombstone, so retrying a later /// failed owner never replays an earlier ExitWorld notification or release. /// internal sealed class LiveEntityTeardownPlan { private readonly Action[] _steps; private readonly bool[] _completed; public LiveEntityTeardownPlan(IEnumerable steps) { ArgumentNullException.ThrowIfNull(steps); _steps = [.. steps]; _completed = new bool[_steps.Length]; } internal int CompletedCount => _completed.Count(static completed => completed); internal bool IsComplete => CompletedCount == _steps.Length; public void Advance() { List? failures = null; for (int i = 0; i < _steps.Length; i++) { if (_completed[i]) continue; try { _steps[i](); _completed[i] = true; } catch (Exception error) { (failures ??= []).Add(error); } } if (failures is not null) throw new AggregateException( "One or more live-entity component teardown steps failed.", failures); } }