refactor(runtime): compose canonical game runtime root
This commit is contained in:
parent
75f9510e10
commit
96ddd16539
6 changed files with 1585 additions and 0 deletions
658
src/AcDream.Runtime/GameRuntime.cs
Normal file
658
src/AcDream.Runtime/GameRuntime.cs
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
public sealed record GameRuntimeDependencies(
|
||||
IRuntimeCombatAttackOperations CombatAttackOperations,
|
||||
IRuntimeCombatTargetOperations CombatTargetOperations,
|
||||
IRuntimeCombatModeOperations CombatModeOperations,
|
||||
IRuntimeSpellCastOperations SpellCastOperations,
|
||||
TimeProvider? TimeProvider = null,
|
||||
Action<string>? Log = null,
|
||||
Action<string>? TimeSyncDiagnostic = null,
|
||||
ILiveSessionOperations? SessionOperations = null,
|
||||
Func<double>? CombatTime = null,
|
||||
uint FirstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||
int MaximumChatEntries = 500);
|
||||
|
||||
[Flags]
|
||||
public enum GameRuntimeTeardownStage
|
||||
{
|
||||
None = 0,
|
||||
HostLeasesReleased = 1 << 0,
|
||||
EventsDetached = 1 << 1,
|
||||
SessionDisposed = 1 << 2,
|
||||
TransitReset = 1 << 3,
|
||||
ActionsDisposed = 1 << 4,
|
||||
MovementDisposed = 1 << 5,
|
||||
CharacterDisposed = 1 << 6,
|
||||
InventoryDisposed = 1 << 7,
|
||||
CommunicationDisposed = 1 << 8,
|
||||
IdentityDisposed = 1 << 9,
|
||||
EntityObjectsDisposed = 1 << 10,
|
||||
Complete =
|
||||
HostLeasesReleased
|
||||
| EventsDetached
|
||||
| SessionDisposed
|
||||
| TransitReset
|
||||
| ActionsDisposed
|
||||
| MovementDisposed
|
||||
| CharacterDisposed
|
||||
| InventoryDisposed
|
||||
| CommunicationDisposed
|
||||
| IdentityDisposed
|
||||
| EntityObjectsDisposed,
|
||||
}
|
||||
|
||||
public readonly record struct GameRuntimeOwnershipSnapshot(
|
||||
bool IsDisposeRequested,
|
||||
bool IsDisposeDrainActive,
|
||||
bool IsDisposed,
|
||||
int HostLeaseCount,
|
||||
GameRuntimeTeardownStage CompletedTeardownStages,
|
||||
LiveSessionOwnershipSnapshot Session,
|
||||
RuntimeLocalPlayerIdentityOwnershipSnapshot PlayerIdentity,
|
||||
RuntimeSimulationOwnershipSnapshot Simulation,
|
||||
RuntimeWorldEnvironmentOwnershipSnapshot Environment,
|
||||
RuntimeWorldTransitOwnershipSnapshot Transit,
|
||||
GameRuntimeEventOwnershipSnapshot Events)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& IsDisposeRequested
|
||||
&& !IsDisposeDrainActive
|
||||
&& HostLeaseCount == 0
|
||||
&& CompletedTeardownStages == GameRuntimeTeardownStage.Complete
|
||||
&& Session.IsConverged
|
||||
&& PlayerIdentity.IsConverged
|
||||
&& Simulation.IsConverged
|
||||
&& Transit.IsSessionIdle
|
||||
&& Events.IsConverged;
|
||||
}
|
||||
|
||||
internal enum GameRuntimeConstructionPoint
|
||||
{
|
||||
ClockCreated,
|
||||
SessionCreated,
|
||||
PlayerIdentityCreated,
|
||||
EntityObjectsCreated,
|
||||
InventoryCreated,
|
||||
CharacterCreated,
|
||||
CommunicationCreated,
|
||||
MovementCreated,
|
||||
ActionsCreated,
|
||||
EnvironmentCreated,
|
||||
TransitCreated,
|
||||
EventsCreated,
|
||||
}
|
||||
|
||||
internal sealed class GameRuntimeConstructionContext
|
||||
{
|
||||
public LiveSessionController? Session { get; set; }
|
||||
public RuntimeLocalPlayerIdentityState? PlayerIdentity { get; set; }
|
||||
public RuntimeEntityObjectLifetime? EntityObjects { get; set; }
|
||||
public RuntimeInventoryState? Inventory { get; set; }
|
||||
public RuntimeCharacterState? Character { get; set; }
|
||||
public RuntimeCommunicationState? Communication { get; set; }
|
||||
public RuntimeLocalPlayerMovementState? Movement { get; set; }
|
||||
public RuntimeActionState? Actions { get; set; }
|
||||
public GameRuntimeEventHub? Events { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One instance-scoped, presentation-independent ownership root for the
|
||||
/// complete live client kernel. Graphical and direct hosts borrow this exact
|
||||
/// object graph; they never reconstruct or copy its mutable state.
|
||||
/// </summary>
|
||||
public sealed class GameRuntime
|
||||
: IGameRuntimeView,
|
||||
IRuntimeEventSource,
|
||||
IDisposable
|
||||
{
|
||||
private const int TeardownStageCount = 11;
|
||||
|
||||
private readonly object _lifetimeGate = new();
|
||||
private readonly Dictionary<long, string> _hostLeases = [];
|
||||
private readonly GameRuntimeEventHub _events;
|
||||
private long _nextHostLeaseId;
|
||||
private int _disposeStage;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposeDrainActive;
|
||||
private bool _disposed;
|
||||
|
||||
public GameRuntime(GameRuntimeDependencies dependencies)
|
||||
: this(dependencies, faultInjection: null)
|
||||
{
|
||||
}
|
||||
|
||||
internal GameRuntime(
|
||||
GameRuntimeDependencies dependencies,
|
||||
Action<GameRuntimeConstructionPoint, GameRuntimeConstructionContext>?
|
||||
faultInjection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dependencies);
|
||||
ArgumentNullException.ThrowIfNull(
|
||||
dependencies.CombatAttackOperations);
|
||||
ArgumentNullException.ThrowIfNull(
|
||||
dependencies.CombatTargetOperations);
|
||||
ArgumentNullException.ThrowIfNull(
|
||||
dependencies.CombatModeOperations);
|
||||
ArgumentNullException.ThrowIfNull(
|
||||
dependencies.SpellCastOperations);
|
||||
if (dependencies.MaximumChatEntries <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(dependencies.MaximumChatEntries));
|
||||
}
|
||||
|
||||
var context = new GameRuntimeConstructionContext();
|
||||
var construction = new ConstructionTransaction();
|
||||
try
|
||||
{
|
||||
var clock = new GameRuntimeClock();
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.ClockCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Session = dependencies.SessionOperations is null
|
||||
? new LiveSessionController()
|
||||
: new LiveSessionController(dependencies.SessionOperations);
|
||||
construction.Own(context.Session);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.SessionCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.PlayerIdentity = new RuntimeLocalPlayerIdentityState();
|
||||
construction.Own(context.PlayerIdentity);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.PlayerIdentityCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.EntityObjects = new RuntimeEntityObjectLifetime(
|
||||
dependencies.FirstLocalEntityId);
|
||||
construction.Own(context.EntityObjects);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.EntityObjectsCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Inventory = new RuntimeInventoryState(
|
||||
context.EntityObjects);
|
||||
construction.Own(context.Inventory);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.InventoryCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Character = new RuntimeCharacterState();
|
||||
construction.Own(context.Character);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.CharacterCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Communication = new RuntimeCommunicationState(
|
||||
dependencies.MaximumChatEntries);
|
||||
construction.Own(context.Communication);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.CommunicationCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Movement = new RuntimeLocalPlayerMovementState();
|
||||
construction.Own(context.Movement);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.MovementCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Actions = new RuntimeActionState(
|
||||
context.Inventory.Transactions,
|
||||
context.Character.Spellbook,
|
||||
dependencies.CombatAttackOperations,
|
||||
dependencies.CombatTargetOperations,
|
||||
dependencies.CombatModeOperations,
|
||||
dependencies.SpellCastOperations,
|
||||
dependencies.CombatTime);
|
||||
construction.Own(context.Actions);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.ActionsCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
var environment = new RuntimeWorldEnvironmentState(
|
||||
dependencies.TimeProvider,
|
||||
dependencies.Log,
|
||||
dependencies.TimeSyncDiagnostic);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.EnvironmentCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
var transit = new RuntimeWorldTransitState(dependencies.Log);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.TransitCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.EntityObjects.BindEventContext(
|
||||
() => context.Session.Generation,
|
||||
() => clock.FrameNumber);
|
||||
context.Events = new GameRuntimeEventHub(
|
||||
context.EntityObjects,
|
||||
context.Communication);
|
||||
construction.Own(context.Events);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.EventsCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
Clock = clock;
|
||||
Session = context.Session;
|
||||
PlayerIdentity = context.PlayerIdentity;
|
||||
EntityObjects = context.EntityObjects;
|
||||
InventoryOwner = context.Inventory;
|
||||
CharacterOwner = context.Character;
|
||||
CommunicationOwner = context.Communication;
|
||||
MovementOwner = context.Movement;
|
||||
ActionOwner = context.Actions;
|
||||
EnvironmentOwner = environment;
|
||||
TransitOwner = transit;
|
||||
_events = context.Events;
|
||||
construction.Complete();
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
construction.RollbackAndThrow(failure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
public GameRuntimeClock Clock { get; }
|
||||
public LiveSessionController Session { get; }
|
||||
public RuntimeLocalPlayerIdentityState PlayerIdentity { get; }
|
||||
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||
public RuntimeInventoryState InventoryOwner { get; }
|
||||
public RuntimeCharacterState CharacterOwner { get; }
|
||||
public RuntimeCommunicationState CommunicationOwner { get; }
|
||||
public RuntimeActionState ActionOwner { get; }
|
||||
public RuntimeLocalPlayerMovementState MovementOwner { get; }
|
||||
public RuntimeWorldEnvironmentState EnvironmentOwner { get; }
|
||||
public RuntimeWorldTransitState TransitOwner { get; }
|
||||
|
||||
public RuntimeGenerationToken Generation => Session.Generation;
|
||||
|
||||
public RuntimeLifecycleSnapshot Lifecycle
|
||||
{
|
||||
get
|
||||
{
|
||||
RuntimeLifecycleState state;
|
||||
if (_disposed)
|
||||
state = RuntimeLifecycleState.Disposed;
|
||||
else if (Session.IsInWorld)
|
||||
state = RuntimeLifecycleState.InWorld;
|
||||
else if (Session.CurrentSession is not null)
|
||||
state = RuntimeLifecycleState.Starting;
|
||||
else if (Session.SessionGeneration == 0UL)
|
||||
state = RuntimeLifecycleState.Constructed;
|
||||
else
|
||||
state = RuntimeLifecycleState.Stopped;
|
||||
|
||||
return new RuntimeLifecycleSnapshot(
|
||||
Generation,
|
||||
state,
|
||||
PlayerIdentity.ServerGuid,
|
||||
Session.CurrentSession is not null);
|
||||
}
|
||||
}
|
||||
|
||||
IGameRuntimeClock IGameRuntimeView.Clock => Clock;
|
||||
public IRuntimeEntityView Entities => EntityObjects.EntityView;
|
||||
public IRuntimeInventoryView Inventory => EntityObjects.InventoryView;
|
||||
public IRuntimeInventoryStateView InventoryState => InventoryOwner.View;
|
||||
public IRuntimeCharacterView Character => CharacterOwner.View;
|
||||
public IRuntimeSocialView Social => CommunicationOwner.SocialView;
|
||||
public IRuntimeChatView Chat => CommunicationOwner.View;
|
||||
public IRuntimeActionView Actions => ActionOwner.View;
|
||||
public IRuntimeMovementView Movement => MovementOwner.View;
|
||||
public IRuntimeWorldEnvironmentView Environment => EnvironmentOwner;
|
||||
public IRuntimePortalView Portal => TransitOwner;
|
||||
|
||||
internal IGameRuntimeEventSink EventSink => _events;
|
||||
|
||||
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||
new(
|
||||
Generation,
|
||||
Lifecycle.State,
|
||||
Clock.FrameNumber,
|
||||
Entities.Count,
|
||||
Entities.MaterializedCount,
|
||||
Inventory.ObjectCount,
|
||||
Inventory.ContainerCount,
|
||||
InventoryState.Snapshot,
|
||||
Character.Snapshot,
|
||||
Social.Snapshot,
|
||||
Chat.Revision,
|
||||
Chat.Count,
|
||||
Actions.Snapshot,
|
||||
Movement.Snapshot,
|
||||
Environment.Snapshot,
|
||||
Environment.Ownership,
|
||||
Portal.Snapshot,
|
||||
Portal.Ownership);
|
||||
|
||||
public IDisposable Subscribe(IRuntimeEventObserver observer)
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(
|
||||
_disposeRequested || _disposed,
|
||||
this);
|
||||
return _events.Subscribe(observer);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable AcquireHostLease(string name)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
lock (_lifetimeGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(
|
||||
_disposeRequested || _disposed,
|
||||
this);
|
||||
long id = checked(++_nextHostLeaseId);
|
||||
_hostLeases.Add(id, name);
|
||||
return new HostLease(this, id);
|
||||
}
|
||||
}
|
||||
|
||||
public GameRuntimeOwnershipSnapshot CaptureOwnership()
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
{
|
||||
return new GameRuntimeOwnershipSnapshot(
|
||||
_disposeRequested,
|
||||
_disposeDrainActive,
|
||||
_disposed,
|
||||
_hostLeases.Count,
|
||||
CompletedTeardownStages,
|
||||
Session.CaptureOwnership(),
|
||||
PlayerIdentity.CaptureOwnership(),
|
||||
RuntimeSimulationOwnership.Capture(
|
||||
EntityObjects,
|
||||
InventoryOwner,
|
||||
CharacterOwner,
|
||||
CommunicationOwner,
|
||||
ActionOwner,
|
||||
MovementOwner),
|
||||
EnvironmentOwner.CaptureOwnership(),
|
||||
TransitOwner.CaptureOwnership(),
|
||||
_events.CaptureOwnership());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes transport and all per-generation routes inert while retaining the
|
||||
/// root for ordered host projection teardown.
|
||||
/// </summary>
|
||||
public void StopSession()
|
||||
{
|
||||
Session.Dispose();
|
||||
if (!Session.IsDisposalComplete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Runtime session shutdown was deferred by a re-entrant callback.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
{
|
||||
if (_disposed || _disposeDrainActive)
|
||||
return;
|
||||
_disposeRequested = true;
|
||||
_disposeDrainActive = true;
|
||||
}
|
||||
|
||||
List<Exception>? completedStageFailures = null;
|
||||
try
|
||||
{
|
||||
while (_disposeStage < TeardownStageCount)
|
||||
{
|
||||
bool complete;
|
||||
try
|
||||
{
|
||||
complete = DrainCurrentStage();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
complete = IsCurrentStageComplete();
|
||||
if (!complete)
|
||||
throw;
|
||||
(completedStageFailures ??= []).Add(error);
|
||||
}
|
||||
|
||||
if (!complete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"GameRuntime teardown stage {_disposeStage} did not complete.");
|
||||
}
|
||||
_disposeStage++;
|
||||
}
|
||||
|
||||
lock (_lifetimeGate)
|
||||
_disposed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
_disposeDrainActive = false;
|
||||
}
|
||||
|
||||
if (completedStageFailures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"GameRuntime reached terminal ownership with callback failures.",
|
||||
completedStageFailures);
|
||||
}
|
||||
}
|
||||
|
||||
private GameRuntimeTeardownStage CompletedTeardownStages =>
|
||||
_disposeStage switch
|
||||
{
|
||||
<= 0 => GameRuntimeTeardownStage.None,
|
||||
1 => GameRuntimeTeardownStage.HostLeasesReleased,
|
||||
2 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached,
|
||||
3 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached
|
||||
| GameRuntimeTeardownStage.SessionDisposed,
|
||||
4 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached
|
||||
| GameRuntimeTeardownStage.SessionDisposed
|
||||
| GameRuntimeTeardownStage.TransitReset,
|
||||
5 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached
|
||||
| GameRuntimeTeardownStage.SessionDisposed
|
||||
| GameRuntimeTeardownStage.TransitReset
|
||||
| GameRuntimeTeardownStage.ActionsDisposed,
|
||||
6 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached
|
||||
| GameRuntimeTeardownStage.SessionDisposed
|
||||
| GameRuntimeTeardownStage.TransitReset
|
||||
| GameRuntimeTeardownStage.ActionsDisposed
|
||||
| GameRuntimeTeardownStage.MovementDisposed,
|
||||
7 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached
|
||||
| GameRuntimeTeardownStage.SessionDisposed
|
||||
| GameRuntimeTeardownStage.TransitReset
|
||||
| GameRuntimeTeardownStage.ActionsDisposed
|
||||
| GameRuntimeTeardownStage.MovementDisposed
|
||||
| GameRuntimeTeardownStage.CharacterDisposed,
|
||||
8 => GameRuntimeTeardownStage.HostLeasesReleased
|
||||
| GameRuntimeTeardownStage.EventsDetached
|
||||
| GameRuntimeTeardownStage.SessionDisposed
|
||||
| GameRuntimeTeardownStage.TransitReset
|
||||
| GameRuntimeTeardownStage.ActionsDisposed
|
||||
| GameRuntimeTeardownStage.MovementDisposed
|
||||
| GameRuntimeTeardownStage.CharacterDisposed
|
||||
| GameRuntimeTeardownStage.InventoryDisposed,
|
||||
9 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.IdentityDisposed
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
10 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
_ => GameRuntimeTeardownStage.Complete,
|
||||
};
|
||||
|
||||
private bool DrainCurrentStage()
|
||||
{
|
||||
switch (_disposeStage)
|
||||
{
|
||||
case 0:
|
||||
lock (_lifetimeGate)
|
||||
{
|
||||
if (_hostLeases.Count != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"GameRuntime cannot retire while host leases remain: "
|
||||
+ string.Join(", ", _hostLeases.Values));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case 1:
|
||||
_events.Dispose();
|
||||
return _events.CaptureOwnership().IsConverged;
|
||||
case 2:
|
||||
StopSession();
|
||||
return Session.CaptureOwnership().IsConverged;
|
||||
case 3:
|
||||
TransitOwner.ResetSession();
|
||||
return TransitOwner.CaptureOwnership().IsSessionIdle;
|
||||
case 4:
|
||||
ActionOwner.Dispose();
|
||||
return ActionOwner.CaptureOwnership().IsConverged;
|
||||
case 5:
|
||||
MovementOwner.Dispose();
|
||||
return MovementOwner.CaptureOwnership().IsConverged;
|
||||
case 6:
|
||||
CharacterOwner.Dispose();
|
||||
return CharacterOwner.CaptureOwnership().IsConverged;
|
||||
case 7:
|
||||
InventoryOwner.Dispose();
|
||||
return InventoryOwner.CaptureOwnership().IsConverged;
|
||||
case 8:
|
||||
CommunicationOwner.Dispose();
|
||||
return CommunicationOwner.CaptureOwnership().IsConverged;
|
||||
case 9:
|
||||
PlayerIdentity.Dispose();
|
||||
return PlayerIdentity.CaptureOwnership().IsConverged;
|
||||
case 10:
|
||||
EntityObjects.Dispose();
|
||||
return EntityObjects.CaptureOwnership().IsConverged
|
||||
&& EntityObjects.Physics.CaptureOwnership().IsConverged;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCurrentStageComplete() =>
|
||||
_disposeStage switch
|
||||
{
|
||||
0 => HostLeaseCount == 0,
|
||||
1 => _events.CaptureOwnership().IsConverged,
|
||||
2 => Session.CaptureOwnership().IsConverged,
|
||||
3 => TransitOwner.CaptureOwnership().IsSessionIdle,
|
||||
4 => ActionOwner.CaptureOwnership().IsConverged,
|
||||
5 => MovementOwner.CaptureOwnership().IsConverged,
|
||||
6 => CharacterOwner.CaptureOwnership().IsConverged,
|
||||
7 => InventoryOwner.CaptureOwnership().IsConverged,
|
||||
8 => CommunicationOwner.CaptureOwnership().IsConverged,
|
||||
9 => PlayerIdentity.CaptureOwnership().IsConverged,
|
||||
10 => EntityObjects.CaptureOwnership().IsConverged
|
||||
&& EntityObjects.Physics.CaptureOwnership().IsConverged,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
private int HostLeaseCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
return _hostLeases.Count;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseHostLease(long id)
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
_hostLeases.Remove(id);
|
||||
}
|
||||
|
||||
private static void Fault(
|
||||
GameRuntimeConstructionPoint point,
|
||||
GameRuntimeConstructionContext context,
|
||||
Action<GameRuntimeConstructionPoint, GameRuntimeConstructionContext>?
|
||||
faultInjection) =>
|
||||
faultInjection?.Invoke(point, context);
|
||||
|
||||
private sealed class HostLease(GameRuntime owner, long id) : IDisposable
|
||||
{
|
||||
private GameRuntime? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.ReleaseHostLease(id);
|
||||
}
|
||||
|
||||
private sealed class ConstructionTransaction
|
||||
{
|
||||
private readonly List<IDisposable> _owners = [];
|
||||
private bool _complete;
|
||||
|
||||
public void Own(IDisposable owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_complete)
|
||||
throw new InvalidOperationException(
|
||||
"Runtime construction already completed.");
|
||||
_owners.Add(owner);
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_complete = true;
|
||||
_owners.Clear();
|
||||
}
|
||||
|
||||
public void RollbackAndThrow(Exception failure)
|
||||
{
|
||||
var failures = new List<Exception> { failure };
|
||||
for (int i = _owners.Count - 1; i >= 0; i--)
|
||||
{
|
||||
try
|
||||
{
|
||||
_owners[i].Dispose();
|
||||
}
|
||||
catch (Exception cleanup)
|
||||
{
|
||||
failures.Add(cleanup);
|
||||
}
|
||||
}
|
||||
_owners.Clear();
|
||||
if (failures.Count == 1)
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
||||
.Capture(failure)
|
||||
.Throw();
|
||||
throw new AggregateException(
|
||||
"GameRuntime construction and rollback both failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
403
src/AcDream.Runtime/GameRuntimeEventHub.cs
Normal file
403
src/AcDream.Runtime/GameRuntimeEventHub.cs
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
public readonly record struct GameRuntimeEventOwnershipSnapshot(
|
||||
bool IsDisposeRequested,
|
||||
bool IsDisposed,
|
||||
bool OwnerEventsAttached,
|
||||
int ObserverCount,
|
||||
long DispatchFailureCount,
|
||||
bool HasLastDispatchFailure)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !OwnerEventsAttached
|
||||
&& ObserverCount == 0;
|
||||
}
|
||||
|
||||
internal interface IGameRuntimeEventSink
|
||||
{
|
||||
void EmitCommand(
|
||||
RuntimeCommandDomain domain,
|
||||
int operation,
|
||||
RuntimeCommandStatus status,
|
||||
uint primaryObjectId = 0u,
|
||||
string? text = null);
|
||||
|
||||
void EmitLifecycle(
|
||||
RuntimeLifecycleState previous,
|
||||
RuntimeLifecycleState current);
|
||||
|
||||
void EmitMovement(in RuntimeMovementSnapshot movement);
|
||||
|
||||
void EmitPortal(in RuntimePortalSnapshot portal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazy ordered event projection over one exact Runtime owner graph. With no
|
||||
/// observers it attaches no owner callbacks and adds no production event work.
|
||||
/// </summary>
|
||||
internal sealed class GameRuntimeEventHub
|
||||
: IRuntimeEventSource,
|
||||
IGameRuntimeEventSink,
|
||||
IRuntimeEntityObjectObserver,
|
||||
IRuntimeCommunicationObserver,
|
||||
IDisposable
|
||||
{
|
||||
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
||||
private readonly RuntimeCommunicationState _communication;
|
||||
private readonly object _observerGate = new();
|
||||
private IRuntimeEventObserver[] _observers = [];
|
||||
private IDisposable? _entityObjectSubscription;
|
||||
private IDisposable? _communicationSubscription;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposed;
|
||||
|
||||
public GameRuntimeEventHub(
|
||||
RuntimeEntityObjectLifetime entityObjects,
|
||||
RuntimeCommunicationState communication)
|
||||
{
|
||||
_entityObjects = entityObjects
|
||||
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||
_communication = communication
|
||||
?? throw new ArgumentNullException(nameof(communication));
|
||||
}
|
||||
|
||||
public long DispatchFailureCount { get; private set; }
|
||||
public Exception? LastDispatchFailure { get; private set; }
|
||||
|
||||
public GameRuntimeEventOwnershipSnapshot CaptureOwnership()
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
return new GameRuntimeEventOwnershipSnapshot(
|
||||
_disposeRequested,
|
||||
_disposed,
|
||||
OwnerEventsAttached,
|
||||
_observers.Length,
|
||||
DispatchFailureCount,
|
||||
LastDispatchFailure is not null);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Subscribe(IRuntimeEventObserver observer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(observer);
|
||||
lock (_observerGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(
|
||||
_disposeRequested || _disposed,
|
||||
this);
|
||||
IRuntimeEventObserver[] current = _observers;
|
||||
if (Array.IndexOf(current, observer) >= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The runtime observer is already subscribed.");
|
||||
}
|
||||
|
||||
if (!OwnerEventsAttached)
|
||||
AttachOwnerEvents();
|
||||
|
||||
var replacement = new IRuntimeEventObserver[current.Length + 1];
|
||||
Array.Copy(current, replacement, current.Length);
|
||||
replacement[^1] = observer;
|
||||
Volatile.Write(ref _observers, replacement);
|
||||
}
|
||||
|
||||
return new ObserverSubscription(this, observer);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposeRequested = true;
|
||||
Volatile.Write(ref _observers, []);
|
||||
|
||||
List<Exception>? failures = null;
|
||||
TryDispose(
|
||||
ref _communicationSubscription,
|
||||
"communication event subscription",
|
||||
ref failures);
|
||||
TryDispose(
|
||||
ref _entityObjectSubscription,
|
||||
"entity/object event subscription",
|
||||
ref failures);
|
||||
_disposed = !OwnerEventsAttached;
|
||||
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Runtime event subscriptions did not converge.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EmitCommand(
|
||||
RuntimeCommandDomain domain,
|
||||
int operation,
|
||||
RuntimeCommandStatus status,
|
||||
uint primaryObjectId = 0u,
|
||||
string? text = null)
|
||||
{
|
||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||
return;
|
||||
var delta = new RuntimeCommandDelta(
|
||||
NextStamp(),
|
||||
domain,
|
||||
operation,
|
||||
status,
|
||||
primaryObjectId,
|
||||
text);
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnCommand(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EmitLifecycle(
|
||||
RuntimeLifecycleState previous,
|
||||
RuntimeLifecycleState current)
|
||||
{
|
||||
if (current == previous
|
||||
|| !TryObservers(out IRuntimeEventObserver[] observers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = new RuntimeLifecycleDelta(
|
||||
NextStamp(),
|
||||
previous,
|
||||
current);
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnLifecycle(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EmitMovement(in RuntimeMovementSnapshot movement)
|
||||
{
|
||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||
return;
|
||||
var delta = new RuntimeMovementDelta(NextStamp(), movement);
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnMovement(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EmitPortal(in RuntimePortalSnapshot portal)
|
||||
{
|
||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||
return;
|
||||
var delta = new RuntimePortalDelta(NextStamp(), portal);
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnPortal(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnEntity(in RuntimeEntityDelta delta)
|
||||
{
|
||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||
return;
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnEntity(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInventory(in RuntimeInventoryDelta delta)
|
||||
{
|
||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||
return;
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnInventory(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnChat(in RuntimeCommunicationEvent committed)
|
||||
{
|
||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||
return;
|
||||
var delta = new RuntimeChatDelta(NextStamp(), committed.Entry);
|
||||
foreach (IRuntimeEventObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnChat(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool OwnerEventsAttached =>
|
||||
_entityObjectSubscription is not null
|
||||
|| _communicationSubscription is not null;
|
||||
|
||||
private void AttachOwnerEvents()
|
||||
{
|
||||
IDisposable? entity = null;
|
||||
IDisposable? communication = null;
|
||||
try
|
||||
{
|
||||
entity = _entityObjects.Events.Subscribe(this);
|
||||
communication = _communication.Events.Subscribe(this);
|
||||
_entityObjectSubscription = entity;
|
||||
_communicationSubscription = communication;
|
||||
}
|
||||
catch
|
||||
{
|
||||
communication?.Dispose();
|
||||
entity?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Unsubscribe(IRuntimeEventObserver observer)
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
IRuntimeEventObserver[] current = _observers;
|
||||
int index = Array.IndexOf(current, observer);
|
||||
if (index < 0)
|
||||
return;
|
||||
if (current.Length == 1)
|
||||
{
|
||||
Volatile.Write(ref _observers, []);
|
||||
DetachOwnerEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
var replacement = new IRuntimeEventObserver[current.Length - 1];
|
||||
if (index != 0)
|
||||
Array.Copy(current, 0, replacement, 0, index);
|
||||
if (index != current.Length - 1)
|
||||
{
|
||||
Array.Copy(
|
||||
current,
|
||||
index + 1,
|
||||
replacement,
|
||||
index,
|
||||
current.Length - index - 1);
|
||||
}
|
||||
Volatile.Write(ref _observers, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private void DetachOwnerEvents()
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
TryDispose(
|
||||
ref _communicationSubscription,
|
||||
"communication event subscription",
|
||||
ref failures);
|
||||
TryDispose(
|
||||
ref _entityObjectSubscription,
|
||||
"entity/object event subscription",
|
||||
ref failures);
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Runtime event subscriptions did not detach.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDispose(
|
||||
ref IDisposable? subscription,
|
||||
string name,
|
||||
ref List<Exception>? failures)
|
||||
{
|
||||
if (subscription is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
subscription.Dispose();
|
||||
subscription = null;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"{name} did not detach.",
|
||||
error));
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeEventStamp NextStamp() =>
|
||||
_entityObjects.Events.NextStamp();
|
||||
|
||||
private bool TryObservers(out IRuntimeEventObserver[] observers)
|
||||
{
|
||||
observers = Volatile.Read(ref _observers);
|
||||
return observers.Length != 0;
|
||||
}
|
||||
|
||||
private void RecordDispatchFailure(Exception error)
|
||||
{
|
||||
DispatchFailureCount++;
|
||||
LastDispatchFailure = error;
|
||||
}
|
||||
|
||||
private sealed class ObserverSubscription(
|
||||
GameRuntimeEventHub owner,
|
||||
IRuntimeEventObserver observer)
|
||||
: IDisposable
|
||||
{
|
||||
private GameRuntimeEventHub? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Unsubscribe(observer);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public readonly record struct RuntimeLocalPlayerIdentityOwnershipSnapshot(
|
||||
bool IsDisposed,
|
||||
uint ServerGuid,
|
||||
long Revision)
|
||||
{
|
||||
public bool IsConverged => IsDisposed && ServerGuid == 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical session-scoped identity of the local player. Graphical and direct
|
||||
/// hosts borrow this exact owner; presentation adapters may not mirror it.
|
||||
/// </summary>
|
||||
public sealed class RuntimeLocalPlayerIdentityState : IDisposable
|
||||
{
|
||||
private uint _serverGuid;
|
||||
private long _revision;
|
||||
private bool _disposed;
|
||||
|
||||
public uint ServerGuid
|
||||
{
|
||||
get => _serverGuid;
|
||||
set
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_serverGuid == value)
|
||||
return;
|
||||
_serverGuid = value;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ServerGuid = 0u;
|
||||
}
|
||||
|
||||
public RuntimeLocalPlayerIdentityOwnershipSnapshot CaptureOwnership() =>
|
||||
new(_disposed, _serverGuid, Revision);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_serverGuid = 0u;
|
||||
Interlocked.Increment(ref _revision);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,30 @@ public enum LiveSessionStartStatus
|
|||
Failed,
|
||||
}
|
||||
|
||||
public readonly record struct LiveSessionOwnershipSnapshot(
|
||||
bool IsDisposed,
|
||||
bool IsDisposeRequested,
|
||||
bool IsInWorld,
|
||||
bool HasActiveSession,
|
||||
bool HasRetiredSession,
|
||||
bool HasPendingInitialReset,
|
||||
bool HasPendingOperation,
|
||||
int OperationDepth,
|
||||
ulong Generation,
|
||||
RuntimeTeardownStage LastTeardownStages)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& IsDisposeRequested
|
||||
&& !IsInWorld
|
||||
&& !HasActiveSession
|
||||
&& !HasRetiredSession
|
||||
&& !HasPendingInitialReset
|
||||
&& !HasPendingOperation
|
||||
&& OperationDepth == 0
|
||||
&& LastTeardownStages == RuntimeTeardownStage.Complete;
|
||||
}
|
||||
|
||||
public sealed record LiveSessionCharacterSelection(
|
||||
int ActiveIndex,
|
||||
uint CharacterId,
|
||||
|
|
@ -272,6 +296,24 @@ public sealed class LiveSessionController
|
|||
get { lock (_gate) return _disposed; }
|
||||
}
|
||||
|
||||
public LiveSessionOwnershipSnapshot CaptureOwnership()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return new LiveSessionOwnershipSnapshot(
|
||||
_disposed,
|
||||
_disposeRequested,
|
||||
_inWorld,
|
||||
_scope is not null,
|
||||
_retiredScope is not null,
|
||||
_pendingInitialResetHost is not null,
|
||||
_pendingOperation is not null,
|
||||
_operationDepth,
|
||||
_generation,
|
||||
_lastTeardownStages);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveSessionStartResult Start(
|
||||
LiveSessionConnectOptions options,
|
||||
ILiveSessionLifecycleHost host)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue