acdream/src/AcDream.App/Composition/SessionPlayerRuntimeBindings.cs
Erik 7771c07fb6 refactor(app): compose session and player startup
Move streaming, live-session, hydration, local-player, combat, and teleport construction behind the typed Phase-7 boundary. Add exact-owner runtime bindings and focused spawn-claim classification so partial startup rolls back without retaining old session targets while preserving the accepted construction and frame dependencies.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 18:28:32 +02:00

95 lines
2.8 KiB
C#

using AcDream.App.Rendering;
using AcDream.App.World;
namespace AcDream.App.Composition;
/// <summary>
/// Named Phase-7 external edges. Releases run in reverse order; completed
/// edges are removed immediately so an incomplete cleanup retries only the
/// remaining work.
/// </summary>
internal sealed class SessionPlayerRuntimeBindings : IDisposable
{
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
private bool _deactivationStarted;
public void Adopt(string name, IDisposable binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(binding);
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
_bindings.Add((name, binding));
}
public void BindEntityReady(
EquippedChildRenderController source,
Action<LiveEntityReadyCandidate> handler)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(handler);
source.EntityReady += handler;
Adopt(
"equipped-child entity ready",
new DelegateBinding(() => source.EntityReady -= handler));
}
public void BindAppearanceApplied(
LiveEntityHydrationController source,
Action<uint> handler)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(handler);
source.AppearanceApplied += handler;
Adopt(
"live appearance applied",
new DelegateBinding(() => source.AppearanceApplied -= handler));
}
public void Dispose()
{
if (_deactivationStarted && _bindings.Count == 0)
return;
_deactivationStarted = true;
List<Exception>? failures = null;
for (int i = _bindings.Count - 1; i >= 0; i--)
{
(string name, IDisposable binding) = _bindings[i];
try
{
binding.Dispose();
_bindings.RemoveAt(i);
}
catch (Exception failure)
{
(failures ??= []).Add(new InvalidOperationException(
$"Session/player binding '{name}' did not detach.",
failure));
}
}
if (failures is not null)
{
throw new AggregateException(
"Session/player binding cleanup remains incomplete.",
failures);
}
}
private sealed class DelegateBinding : IDisposable
{
private Action? _release;
public DelegateBinding(Action release) =>
_release = release ?? throw new ArgumentNullException(nameof(release));
public void Dispose()
{
Action? release = _release;
if (release is null)
return;
release();
_release = null;
}
}
}