refactor(runtime): own interaction transactions

Move use throttling, appraisal identity, queued interactions, and exact post-arrival pickup state beneath RuntimeActionState. Keep App as the picker, movement, transport, and retained-presentation adapter while preserving retail send and UseDone ordering. Add reset, disposal, GUID-reuse, callback-reentrancy, and transport-failure coverage.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-26 11:13:09 +02:00
parent be73bccf5a
commit f5f7b4177f
31 changed files with 1365 additions and 432 deletions

View file

@ -1,4 +1,5 @@
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Selection;
namespace AcDream.Runtime.Gameplay;
@ -6,12 +7,14 @@ namespace AcDream.Runtime.Gameplay;
public readonly record struct RuntimeActionOwnershipSnapshot(
bool IsDisposed,
bool InternalSubscriptionsAttached,
bool InteractionTransactionsDisposed,
uint SelectedObjectId,
uint PreviousObjectId,
uint PreviousValidObjectId,
CombatMode CombatMode,
int TrackedTargetHealthCount,
InteractionMode InteractionMode,
RuntimeInteractionTransactionSnapshot InteractionTransactions,
long SelectionRevision,
long CombatRevision,
long InteractionRevision)
@ -19,12 +22,14 @@ public readonly record struct RuntimeActionOwnershipSnapshot(
public bool IsConverged =>
IsDisposed
&& !InternalSubscriptionsAttached
&& InteractionTransactionsDisposed
&& SelectedObjectId == 0u
&& PreviousObjectId == 0u
&& PreviousValidObjectId == 0u
&& CombatMode == CombatMode.NonCombat
&& TrackedTargetHealthCount == 0
&& InteractionMode == InteractionMode.None;
&& InteractionMode == InteractionMode.None
&& InteractionTransactions.IsConverged;
}
/// <summary>
@ -40,11 +45,14 @@ public sealed class RuntimeActionState : IDisposable
private long _combatRevision;
private long _interactionRevision;
public RuntimeActionState()
public RuntimeActionState(InventoryTransactionState inventoryTransactions)
{
ArgumentNullException.ThrowIfNull(inventoryTransactions);
Selection = new SelectionState();
Combat = new CombatState();
Interaction = new InteractionState();
Transactions = new RuntimeInteractionTransactionState(
inventoryTransactions);
View = new ActionView(this);
Selection.Changed += OnSelectionChanged;
@ -57,18 +65,21 @@ public sealed class RuntimeActionState : IDisposable
public SelectionState Selection { get; }
public CombatState Combat { get; }
public InteractionState Interaction { get; }
public RuntimeInteractionTransactionState Transactions { get; }
public IRuntimeActionView View { get; }
public bool IsDisposed => _disposed;
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
_disposed,
_internalSubscriptionsAttached,
Transactions.IsDisposed,
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));
@ -82,6 +93,7 @@ public sealed class RuntimeActionState : IDisposable
{
ObjectDisposedException.ThrowIf(_disposed, this);
List<Exception>? failures = null;
Try(Transactions.ResetSession, ref failures);
Try(Interaction.ResetSession, ref failures);
Try(() => Selection.Reset(), ref failures);
Try(Combat.Clear, ref failures);
@ -101,6 +113,7 @@ public sealed class RuntimeActionState : IDisposable
List<Exception>? failures = null;
try
{
Try(Transactions.Dispose, ref failures);
Try(Interaction.ResetSession, ref failures);
Try(() => Selection.Reset(), ref failures);
Try(Combat.Clear, ref failures);
@ -160,7 +173,8 @@ public sealed class RuntimeActionState : IDisposable
owner.Combat.TrackedTargetCount,
Interlocked.Read(ref owner._interactionRevision),
owner.Interaction.Current.Kind,
owner.Interaction.Current.SourceObjectId);
owner.Interaction.Current.SourceObjectId,
owner.Transactions.CaptureOwnership());
public bool TryGetHealth(uint objectId, out float healthPercent)
{