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>
176 lines
6.1 KiB
C#
176 lines
6.1 KiB
C#
using AcDream.App.Interaction;
|
|
using AcDream.App.Net;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Session;
|
|
using AcDream.Runtime.World;
|
|
using AcDream.UI.Abstractions;
|
|
|
|
namespace AcDream.App.Runtime;
|
|
|
|
/// <summary>
|
|
/// 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,
|
|
IGameRuntimeCommands,
|
|
IRuntimeEventSource,
|
|
IDisposable
|
|
{
|
|
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(
|
|
GameRuntime runtime,
|
|
LiveSessionHost sessionHost,
|
|
ICommandBus commands,
|
|
SelectionInteractionController selection)
|
|
{
|
|
_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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
public IRuntimeCombatCommands Combat => _commands;
|
|
public IRuntimeMagicCommands Magic => _commands;
|
|
public IRuntimeMovementCommands MovementCommands => _commands;
|
|
IRuntimeMovementCommands IGameRuntimeCommands.Movement => _commands;
|
|
public IRuntimeChatCommands ChatCommands => _commands;
|
|
IRuntimeChatCommands IGameRuntimeCommands.Chat => _commands;
|
|
public IRuntimePortalCommands PortalCommands => _commands;
|
|
IRuntimePortalCommands IGameRuntimeCommands.Portal => _commands;
|
|
public IRuntimeInventoryStateCommands InventoryCommands => _commands;
|
|
IRuntimeInventoryStateCommands IGameRuntimeCommands.InventoryState =>
|
|
_commands;
|
|
public IRuntimeSpellbookCommands SpellbookCommands => _commands;
|
|
IRuntimeSpellbookCommands IGameRuntimeCommands.Spellbook => _commands;
|
|
public IRuntimeCharacterCommands CharacterCommands => _commands;
|
|
IRuntimeCharacterCommands IGameRuntimeCommands.Character => _commands;
|
|
public IRuntimeSocialCommands SocialCommands => _commands;
|
|
IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands;
|
|
|
|
public RuntimeStateCheckpoint CaptureCheckpoint()
|
|
{
|
|
RuntimeStateCheckpoint checkpoint = _runtime.CaptureCheckpoint();
|
|
return checkpoint with { Lifecycle = Lifecycle.State };
|
|
}
|
|
|
|
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()
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|