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:
Erik 2026-07-26 10:44:09 +02:00
parent bb45afef33
commit b298f99f91
38 changed files with 711 additions and 93 deletions

View file

@ -1,4 +1,5 @@
using AcDream.Core.Combat;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.UI;

View file

@ -1,79 +0,0 @@
namespace AcDream.App.UI;
public enum InteractionModeKind
{
None,
Use,
Examine,
UseItemOnTarget,
}
public readonly record struct InteractionMode(
InteractionModeKind Kind,
uint SourceObjectId = 0)
{
public static InteractionMode None => new(InteractionModeKind.None);
}
public readonly record struct InteractionModeTransition(
InteractionMode Previous,
InteractionMode Current);
/// <summary>
/// Single App-layer owner for retail's pointer interaction mode. Core selection
/// remains session truth; this state represents temporary UI orchestration.
/// </summary>
public sealed class InteractionState
{
public InteractionMode Current { get; private set; } = InteractionMode.None;
public event Action<InteractionModeTransition>? Changed;
public bool EnterUse() => Set(new InteractionMode(InteractionModeKind.Use));
public bool EnterExamine() => Set(new InteractionMode(InteractionModeKind.Examine));
public bool EnterUseItemOnTarget(uint sourceObjectId)
{
if (sourceObjectId == 0)
throw new ArgumentOutOfRangeException(nameof(sourceObjectId));
return Set(new InteractionMode(InteractionModeKind.UseItemOnTarget, sourceObjectId));
}
public bool Clear() => Set(InteractionMode.None);
/// <summary>
/// Publishes the session-reset edge even when the mode is already clear so
/// a retry can repair any observer that missed an earlier notification.
/// </summary>
public void ResetSession()
{
InteractionMode previous = Current;
Current = InteractionMode.None;
Action<InteractionModeTransition>? listeners = Changed;
if (listeners is null)
return;
var transition = new InteractionModeTransition(previous, InteractionMode.None);
List<Exception>? failures = null;
foreach (Action<InteractionModeTransition> listener in listeners.GetInvocationList())
{
try { listener(transition); }
catch (Exception error) { (failures ??= []).Add(error); }
}
if (failures is not null)
throw new AggregateException(
"One or more interaction-mode reset observers failed.",
failures);
}
private bool Set(InteractionMode mode)
{
if (Current == mode)
return false;
InteractionMode previous = Current;
Current = mode;
Changed?.Invoke(new InteractionModeTransition(previous, mode));
return true;
}
}

View file

@ -1,6 +1,7 @@
using System;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.UI;
@ -74,6 +75,7 @@ public sealed class ItemInteractionController : IDisposable
public ItemInteractionController(
ClientObjectTable objects,
InventoryTransactionState transactions,
InteractionState interactionState,
Func<uint> playerGuid,
Action<uint>? sendUse,
Action<uint, uint>? sendUseWithTarget,
@ -82,7 +84,6 @@ public sealed class ItemInteractionController : IDisposable
Action<uint>? sendExamine = null,
Func<long>? nowMs = null,
Action<string>? toast = null,
InteractionState? interactionState = null,
Func<bool>? readyForInventoryRequest = null,
Func<uint>? activeVendorId = null,
Func<uint>? groundObjectId = null,
@ -133,7 +134,8 @@ public sealed class ItemInteractionController : IDisposable
_dragOnPlayerOpensSecureTrade = dragOnPlayerOpensSecureTrade ?? (() => true);
_systemMessage = systemMessage;
_requestUse = requestUse;
_interactionState = interactionState ?? new InteractionState();
_interactionState = interactionState
?? throw new ArgumentNullException(nameof(interactionState));
_transactions = transactions
?? throw new ArgumentNullException(nameof(transactions));
if (!ReferenceEquals(_transactions.Objects, _objects))