using System.Collections.Generic; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.UI.Abstractions.Tests.Input; /// /// Issue #358 — Ctrl+M () /// never fired from a live session's , despite /// the downstream GameplayInputCommandController callback being /// verified correct and the chord's modifier matching being verified /// correct in isolation. /// /// /// Root cause, found by the first test below failing against the FULL /// production wiring shape (real , /// not an ad-hoc test binding; the dispatcher's actual default scope stack — /// production never calls PushScope/PopScope anywhere, grepped /// clean across src/AcDream.App, so the stack is permanently /// + , exactly /// what the constructor pushes; and a synthetic Ctrl+M keydown): the Ctrl+M /// binding existed ONLY in /// (the pre-K.1c WASD-only preset, preserved solely as a regression anchor /// for tests pinning that older layout — its own doc comment says outright /// it is "NOT the GameWindow startup source after K.1c"). Production loads /// bindings through , which falls back /// to when no keybinds.json /// exists on disk (the verified state on this machine) — and /// never carried the Ctrl+M binding /// over. This is the same class of defect as the a5a7eb4f jump fix — /// two construction paths for the same table shape, only one of them wired /// to production — except here it is two DEFAULT-BINDING-SET methods rather /// than two controller instances, and the binding was simply added to the /// wrong one. Fixed by adding the chord to /// alongside the other /// Acdream* debug/utility actions relocated there to avoid retail /// conflicts (KeyBindings.cs, the "Acdream debug actions" block) — retail's /// own keymap (docs/research/named-retail/retail-default.keymap.txt) /// has no Ctrl+M binding at all, so this doesn't collide with anything /// retail-faithful. /// /// public sealed class MuteChordDispatchTests { private static (InputDispatcher dispatcher, FakeKeyboardSource kb, FakeMouseSource mouse, List<(InputAction Action, ActivationType Activation)> fired) BuildWithRetailDefaults() { var kb = new FakeKeyboardSource(); var mouse = new FakeMouseSource(); // WantCaptureKeyboard defaults to false — no widget focused. KeyBindings bindings = KeyBindings.RetailDefaults(); InputDispatcher dispatcher = InputDispatcher.CreateDetached(kb, mouse, bindings); dispatcher.Attach(); // dispatcher constructor already pushes [Always, Game] — the // production default; nothing in src/AcDream.App ever pushes // another scope on top of it. var fired = new List<(InputAction, ActivationType)>(); dispatcher.Fired += (a, t) => fired.Add((a, t)); return (dispatcher, kb, mouse, fired); } [Fact] public void CtrlM_WithNoWidgetFocused_FiresAcdreamToggleAudioMute() { // The baseline production shape: KeyBindings.LoadOrDefault's fallback // (RetailDefaults, matching the verified "no keybinds.json on disk" // state), the dispatcher's real default [Always, Game] scope stack, // nothing capturing the keyboard. This is the regression pin for // #358's fix — before the fix, RetailDefaults carried no binding for // AcdreamToggleAudioMute at all and this assertion failed with an // EMPTY fired collection (not a wrong-action mismatch), proving the // gap was a missing table entry, not a matching/scope/capture bug. (_, FakeKeyboardSource kb, _, var fired) = BuildWithRetailDefaults(); kb.EmitKeyDown(Key.M, ModifierMask.Ctrl); Assert.Contains((InputAction.AcdreamToggleAudioMute, ActivationType.Press), fired); } [Fact] public void CtrlM_WhileAnyWidgetHoldsKeyboardFocus_IsSuppressed() { // Supporting evidence gathered during the #358 investigation, kept as // a regression pin in its own right: InputDispatcher.OnKeyDown // (src/AcDream.UI.Abstractions/Input/InputDispatcher.cs) returns // BEFORE calling FindActive at all when _mouse.WantCaptureKeyboard is // true. Production wires WantCaptureKeyboard to UiRoot.WantsKeyboard, // which is "KeyboardFocus is not null" — ANY focused widget, not // just a text-entry field specifically (the only AcceptsFocus=true // widget type in the retained UI is UiField, so in practice this // means any focused text field, e.g. chat entry or a Settings // numeric box). This gate is real and total, but it was RULED OUT as // #358's root cause: the baseline test above reproduced the bug with // nothing focused at all. (_, FakeKeyboardSource kb, FakeMouseSource mouse, var fired) = BuildWithRetailDefaults(); mouse.WantCaptureKeyboard = true; kb.EmitKeyDown(Key.M, ModifierMask.Ctrl); Assert.DoesNotContain((InputAction.AcdreamToggleAudioMute, ActivationType.Press), fired); } }