refactor(app): compose content effects and audio startup
Move Phase-2 startup behind typed composition/publication boundaries, replace the GameWindow-capturing PhysicsScript gate with a focused deferred source, and own animation-hook registrations reversibly. Make OpenAL construction and teardown transactional so every device, context, source, and buffer prefix is retryable without replay.
This commit is contained in:
parent
1d51e35c14
commit
60a1698ce7
12 changed files with 1901 additions and 152 deletions
105
src/AcDream.App/Composition/AnimationHookRegistrationSet.cs
Normal file
105
src/AcDream.App/Composition/AnimationHookRegistrationSet.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Lifetime owner for animation-hook router edges. Registration order remains
|
||||
/// retail-observable dispatch order; teardown runs in reverse and retains only
|
||||
/// unregister operations that actually failed.
|
||||
/// </summary>
|
||||
internal sealed class AnimationHookRegistrationSet : IDisposable,
|
||||
IRetryableResourceCleanup
|
||||
{
|
||||
private sealed class Entry(IAnimationHookSink sink)
|
||||
{
|
||||
public IAnimationHookSink Sink { get; } = sink;
|
||||
public bool Removed { get; set; }
|
||||
}
|
||||
|
||||
private readonly Action<IAnimationHookSink> _register;
|
||||
private readonly Action<IAnimationHookSink> _unregister;
|
||||
private readonly List<Entry> _entries = [];
|
||||
private bool _disposing;
|
||||
private bool _closed;
|
||||
|
||||
public AnimationHookRegistrationSet(AnimationHookRouter router)
|
||||
: this(
|
||||
(router ?? throw new ArgumentNullException(nameof(router))).Register,
|
||||
router.Unregister)
|
||||
{
|
||||
}
|
||||
|
||||
internal AnimationHookRegistrationSet(
|
||||
Action<IAnimationHookSink> register,
|
||||
Action<IAnimationHookSink> unregister)
|
||||
{
|
||||
_register = register ?? throw new ArgumentNullException(nameof(register));
|
||||
_unregister = unregister ?? throw new ArgumentNullException(nameof(unregister));
|
||||
}
|
||||
|
||||
public bool IsCleanupComplete =>
|
||||
_entries.All(static entry => entry.Removed);
|
||||
|
||||
public int ActiveCount =>
|
||||
_entries.Count(static entry => !entry.Removed);
|
||||
|
||||
public void Register(IAnimationHookSink sink)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sink);
|
||||
ObjectDisposedException.ThrowIf(_closed, this);
|
||||
if (_entries.Any(entry =>
|
||||
!entry.Removed && ReferenceEquals(entry.Sink, sink)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_register(sink);
|
||||
_entries.Add(new Entry(sink));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_closed = true;
|
||||
RetryCleanup();
|
||||
}
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
if (_disposing || IsCleanupComplete)
|
||||
return;
|
||||
|
||||
_closed = true;
|
||||
_disposing = true;
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
for (int i = _entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Entry entry = _entries[i];
|
||||
if (entry.Removed)
|
||||
continue;
|
||||
try
|
||||
{
|
||||
_unregister(entry.Sink);
|
||||
entry.Removed = true;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Animation-hook sink '{entry.Sink.GetType().Name}' could not be unregistered.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposing = false;
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"Animation-hook registration cleanup remains incomplete.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue