refactor(runtime): own canonical action state
Move selection, combat, and interaction target mode under one Runtime owner; make plugins, retained UI, session routing, and typed runtime views borrow its exact children; and add failure-safe reset, instance isolation, source ownership, and normalized checkpoint coverage without changing retail ordering. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
bb45afef33
commit
b298f99f91
38 changed files with 711 additions and 93 deletions
177
src/AcDream.Runtime/Gameplay/RuntimeActionState.cs
Normal file
177
src/AcDream.Runtime/Gameplay/RuntimeActionState.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public readonly record struct RuntimeActionOwnershipSnapshot(
|
||||
bool IsDisposed,
|
||||
bool InternalSubscriptionsAttached,
|
||||
uint SelectedObjectId,
|
||||
uint PreviousObjectId,
|
||||
uint PreviousValidObjectId,
|
||||
CombatMode CombatMode,
|
||||
int TrackedTargetHealthCount,
|
||||
InteractionMode InteractionMode,
|
||||
long SelectionRevision,
|
||||
long CombatRevision,
|
||||
long InteractionRevision)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !InternalSubscriptionsAttached
|
||||
&& SelectedObjectId == 0u
|
||||
&& PreviousObjectId == 0u
|
||||
&& PreviousValidObjectId == 0u
|
||||
&& CombatMode == CombatMode.NonCombat
|
||||
&& TrackedTargetHealthCount == 0
|
||||
&& InteractionMode == InteractionMode.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical presentation-independent owner for selection, combat
|
||||
/// notifications/mode, and temporary interaction target mode. 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;
|
||||
|
||||
public RuntimeActionState()
|
||||
{
|
||||
Selection = new SelectionState();
|
||||
Combat = new CombatState();
|
||||
Interaction = new InteractionState();
|
||||
View = new ActionView(this);
|
||||
|
||||
Selection.Changed += OnSelectionChanged;
|
||||
Combat.CombatModeChanged += OnCombatModeChanged;
|
||||
Combat.HealthChanged += OnHealthChanged;
|
||||
Interaction.Changed += OnInteractionChanged;
|
||||
_internalSubscriptionsAttached = true;
|
||||
}
|
||||
|
||||
public SelectionState Selection { get; }
|
||||
public CombatState Combat { get; }
|
||||
public InteractionState Interaction { get; }
|
||||
public IRuntimeActionView View { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
|
||||
_disposed,
|
||||
_internalSubscriptionsAttached,
|
||||
Selection.SelectedObjectId ?? 0u,
|
||||
Selection.PreviousObjectId ?? 0u,
|
||||
Selection.PreviousValidObjectId ?? 0u,
|
||||
Combat.CurrentMode,
|
||||
Combat.TrackedTargetCount,
|
||||
Interaction.Current,
|
||||
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(Interaction.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(Interaction.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;
|
||||
_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);
|
||||
|
||||
private void OnHealthChanged(uint _, float __) =>
|
||||
Interlocked.Increment(ref _combatRevision);
|
||||
|
||||
private void OnInteractionChanged(InteractionModeTransition _) =>
|
||||
Interlocked.Increment(ref _interactionRevision);
|
||||
|
||||
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);
|
||||
|
||||
public bool TryGetHealth(uint objectId, out float healthPercent)
|
||||
{
|
||||
if (!owner.Combat.HasHealth(objectId))
|
||||
{
|
||||
healthPercent = 0f;
|
||||
return false;
|
||||
}
|
||||
|
||||
healthPercent = owner.Combat.GetHealthPercent(objectId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue