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);
}
///
/// Lazy ordered event projection over one exact Runtime owner graph. With no
/// observers it attaches no owner callbacks and adds no production event work.
///
internal sealed class GameRuntimeEventHub
: IRuntimeEventSource,
IGameRuntimeEventSink,
IRuntimeEntityObjectObserver,
IRuntimeCommunicationObserver,
IDisposable
{
private readonly RuntimeEntityObjectLifetime _entityObjects;
private readonly RuntimeCommunicationState _communication;
private readonly RuntimeActionState _actions;
private readonly object _observerGate = new();
private IRuntimeEventObserver[] _observers = [];
private IDisposable? _entityObjectSubscription;
private IDisposable? _communicationSubscription;
private bool _actionEventsAttached;
private bool _disposeRequested;
private bool _disposed;
public GameRuntimeEventHub(
RuntimeEntityObjectLifetime entityObjects,
RuntimeCommunicationState communication,
RuntimeActionState actions)
{
_entityObjects = entityObjects
?? throw new ArgumentNullException(nameof(entityObjects));
_communication = communication
?? throw new ArgumentNullException(nameof(communication));
_actions = actions
?? throw new ArgumentNullException(nameof(actions));
}
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? failures = null;
TryDispose(
ref _communicationSubscription,
"communication event subscription",
ref failures);
TryDispose(
ref _entityObjectSubscription,
"entity/object event subscription",
ref failures);
DetachActionEvents();
_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
|| _actionEventsAttached;
private void AttachOwnerEvents()
{
IDisposable? entity = null;
IDisposable? communication = null;
try
{
entity = _entityObjects.Events.Subscribe(this);
communication = _communication.Events.Subscribe(this);
_actions.CombatChanged += OnCombatChanged;
_actionEventsAttached = true;
_entityObjectSubscription = entity;
_communicationSubscription = communication;
}
catch
{
DetachActionEvents();
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? failures = null;
TryDispose(
ref _communicationSubscription,
"communication event subscription",
ref failures);
TryDispose(
ref _entityObjectSubscription,
"entity/object event subscription",
ref failures);
DetachActionEvents();
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? 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 void OnCombatChanged()
{
if (!TryObservers(out IRuntimeEventObserver[] observers))
return;
RuntimeActionSnapshot actions = _actions.View.Snapshot;
var delta = new RuntimeCombatDelta(
NextStamp(),
actions.CombatMode,
actions.TrackedTargetHealthCount,
actions.CombatAttack);
foreach (IRuntimeEventObserver observer in observers)
{
try
{
observer.OnCombat(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
}
private void DetachActionEvents()
{
if (!_actionEventsAttached)
return;
_actions.CombatChanged -= OnCombatChanged;
_actionEventsAttached = false;
}
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);
}
}