refactor(runtime): cut graphical host over to canonical root

Make every App composition phase borrow one GameRuntime, retire the duplicate view/event adapters, and dispose the root only after its graphical borrowers release. This preserves synchronous UI commands while giving shutdown one exact ownership ledger.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 19:06:09 +02:00
parent ecb9f79444
commit ce41efb9e5
29 changed files with 613 additions and 778 deletions

View file

@ -1,10 +1,6 @@
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
using AcDream.UI.Abstractions;
@ -12,9 +8,9 @@ using AcDream.UI.Abstractions;
namespace AcDream.App.Runtime;
/// <summary>
/// App composition root for Slice J's borrowed runtime boundary. Focused
/// adapters project the existing canonical owners; this root owns only those
/// adapters and never owns or copies gameplay state.
/// Synchronous graphical command adapter over one canonical
/// <see cref="GameRuntime"/>. It owns only its host lease and observer
/// subscriptions; every view and mutable owner is borrowed directly.
/// </summary>
internal sealed class CurrentGameRuntimeAdapter
: IGameRuntimeView,
@ -22,72 +18,80 @@ internal sealed class CurrentGameRuntimeAdapter
IRuntimeEventSource,
IDisposable
{
private readonly CurrentGameRuntimeViewAdapter _view;
private readonly CurrentGameRuntimeEventAdapter _events;
private readonly GameRuntime _runtime;
private readonly CurrentGameRuntimeCommandAdapter _commands;
private readonly IDisposable _hostLease;
private readonly object _subscriptionGate = new();
private readonly HashSet<AdapterSubscription> _subscriptions = [];
private bool _disposed;
public CurrentGameRuntimeAdapter(
LiveSessionController session,
GameRuntime runtime,
LiveSessionHost sessionHost,
ICommandBus commands,
LocalPlayerIdentityState playerIdentity,
RuntimeEntityObjectLifetime entityObjects,
RuntimeInventoryState inventory,
RuntimeCharacterState character,
RuntimeCommunicationState communication,
RuntimeActionState actions,
RuntimeLocalPlayerMovementState movement,
RuntimeWorldEnvironmentState environment,
RuntimeWorldTransitState worldTransit,
IGameRuntimeClock clock,
SelectionInteractionController selection)
{
_view = new CurrentGameRuntimeViewAdapter(
session,
playerIdentity,
entityObjects,
inventory,
character,
communication,
actions,
movement,
environment,
worldTransit,
clock);
entityObjects.BindEventContext(
() => _view.Generation,
() => clock.FrameNumber);
_events = new CurrentGameRuntimeEventAdapter(
_view,
entityObjects,
communication);
_commands = new CurrentGameRuntimeCommandAdapter(
session,
sessionHost,
commands,
_view,
inventory,
character,
actions,
movement,
selection,
_events);
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
ArgumentNullException.ThrowIfNull(sessionHost);
ArgumentNullException.ThrowIfNull(commands);
ArgumentNullException.ThrowIfNull(selection);
_hostLease = runtime.AcquireHostLease(
"graphical game-runtime command adapter");
try
{
_commands = new CurrentGameRuntimeCommandAdapter(
runtime.Session,
sessionHost,
commands,
this,
runtime.InventoryOwner,
runtime.CharacterOwner,
runtime.ActionOwner,
runtime.MovementOwner,
selection,
runtime.EventSink);
}
catch
{
_hostLease.Dispose();
throw;
}
}
public RuntimeGenerationToken Generation => _view.Generation;
public RuntimeLifecycleSnapshot Lifecycle => _view.Lifecycle;
public IGameRuntimeClock Clock => _view.Clock;
public IRuntimeEntityView Entities => _view.Entities;
public IRuntimeInventoryView Inventory => _view.Inventory;
public IRuntimeInventoryStateView InventoryState => _view.InventoryState;
public IRuntimeCharacterView Character => _view.Character;
public IRuntimeSocialView Social => _view.Social;
public IRuntimeChatView Chat => _view.Chat;
public IRuntimeActionView Actions => _view.Actions;
public IRuntimeMovementView Movement => _view.Movement;
public IRuntimeWorldEnvironmentView Environment => _view.Environment;
public IRuntimePortalView Portal => _view.Portal;
private bool IsActive =>
!_disposed
&& !_runtime.Session.IsDisposalComplete;
public RuntimeGenerationToken Generation => _runtime.Generation;
public RuntimeLifecycleSnapshot Lifecycle
{
get
{
RuntimeLifecycleSnapshot current = _runtime.Lifecycle;
return IsActive
? current
: current with
{
State = RuntimeLifecycleState.Disposed,
HasTransport = false,
};
}
}
public IGameRuntimeClock Clock => _runtime.Clock;
public IRuntimeEntityView Entities => _runtime.Entities;
public IRuntimeInventoryView Inventory => _runtime.Inventory;
public IRuntimeInventoryStateView InventoryState =>
_runtime.InventoryState;
public IRuntimeCharacterView Character => _runtime.Character;
public IRuntimeSocialView Social => _runtime.Social;
public IRuntimeChatView Chat => _runtime.Chat;
public IRuntimeActionView Actions => _runtime.Actions;
public IRuntimeMovementView Movement => _runtime.Movement;
public IRuntimeWorldEnvironmentView Environment => _runtime.Environment;
public IRuntimePortalView Portal => _runtime.Portal;
public IRuntimeSessionCommands Session => _commands;
public IRuntimeSelectionCommands Selection => _commands;
@ -109,18 +113,64 @@ internal sealed class CurrentGameRuntimeAdapter
public IRuntimeSocialCommands SocialCommands => _commands;
IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands;
public RuntimeStateCheckpoint CaptureCheckpoint() =>
_view.CaptureCheckpoint();
public RuntimeStateCheckpoint CaptureCheckpoint()
{
RuntimeStateCheckpoint checkpoint = _runtime.CaptureCheckpoint();
return checkpoint with { Lifecycle = Lifecycle.State };
}
public IDisposable Subscribe(IRuntimeEventObserver observer) =>
_events.Subscribe(observer);
public IDisposable Subscribe(IRuntimeEventObserver observer)
{
ArgumentNullException.ThrowIfNull(observer);
lock (_subscriptionGate)
{
ObjectDisposedException.ThrowIf(_disposed, this);
IDisposable runtimeSubscription = _runtime.Subscribe(observer);
var subscription = new AdapterSubscription(
this,
runtimeSubscription);
_subscriptions.Add(subscription);
return subscription;
}
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_view.Deactivate();
_events.Dispose();
lock (_subscriptionGate)
{
if (_disposed)
return;
_disposed = true;
while (_subscriptions.Count != 0)
_subscriptions.First().DisposeCore();
}
_hostLease.Dispose();
}
private void Remove(AdapterSubscription subscription)
{
lock (_subscriptionGate)
_subscriptions.Remove(subscription);
}
private sealed class AdapterSubscription(
CurrentGameRuntimeAdapter owner,
IDisposable runtimeSubscription) : IDisposable
{
private CurrentGameRuntimeAdapter? _owner = owner;
private IDisposable? _runtimeSubscription = runtimeSubscription;
public void Dispose() => DisposeCore();
public void DisposeCore()
{
IDisposable? runtime = Interlocked.Exchange(
ref _runtimeSubscription,
null);
if (runtime is null)
return;
runtime.Dispose();
Interlocked.Exchange(ref _owner, null)?.Remove(this);
}
}
}