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); /// /// Single App-layer owner for retail's pointer interaction mode. Core selection /// remains session truth; this state represents temporary UI orchestration. /// public sealed class InteractionState { public InteractionMode Current { get; private set; } = InteractionMode.None; public event Action? 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); /// /// Publishes the session-reset edge even when the mode is already clear so /// a retry can repair any observer that missed an earlier notification. /// public void ResetSession() { InteractionMode previous = Current; Current = InteractionMode.None; Action? listeners = Changed; if (listeners is null) return; var transition = new InteractionModeTransition(previous, InteractionMode.None); List? failures = null; foreach (Action 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; } }