refactor(app): complete session startup composition

Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 18:49:31 +02:00
parent 7fa60971e2
commit 826f9ea9b5
22 changed files with 924 additions and 412 deletions

View file

@ -10,7 +10,9 @@ namespace AcDream.App.Composition;
/// </summary>
internal sealed class LivePresentationRuntimeBindings : IDisposable
{
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
private sealed record Entry(string Name, IDisposable Binding);
private readonly List<Entry> _bindings = [];
private bool _deactivationStarted;
public void Adopt(string name, IDisposable binding)
@ -18,7 +20,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(binding);
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
_bindings.Add((name, binding));
_bindings.Add(new Entry(name, binding));
}
public IDisposable AdoptOwned(string name, IDisposable binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(binding);
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
var entry = new Entry(name, binding);
_bindings.Add(entry);
return new Adoption(this, entry);
}
public void AdoptRelease(string name, Action release) =>
@ -56,16 +68,16 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
List<Exception>? failures = null;
for (int i = _bindings.Count - 1; i >= 0; i--)
{
(string name, IDisposable binding) = _bindings[i];
Entry entry = _bindings[i];
try
{
binding.Dispose();
entry.Binding.Dispose();
_bindings.RemoveAt(i);
}
catch (Exception failure)
{
(failures ??= []).Add(new InvalidOperationException(
$"Live-presentation binding '{name}' did not detach.",
$"Live-presentation binding '{entry.Name}' did not detach.",
failure));
}
}
@ -78,6 +90,38 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
}
}
private void ReleaseAdoption(Entry expected)
{
int index = _bindings.IndexOf(expected);
if (index < 0)
return;
expected.Binding.Dispose();
_bindings.RemoveAt(index);
}
private sealed class Adoption : IDisposable
{
private LivePresentationRuntimeBindings? _owner;
private readonly Entry _expected;
public Adoption(
LivePresentationRuntimeBindings owner,
Entry expected)
{
_owner = owner;
_expected = expected;
}
public void Dispose()
{
LivePresentationRuntimeBindings? owner = _owner;
if (owner is null)
return;
owner.ReleaseAdoption(_expected);
_owner = null;
}
}
private sealed class DelegateBinding : IDisposable
{
private Action? _release;