acdream/src/AcDream.Runtime/Gameplay/RuntimeActionState.cs

274 lines
9.7 KiB
C#

using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Selection;
using AcDream.Core.Spells;
namespace AcDream.Runtime.Gameplay;
public readonly record struct RuntimeActionOwnershipSnapshot(
bool IsDisposed,
bool InternalSubscriptionsAttached,
bool InteractionTransactionsDisposed,
bool CombatAttackDisposed,
bool CombatTargetDisposed,
bool SpellCastReset,
uint SelectedObjectId,
uint PreviousObjectId,
uint PreviousValidObjectId,
CombatMode CombatMode,
int TrackedTargetHealthCount,
InteractionMode InteractionMode,
RuntimeInteractionTransactionSnapshot InteractionTransactions,
long SelectionRevision,
long CombatRevision,
long InteractionRevision)
{
public bool IsConverged =>
IsDisposed
&& !InternalSubscriptionsAttached
&& InteractionTransactionsDisposed
&& CombatAttackDisposed
&& CombatTargetDisposed
&& SpellCastReset
&& SelectedObjectId == 0u
&& PreviousObjectId == 0u
&& PreviousValidObjectId == 0u
&& CombatMode == CombatMode.NonCombat
&& TrackedTargetHealthCount == 0
&& InteractionMode == InteractionMode.None
&& InteractionTransactions.IsConverged;
}
/// <summary>
/// Canonical presentation-independent owner for selection, interaction
/// transactions, combat mode/attack/target intent, and spell-cast intent.
/// Graphical and no-window hosts borrow these exact instances.
/// </summary>
public sealed class RuntimeActionState : IDisposable
{
private bool _disposed;
private bool _internalSubscriptionsAttached;
private long _selectionRevision;
private long _combatRevision;
private long _interactionRevision;
private long _combatIntentRevision;
private long _magicIntentRevision;
public RuntimeActionState(
InventoryTransactionState inventoryTransactions,
Spellbook spellbook,
IRuntimeCombatAttackOperations combatAttackOperations,
IRuntimeCombatTargetOperations combatTargetOperations,
IRuntimeCombatModeOperations combatModeOperations,
IRuntimeSpellCastOperations spellCastOperations,
Func<double>? now = null)
{
ArgumentNullException.ThrowIfNull(inventoryTransactions);
ArgumentNullException.ThrowIfNull(spellbook);
ArgumentNullException.ThrowIfNull(combatAttackOperations);
ArgumentNullException.ThrowIfNull(combatTargetOperations);
ArgumentNullException.ThrowIfNull(combatModeOperations);
ArgumentNullException.ThrowIfNull(spellCastOperations);
Selection = new SelectionState();
Combat = new CombatState();
Interaction = new InteractionState();
Transactions = new RuntimeInteractionTransactionState(
inventoryTransactions);
CombatAttack = new RuntimeCombatAttackState(
Combat,
combatAttackOperations,
now);
CombatTarget = new RuntimeCombatTargetState(
Combat,
Selection,
combatTargetOperations);
CombatMode = new RuntimeCombatModeState(
Combat,
combatModeOperations);
SpellCast = new RuntimeSpellCastState(
spellbook,
Selection,
spellCastOperations);
View = new ActionView(this);
Selection.Changed += OnSelectionChanged;
Combat.CombatModeChanged += OnCombatModeChanged;
Combat.HealthChanged += OnHealthChanged;
Interaction.Changed += OnInteractionChanged;
CombatAttack.StateChanged += OnCombatAttackChanged;
SpellCast.StateChanged += OnSpellCastChanged;
_internalSubscriptionsAttached = true;
}
public SelectionState Selection { get; }
public CombatState Combat { get; }
public InteractionState Interaction { get; }
public RuntimeInteractionTransactionState Transactions { get; }
public RuntimeCombatAttackState CombatAttack { get; }
public RuntimeCombatTargetState CombatTarget { get; }
public RuntimeCombatModeState CombatMode { get; }
public RuntimeSpellCastState SpellCast { get; }
public IRuntimeActionView View { get; }
public bool IsDisposed => _disposed;
internal event Action? CombatChanged;
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
_disposed,
_internalSubscriptionsAttached,
Transactions.IsDisposed,
CombatAttack.IsDisposed,
CombatTarget.IsDisposed,
SpellCast.LastRequestedSpellId is null
&& SpellCast.LastRequestedTargetId is null,
Selection.SelectedObjectId ?? 0u,
Selection.PreviousObjectId ?? 0u,
Selection.PreviousValidObjectId ?? 0u,
Combat.CurrentMode,
Combat.TrackedTargetCount,
Interaction.Current,
Transactions.CaptureOwnership(),
Interlocked.Read(ref _selectionRevision),
Interlocked.Read(ref _combatRevision),
Interlocked.Read(ref _interactionRevision));
/// <summary>
/// Restores the action group to the equivalent of a fresh retail character
/// session. Each owner commits before notifying observers, so every suffix
/// is attempted and a retry converges after observer failure.
/// </summary>
public void ResetSession()
{
ObjectDisposedException.ThrowIf(_disposed, this);
List<Exception>? failures = null;
Try(Transactions.ResetSession, ref failures);
Try(Interaction.ResetSession, ref failures);
Try(SpellCast.Reset, ref failures);
Try(CombatAttack.ResetSession, ref failures);
Try(() => Selection.Reset(), ref failures);
Try(Combat.Clear, ref failures);
if (failures is not null)
{
throw new AggregateException(
"Runtime action state did not converge during reset.",
failures);
}
}
public void Dispose()
{
if (_disposed)
return;
List<Exception>? failures = null;
try
{
Try(Transactions.Dispose, ref failures);
Try(Interaction.ResetSession, ref failures);
Try(SpellCast.Reset, ref failures);
Try(CombatAttack.ResetSession, ref failures);
Try(() => Selection.Reset(), ref failures);
Try(Combat.Clear, ref failures);
}
finally
{
Selection.Changed -= OnSelectionChanged;
Combat.CombatModeChanged -= OnCombatModeChanged;
Combat.HealthChanged -= OnHealthChanged;
Interaction.Changed -= OnInteractionChanged;
CombatAttack.StateChanged -= OnCombatAttackChanged;
SpellCast.StateChanged -= OnSpellCastChanged;
Try(CombatAttack.Dispose, ref failures);
Try(CombatTarget.Dispose, ref failures);
_internalSubscriptionsAttached = false;
_disposed = true;
}
if (failures is not null)
{
throw new AggregateException(
"Runtime action state did not converge during disposal.",
failures);
}
}
private void OnSelectionChanged(SelectionTransition _) =>
Interlocked.Increment(ref _selectionRevision);
private void OnCombatModeChanged(CombatMode _)
{
Interlocked.Increment(ref _combatRevision);
CombatChanged?.Invoke();
}
private void OnHealthChanged(uint _, float __)
{
Interlocked.Increment(ref _combatRevision);
CombatChanged?.Invoke();
}
private void OnInteractionChanged(InteractionModeTransition _) =>
Interlocked.Increment(ref _interactionRevision);
private void OnCombatAttackChanged()
{
Interlocked.Increment(ref _combatIntentRevision);
CombatChanged?.Invoke();
}
private void OnSpellCastChanged() =>
Interlocked.Increment(ref _magicIntentRevision);
private static void Try(Action action, ref List<Exception>? failures)
{
try
{
action();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
private sealed class ActionView(RuntimeActionState owner)
: IRuntimeActionView
{
public RuntimeActionSnapshot Snapshot => new(
Interlocked.Read(ref owner._selectionRevision),
owner.Selection.SelectedObjectId ?? 0u,
owner.Selection.PreviousObjectId ?? 0u,
owner.Selection.PreviousValidObjectId ?? 0u,
Interlocked.Read(ref owner._combatRevision),
owner.Combat.CurrentMode,
owner.Combat.TrackedTargetCount,
Interlocked.Read(ref owner._interactionRevision),
owner.Interaction.Current.Kind,
owner.Interaction.Current.SourceObjectId,
owner.Transactions.CaptureOwnership(),
new RuntimeCombatAttackSnapshot(
Interlocked.Read(ref owner._combatIntentRevision),
owner.CombatAttack.RequestedHeight,
owner.CombatAttack.DesiredPower,
owner.CombatAttack.PowerBarLevel,
owner.CombatAttack.BuildInProgress,
owner.CombatAttack.AttackRequestInProgress,
owner.CombatAttack.RequestedAttackPower),
new RuntimeSpellCastSnapshot(
Interlocked.Read(ref owner._magicIntentRevision),
owner.SpellCast.LastRequestedSpellId ?? 0u,
owner.SpellCast.LastRequestedTargetId ?? 0u));
public bool TryGetHealth(uint objectId, out float healthPercent)
{
if (!owner.Combat.HasHealth(objectId))
{
healthPercent = 0f;
return false;
}
healthPercent = owner.Combat.GetHealthPercent(objectId);
return true;
}
}
}