using System.Collections.Generic; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.UI.Abstractions.Tests.Input; /// /// K.1b: per-frame held-key /// polling. Movement (WASD/Shift/Space) needs "is W currently held this /// frame" — that's IsActionHeld. The dispatcher's Fired events /// drive press/release transitions; IsActionHeld drives the /// per-frame MovementInput struct. /// public class InputDispatcherIsActionHeldTests { private static (InputDispatcher dispatcher, FakeKeyboardSource kb, FakeMouseSource mouse, KeyBindings bindings) Build() { var kb = new FakeKeyboardSource(); var mouse = new FakeMouseSource(); var bindings = new KeyBindings(); var dispatcher = new InputDispatcher(kb, mouse, bindings); return (dispatcher, kb, mouse, bindings); } [Fact] public void IsActionHeld_returns_true_while_bound_key_held() { var (dispatcher, kb, _, bindings) = Build(); bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); kb.EmitKeyDown(Key.W, ModifierMask.None); Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); kb.EmitKeyUp(Key.W, ModifierMask.None); Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); } [Fact] public void IsActionHeld_returns_false_when_no_binding_for_action() { var (dispatcher, kb, _, bindings) = Build(); // No binding for MovementBackup at all. bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); kb.EmitKeyDown(Key.W, ModifierMask.None); Assert.False(dispatcher.IsActionHeld(InputAction.MovementBackup)); } [Fact] public void IsActionHeld_modifier_mismatch_returns_false() { var (dispatcher, kb, _, bindings) = Build(); bindings.Add(new Binding(new KeyChord(Key.A, ModifierMask.Ctrl), InputAction.SelectionExamine)); // A held without Ctrl — chord doesn't match. kb.EmitKeyDown(Key.A, ModifierMask.None); Assert.False(dispatcher.IsActionHeld(InputAction.SelectionExamine)); // Now release A and press Ctrl+A. kb.EmitKeyUp(Key.A, ModifierMask.None); kb.EmitKeyDown(Key.A, ModifierMask.Ctrl); Assert.True(dispatcher.IsActionHeld(InputAction.SelectionExamine)); } [Fact] public void IsActionHeld_any_of_multiple_bindings_satisfies() { var (dispatcher, kb, _, bindings) = Build(); bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); bindings.Add(new Binding(new KeyChord(Key.Up, ModifierMask.None), InputAction.MovementForward)); kb.EmitKeyDown(Key.Up, ModifierMask.None); Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); kb.EmitKeyUp(Key.Up, ModifierMask.None); Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); kb.EmitKeyDown(Key.W, ModifierMask.None); Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); } [Fact] public void IsActionHeld_works_for_mouse_button_chord() { var (dispatcher, _, mouse, bindings) = Build(); var rmb = new KeyChord(InputDispatcher.MouseButtonToKey(MouseButton.Right), ModifierMask.None, Device: 1); bindings.Add(new Binding(rmb, InputAction.AcdreamRmbOrbitHold, ActivationType.Hold)); Assert.False(dispatcher.IsActionHeld(InputAction.AcdreamRmbOrbitHold)); mouse.EmitMouseDown(MouseButton.Right, ModifierMask.None); Assert.True(dispatcher.IsActionHeld(InputAction.AcdreamRmbOrbitHold)); mouse.EmitMouseUp(MouseButton.Right, ModifierMask.None); Assert.False(dispatcher.IsActionHeld(InputAction.AcdreamRmbOrbitHold)); } [Fact] public void IsActionHeld_returns_false_for_None_action() { var (dispatcher, _, _, _) = Build(); Assert.False(dispatcher.IsActionHeld(InputAction.None)); } [Fact] public void IsActionHeld_None_chord_remains_held_when_user_adds_Shift() { // K-fix3 (2026-04-26): Shift is the walk modifier in retail AC. // The user holding W (default = run) and then pressing Shift // should drop them to walk speed, NOT stop forward motion. Prior // to this fix, IsChordHeld required CurrentModifiers to match // chord.Modifiers EXACTLY — so (W, None) failed to match while // CurrentModifiers=Shift, and the player stopped on Shift-press. // Now: when chord requires no modifiers, Shift is allowed to // coexist (other modifiers — Ctrl, Alt, Win — still mismatch). var (dispatcher, kb, _, bindings) = Build(); bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); kb.EmitKeyDown(Key.W, ModifierMask.None); Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); // User now holds Shift while still holding W. CurrentModifiers // becomes Shift; W is still physically down. kb.CurrentModifiers = ModifierMask.Shift; Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); } [Fact] public void IsActionHeld_None_chord_does_not_fire_when_user_adds_Ctrl() { // Counterpart to the Shift test above: Ctrl is NOT a movement // modifier, so Ctrl+W should be a different chord. Without an // explicit (W, Ctrl) binding the action stays inactive — that's // what makes Ctrl+F* / Ctrl+1-9 / etc. distinct from the bare // F* / 1-9 chords. var (dispatcher, kb, _, bindings) = Build(); bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); kb.EmitKeyDown(Key.W, ModifierMask.None); Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); kb.CurrentModifiers = ModifierMask.Ctrl; Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); } [Fact] public void IsActionHeld_does_not_check_WantCaptureMouse() { // Per-frame held-state lookup is independent of UI capture: even // with WantCaptureMouse=true a movement key already held when // ImGui took focus continues to read as held until KeyUp. Press // events ARE gated (the Press wouldn't fire while UI captures), // but IsActionHeld answers the keyboard's underlying "is the // physical key down right now" — which the legacy IsKeyPressed // also did. The per-frame OnUpdate guard on // ImGui.GetIO().WantCaptureKeyboard is what suppresses movement // when chat is focused. var (dispatcher, kb, mouse, bindings) = Build(); bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward)); kb.EmitKeyDown(Key.W, ModifierMask.None); mouse.WantCaptureMouse = true; mouse.WantCaptureKeyboard = true; // Even with both capture flags set, IsActionHeld remains true // because W is physically held. The dispatcher only suppresses // press transitions. Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); } }