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>
109 lines
4.3 KiB
C#
109 lines
4.3 KiB
C#
using System.Linq;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.UI.Abstractions.Tests.Input;
|
|
|
|
public class KeyBindingsTests
|
|
{
|
|
[Fact]
|
|
public void Add_appends_to_All()
|
|
{
|
|
var b = new KeyBindings();
|
|
var binding = new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward);
|
|
b.Add(binding);
|
|
Assert.Single(b.All);
|
|
Assert.Equal(binding, b.All[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Remove_drops_binding()
|
|
{
|
|
var b = new KeyBindings();
|
|
var binding = new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward);
|
|
b.Add(binding);
|
|
Assert.True(b.Remove(binding));
|
|
Assert.Empty(b.All);
|
|
}
|
|
|
|
[Fact]
|
|
public void Clear_empties_all()
|
|
{
|
|
var b = new KeyBindings();
|
|
b.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
b.Add(new Binding(new KeyChord(Key.S, ModifierMask.None), InputAction.MovementBackup));
|
|
b.Clear();
|
|
Assert.Empty(b.All);
|
|
}
|
|
|
|
[Fact]
|
|
public void Find_returns_matching_binding_by_chord_and_activation()
|
|
{
|
|
var b = new KeyBindings();
|
|
var press = new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward, ActivationType.Press);
|
|
var release = new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward, ActivationType.Release);
|
|
b.Add(press);
|
|
b.Add(release);
|
|
|
|
Assert.Equal(press, b.Find(new KeyChord(Key.W, ModifierMask.None), ActivationType.Press));
|
|
Assert.Equal(release, b.Find(new KeyChord(Key.W, ModifierMask.None), ActivationType.Release));
|
|
}
|
|
|
|
[Fact]
|
|
public void Find_returns_null_when_no_match()
|
|
{
|
|
var b = new KeyBindings();
|
|
b.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
Assert.Null(b.Find(new KeyChord(Key.S, ModifierMask.None), ActivationType.Press));
|
|
}
|
|
|
|
[Fact]
|
|
public void ForAction_returns_all_chords_bound_to_action()
|
|
{
|
|
var b = new KeyBindings();
|
|
// Retail-style: W and Up both → MovementForward.
|
|
b.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
b.Add(new Binding(new KeyChord(Key.Up, ModifierMask.None), InputAction.MovementForward));
|
|
b.Add(new Binding(new KeyChord(Key.S, ModifierMask.None), InputAction.MovementBackup));
|
|
|
|
var forward = b.ForAction(InputAction.MovementForward).ToList();
|
|
Assert.Equal(2, forward.Count);
|
|
Assert.Contains(forward, x => x.Chord.Key == Key.W);
|
|
Assert.Contains(forward, x => x.Chord.Key == Key.Up);
|
|
}
|
|
|
|
[Fact]
|
|
public void AcdreamCurrentDefaults_includes_WASD_movement()
|
|
{
|
|
var b = KeyBindings.AcdreamCurrentDefaults();
|
|
Assert.NotNull(b.Find(new KeyChord(Key.W, ModifierMask.None), ActivationType.Press));
|
|
Assert.NotNull(b.Find(new KeyChord(Key.S, ModifierMask.None), ActivationType.Press));
|
|
Assert.NotNull(b.Find(new KeyChord(Key.A, ModifierMask.None), ActivationType.Press));
|
|
Assert.NotNull(b.Find(new KeyChord(Key.D, ModifierMask.None), ActivationType.Press));
|
|
Assert.NotNull(b.Find(new KeyChord(Key.Z, ModifierMask.None), ActivationType.Press));
|
|
Assert.NotNull(b.Find(new KeyChord(Key.X, ModifierMask.None), ActivationType.Press));
|
|
}
|
|
|
|
[Fact]
|
|
public void AcdreamCurrentDefaults_binds_shift_as_hold_for_run()
|
|
{
|
|
// K.1b: when ShiftLeft is held the OS keyboard delivers
|
|
// CurrentModifiers=Shift, so the chord must be (ShiftLeft, Shift).
|
|
// Lookup with the matching modifier mask succeeds.
|
|
var b = KeyBindings.AcdreamCurrentDefaults();
|
|
var hold = b.Find(new KeyChord(Key.ShiftLeft, ModifierMask.Shift), ActivationType.Hold);
|
|
Assert.NotNull(hold);
|
|
Assert.Equal(InputAction.MovementRunLock, hold!.Value.Action);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailDefaults_proxies_AcdreamCurrentDefaults_in_K1a()
|
|
{
|
|
// K.1a stub — RetailDefaults() returns the acdream-current binds so
|
|
// K.1b cutover doesn't change behavior. K.1c flips this to the
|
|
// retail preset.
|
|
var retail = KeyBindings.RetailDefaults();
|
|
var current = KeyBindings.AcdreamCurrentDefaults();
|
|
Assert.Equal(current.All.Count, retail.All.Count);
|
|
}
|
|
}
|