using AcDream.App.Interaction; using AcDream.App.Rendering; using AcDream.App.UI; using AcDream.Core.Combat; using AcDream.UI.Abstractions.Input; namespace AcDream.App.Input; internal interface IGameplayInputActionSurface { void AddFired(Action callback); void RemoveFired(Action callback); void SetCombatScope(InputScope? scope); } internal sealed class DispatcherGameplayInputActionSurface(InputDispatcher dispatcher) : IGameplayInputActionSurface { private readonly InputDispatcher _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); public void AddFired(Action callback) => _dispatcher.Fired += callback; public void RemoveFired(Action callback) => _dispatcher.Fired -= callback; public void SetCombatScope(InputScope? scope) => _dispatcher.SetCombatScope(scope); } internal interface ICombatModeEventSurface { CombatMode CurrentMode { get; } void AddChanged(Action callback); void RemoveChanged(Action callback); } internal sealed class CombatStateModeEventSurface(CombatState combat) : ICombatModeEventSurface { private readonly CombatState _combat = combat ?? throw new ArgumentNullException(nameof(combat)); public CombatMode CurrentMode => _combat.CurrentMode; public void AddChanged(Action callback) => _combat.CombatModeChanged += callback; public void RemoveChanged(Action callback) => _combat.CombatModeChanged -= callback; } internal interface IGameplayInputPriorityTargets { bool HandlePointerAction(InputAction action, ActivationType activation); void HandleScroll(InputAction action); bool HandleCombatAction(InputAction action, ActivationType activation); bool HandleRetainedUiAction(InputAction action); bool HandleSelectionAction(InputAction action); bool HandlePressedMovementAction(InputAction action); void HandleCommand(InputAction action); } /// /// Typed adapter over canonical gameplay owners. Optional presentation owners /// remain no-ops when their stack is disabled; no state is mirrored here. /// internal sealed class RuntimeGameplayInputPriorityTargets : IGameplayInputPriorityTargets { private readonly GameplayInputFrameController _frame; private readonly CameraPointerInputController _pointer; private readonly RetailUiRuntime? _retainedUi; private readonly SelectionInteractionController? _selection; private readonly IGameplayInputCommandTarget _commands; public RuntimeGameplayInputPriorityTargets( GameplayInputFrameController frame, CameraPointerInputController pointer, RetailUiRuntime? retainedUi, SelectionInteractionController? selection, IGameplayInputCommandTarget commands) { _frame = frame ?? throw new ArgumentNullException(nameof(frame)); _pointer = pointer ?? throw new ArgumentNullException(nameof(pointer)); _retainedUi = retainedUi; _selection = selection; _commands = commands ?? throw new ArgumentNullException(nameof(commands)); } public bool HandlePointerAction(InputAction action, ActivationType activation) => _frame.HandlePointerAction(action, activation); public void HandleScroll(InputAction action) => _pointer.HandleScroll(action); public bool HandleCombatAction(InputAction action, ActivationType activation) => _frame.HandleCombatAction(action, activation); public bool HandleRetainedUiAction(InputAction action) => _retainedUi?.HandleInputAction(action) == true; public bool HandleSelectionAction(InputAction action) => _selection?.HandleInputAction(action) == true; public bool HandlePressedMovementAction(InputAction action) => _frame.HandlePressedMovementAction(action); public void HandleCommand(InputAction action) => _commands.Handle(action); } /// /// Sole gameplay subscriber to . The route /// preserves the accepted priority exactly: pointer, scroll, combat, /// Press/DoubleClick gate, retained UI, selection, pressed movement, commands. /// Subscription ownership is reversible and callbacks become inert before the /// live-session shutdown barrier. /// internal sealed class GameplayInputActionRouter : IDisposable { private readonly IGameplayInputActionSurface _actions; private readonly ICombatModeEventSurface _combat; private readonly IGameplayInputPriorityTargets _targets; private readonly HostQuiescenceGate _quiescence; private readonly Action _log; private readonly Action _fired; private readonly Action _combatModeChanged; private readonly bool[] _attached = new bool[2]; private ResourceShutdownTransaction? _detach; private bool _attachStarted; private int _disposeRequested; private int _active; public GameplayInputActionRouter( IGameplayInputActionSurface actions, ICombatModeEventSurface combat, IGameplayInputPriorityTargets targets, HostQuiescenceGate quiescence, Action? log = null) { _actions = actions ?? throw new ArgumentNullException(nameof(actions)); _combat = combat ?? throw new ArgumentNullException(nameof(combat)); _targets = targets ?? throw new ArgumentNullException(nameof(targets)); _quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence)); _log = log ?? (_ => { }); _fired = OnFired; _combatModeChanged = OnCombatModeChanged; } public static GameplayInputActionRouter Create( InputDispatcher dispatcher, CombatState combat, IGameplayInputPriorityTargets targets, HostQuiescenceGate quiescence, Action? log = null) => new( new DispatcherGameplayInputActionSurface(dispatcher), new CombatStateModeEventSurface(combat), targets, quiescence, log); public bool IsDisposalComplete => _attached.All(static attached => !attached); public void Attach() { ObjectDisposedException.ThrowIf( Volatile.Read(ref _disposeRequested) != 0, this); if (_attachStarted) { throw new InvalidOperationException( "Gameplay action attachment has already started."); } _attachStarted = true; try { // Mark each edge before invoking its custom accessor. An accessor // may perform the add and then throw; rollback must still remove it. _attached[0] = true; _actions.AddFired(_fired); _attached[1] = true; _combat.AddChanged(_combatModeChanged); Volatile.Write(ref _active, 1); SetCombatScope(_combat.CurrentMode); } catch (Exception attachError) { Deactivate(); try { EnsureDetachTransaction().CompleteOrThrow(); } catch (Exception rollbackError) { throw new AggregateException( "Gameplay action registration and rollback both failed.", new InvalidOperationException( "Gameplay action registration failed.", attachError), rollbackError); } throw new InvalidOperationException( "Gameplay action registration failed and was rolled back.", attachError); } } public void Deactivate() => Interlocked.Exchange(ref _active, 0); public void Dispose() { Interlocked.Exchange(ref _disposeRequested, 1); Deactivate(); EnsureDetachTransaction().CompleteOrThrow(); } private void OnFired(InputAction action, ActivationType activation) => _quiescence.Invoke(() => { if (Volatile.Read(ref _active) != 0) Route(action, activation); }); private void OnCombatModeChanged(CombatMode mode) => _quiescence.Invoke(() => { if (Volatile.Read(ref _active) != 0) SetCombatScope(mode); }); private void Route(InputAction action, ActivationType activation) { _log($"[input] {action} {activation}"); if (_targets.HandlePointerAction(action, activation)) return; if (action is InputAction.ScrollUp or InputAction.ScrollDown) { if (activation == ActivationType.Press) _targets.HandleScroll(action); return; } if (_targets.HandleCombatAction(action, activation)) return; if (activation is not ActivationType.Press and not ActivationType.DoubleClick and not ActivationType.Click) { return; } if (_targets.HandleRetainedUiAction(action)) return; if (_targets.HandleSelectionAction(action)) return; if (_targets.HandlePressedMovementAction(action)) return; _targets.HandleCommand(action); } private void SetCombatScope(CombatMode mode) => _actions.SetCombatScope(mode switch { CombatMode.Melee => InputScope.MeleeCombat, CombatMode.Missile => InputScope.MissileCombat, CombatMode.Magic => InputScope.MagicCombat, _ => null, }); private ResourceShutdownTransaction EnsureDetachTransaction() => _detach ??= new ResourceShutdownTransaction( new ResourceShutdownStage("gameplay action callbacks", [ new("combat mode", () => Remove(1)), new("dispatcher fired", () => Remove(0)), ])); private void Remove(int index) { if (!_attached[index]) return; switch (index) { case 0: _actions.RemoveFired(_fired); break; case 1: _combat.RemoveChanged(_combatModeChanged); break; default: throw new ArgumentOutOfRangeException(nameof(index)); } _attached[index] = false; } }