fix(input): #358 — RetailDefaults() never carried the Ctrl+M mute binding over from AcdreamCurrentDefaults()

Root cause, confirmed by a full-production-wiring repro test rather than
guessed: InputAction.AcdreamToggleAudioMute was bound to Ctrl+M only in
KeyBindings.AcdreamCurrentDefaults() -- the pre-K.1c WASD-only preset,
whose own doc comment says it is preserved solely as a regression anchor
and is explicitly NOT the GameWindow startup source after K.1c.
KeyBindings.RetailDefaults() -- what KeyBindings.LoadOrDefault actually
falls back to when no keybinds.json exists on disk (the verified state
on the affected machine) -- has its own "Acdream debug actions" block
(Ctrl+F1/F2/F3/F7/F8/F9/F10, Ctrl+Shift+F) but never carried the Ctrl+M
mute binding over into it. The live dispatcher therefore had no Ctrl+M
entry in its binding table at all -- not a modifier-matching bug, not a
scope bug, not a retained-UI-capture bug. Same class as the a5a7eb4f
jump fix (two construction paths, one wired to production), except here
it's two default-binding-set methods rather than two controller
instances, and the binding was simply added to the wrong one. This also
explains the prior "loaded 152 bindings both before and after" mystery:
the count correctly didn't change, because the earlier addition went
into a method nothing in production loads or counts.

MuteChordDispatchTests.CtrlM_WithNoWidgetFocused_FiresAcdreamToggleAudioMute
reproduces the full production shape (real RetailDefaults(), the
dispatcher's actual default [Always, Game] scope stack -- production
never calls PushScope/PopScope anywhere, grepped clean across
src/AcDream.App -- and a synthetic Ctrl+M keydown) and failed with an
EMPTY fired collection before this fix, which is what pinpointed
"missing table entry" over the other ranked hypotheses. A second test,
CtrlM_WhileAnyWidgetHoldsKeyboardFocus_IsSuppressed, pins a separate but
real mechanism found along the way (InputDispatcher.OnKeyDown returns
before FindActive when WantCaptureKeyboard is true, and production wires
that to "any focused widget", not just chat text entry) that was ruled
out as #358's cause since the baseline repro failed with nothing
focused.

Fix: KeyBindings.RetailDefaults() now also binds Ctrl+M to
AcdreamToggleAudioMute. Retail's own keymap has no Ctrl+M binding, so
this doesn't collide with anything retail-faithful. #358 closed in
ISSUES.md with the confirmed mechanism; connected verification (does
Ctrl+M actually mute in a live client) is still owed -- this session's
hard constraints excluded client launches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 14:45:51 +02:00
parent cc58289967
commit dda76d9faf
3 changed files with 162 additions and 28 deletions

View file

@ -358,6 +358,15 @@ public sealed class KeyBindings
b.Add(new(new KeyChord(Key.F8, ModifierMask.Ctrl), InputAction.AcdreamSensitivityDown));
b.Add(new(new KeyChord(Key.F9, ModifierMask.Ctrl), InputAction.AcdreamSensitivityUp));
b.Add(new(new KeyChord(Key.F10, ModifierMask.Ctrl), InputAction.AcdreamCycleWeather));
// #358: AcdreamToggleAudioMute was bound in AcdreamCurrentDefaults()
// (line ~123 above) but never carried over here when K.1c made
// RetailDefaults the GameWindow startup source — AcdreamCurrentDefaults
// is preserved only as a WASD-layout regression anchor and is not
// loaded in production, so the chord was registered in a table the
// live dispatcher never consults. Retail's own keymap has no Ctrl+M
// binding (grepped retail-default.keymap.txt clean), so this doesn't
// collide with anything retail-faithful.
b.Add(new(new KeyChord(Key.M, ModifierMask.Ctrl), InputAction.AcdreamToggleAudioMute));
// K-fix2 (2026-04-26): free-fly toggle keyboard shortcut.
// Retail leaves Ctrl+Shift+F unbound (retail F = SelectionPickUp,