Removes the parallel direct keyboard/mouse polling that K.1a left in
GameWindow alongside the new dispatcher. Now every input flows
through InputDispatcher; legacy IsKeyPressed/KeyDown/MouseDown/MouseUp/
Scroll handlers in GameWindow are deleted (~220-line refactor).
Bindings remain acdream-current (W/S/A/D/Z/X movement, Shift run,
F-key debug surface). K.1c flips them to retail.
Pieces:
- InputDispatcher.IsActionHeld(InputAction): per-frame held-state
query for movement (W/X/A/D/Z/X/Shift/Space) so PlayerMovement-
Controller can read action state without polling raw keys.
Internally walks all bindings for the action; chord match
requires modifier mask exactness.
- InputAction adds AcdreamRmbOrbitHold (Hold-activation, RMB held
drives chase-camera orbit) and AcdreamFlyDown (Ctrl held in fly
mode for descent).
- GameWindow OnInputAction subscriber replaces the entire KeyDown
switch + per-mouse-button handlers. Single dispatcher event drives:
- F1 AcdreamToggleDebugPanel
- F2 AcdreamToggleCollisionWires
- F3 AcdreamDumpNearby
- F7 AcdreamCycleTimeOfDay
- F8 AcdreamSensitivityDown
- F9 AcdreamSensitivityUp
- F10 AcdreamCycleWeather
- F AcdreamToggleFlyMode
- Tab AcdreamTogglePlayerMode (player/fly toggle - K.1c will
reassign this to ToggleChatEntry)
- Esc EscapeKey (cancel fly mode etc.)
- Mouse wheel ScrollUp/ScrollDown (camera zoom)
- RMB held (Hold) drives orbit; LMB drag still drives orbit
camera; mouse position handled by surviving MouseMove handler
which is gated on ImGui WantCaptureMouse.
- MovementInput per-frame: reads from _inputDispatcher.IsActionHeld.
MouseDeltaX hardcoded to 0f (mouse never drives character yaw).
_playerMouseDeltaX field stays defined for chase-camera RMB-orbit
but is never consumed by movement.
- WantCaptureMouse explicit gate at the top of every surviving mouse
handler in GameWindow (defense in depth - dispatcher already gates
via IMouseSource.WantCaptureMouse).
Movement-input boundary preserved: PlayerMovementController.Update
still takes the same MovementInput struct. Existing
PlayerMovementControllerTests continue green - no regression in
motion-command byte production.
Two deviations:
1. Scroll lost magnitude going through the dispatcher (fixed-step
zoom). Acceptable - discrete wheel-tick matches retail feel
anyway.
2. Movement chords are duplicated with both ModifierMask.None and
ModifierMask.Shift (covering "shift held to run while walking
forward" etc.) so the dispatcher's modifier-strict matching
preserves the modifier-blind feel of the old IsKeyPressed
polling. Will be reshaped cleanly in K.1c when retail's
walk-modifier semantics flip (default = run, shift held = walk).
15 new tests:
- InputDispatcherIsActionHeldTests: 7 cases covering chord-held +
release + modifier-mismatch + multi-binding-for-action.
- InputDispatcherTests: 3 scroll-action cases.
- DispatcherToMovementIntegrationTests (Core.Tests): 5 cases
proving FakeKeyboardSource.Press(W) -> dispatcher.IsActionHeld ->
MovementInput.Forward -> PlayerMovementController produces the
expected motion-command bytes. Includes the regression-prevention
test that mouse-X delta value (zero vs nonzero) doesn't affect
the motion bytes.
Solution total: 1133 green (243 Core.Net + 225 UI + 665 Core),
0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the abstraction without changing user-visible behavior.
Existing keyboard/mouse handlers in GameWindow continue working
unchanged. The new InputDispatcher runs alongside, fires
InputAction events, and a diagnostic Console.WriteLine subscriber
proves the path is observable. K.1b cuts the existing handlers
over; K.1c flips bindings to retail.
New types in src/AcDream.UI.Abstractions/Input/:
- InputAction enum (~110 actions, doc-grouped by retail keymap
category: MovementCommands, ItemSelectionCommands, UICommands,
QuickslotCommands, Chat, Combat, Emotes, Camera, Scroll, Mouse
selection, plus Acdream-specific debug actions for the existing
F-key behaviors)
- KeyChord record struct (Silk.NET.Input.Key + ModifierMask + Device)
- ModifierMask [Flags] enum matching retail keymap bit values
(Shift=0x01, Ctrl=0x02, Alt=0x04, Win=0x08)
- ActivationType enum (Press, Release, Hold, DoubleClick, Analog)
- Binding record (chord -> action -> activation)
- InputScope enum with stack semantics (Always at bottom, Game on
top during normal play; Chat / EditField / Dialog / MeleeCombat /
MissileCombat / MagicCombat / Camera push as transient overlays)
- KeyBindings collection class with Find / ForAction / Add / Remove.
AcdreamCurrentDefaults() factory matches today's hardcoded binds
(W/S/A/D/Z/X movement, Shift run, F-key debug surface) so K.1a
doesn't change behavior. RetailDefaults() is K.1c's job; for now
it returns the same map.
- IKeyboardSource / IMouseSource - test-fakeable interfaces wrapping
Silk.NET. Both surface WantCaptureMouse / WantCaptureKeyboard
flags so the dispatcher can gate per ImGui state.
- InputDispatcher: multicast event Fired<InputAction, ActivationType>;
scope stack with PushScope/PopScope/ActiveScope; per-frame Tick()
fires Hold-type bindings for currently-held chords; mouse buttons
encoded as KeyChord with Device=1.
New adapters in src/AcDream.App/Input/:
- SilkKeyboardSource - Silk.NET IKeyboard wrapper, tracks held state
- SilkMouseSource - Silk.NET IMouse wrapper, proxies ImGui WantCapture
flags for both keyboard and mouse
GameWindow.cs:
- Constructs adapters + dispatcher in OnLoad
- Subscribes to dispatcher.Fired with diagnostic Console.WriteLine
("[input] {action} {activation}") so the path is observable in
launch.log without touching any actual game state
- Calls _inputDispatcher.Tick() per frame in OnUpdate
- Existing IsKeyPressed and event handlers unchanged
Memory crib at memory/project_input_pipeline.md describes the five
layers (Silk events -> Source interfaces -> Dispatcher -> Action
events -> Subscribers) with file paths + scope semantics + the K.1c
retail-defaults plan. Indexed in MEMORY.md.
Two deviations from plan, both documented:
1. InputDispatcher placed in UI.Abstractions/Input/ rather than
App/Input/ - it has no Silk dependencies (uses only the test-
fakeable interfaces) and the test fakes live in
UI.Abstractions.Tests. Mirrors LiveCommandBus precedent. Silk
adapters + GameWindow wiring stay in App.
2. WantCaptureKeyboard moved to IMouseSource alongside WantCaptureMouse
(the dispatcher needs both at the same point).
34 new tests covering KeyChord equality, ModifierMask flags,
KeyBindings lookup, dispatcher chord matching with modifier
mismatch rejection, Hold-type Press/Release transitions, Tick()
firing held bindings, scope stack push/pop with mismatched-pop
throwing, WantCapture* gating.
Solution total: 1118 green (243 Core.Net + 215 UI + 660 Core),
0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>