using AcDream.App.Input; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.App.Tests.Input; public sealed class DispatcherMovementInputSourceTests { [Fact] public void UnboundSourceCapturesNeutralInput() { var source = CreateSource(); Assert.Equal(default, source.Capture()); } [Fact] public void CapturesDispatcherHeldStateAndRetailWalkModifier() { var (dispatcher, _, _) = CreateDispatcher(); var source = CreateSource(); source.Bind(dispatcher); Assert.True(dispatcher.TrySetAutomationActionHeld( InputAction.MovementForward, held: true)); Assert.True(dispatcher.TrySetAutomationActionHeld( InputAction.MovementWalkMode, held: true)); MovementInput captured = source.Capture(); Assert.True(captured.Forward); Assert.False(captured.Run); } [Fact] public void RetainedKeyboardCaptureSilencesHeldKeysButDoesNotCancelAutorun() { var (dispatcher, _, mouse) = CreateDispatcher(); var source = CreateSource(); source.Bind(dispatcher); dispatcher.TrySetAutomationActionHeld(InputAction.MovementForward, held: true); Assert.True(source.HandlePressedAction(InputAction.MovementRunLock)); mouse.WantCaptureKeyboard = true; MovementInput captured = source.Capture(); Assert.True(captured.Forward); Assert.True(source.AutoRunActive); source.HandlePressedAction(InputAction.MovementRunLock); captured = source.Capture(); Assert.False(captured.Forward); } [Fact] public void DevToolsKeyboardCapturePausesAutorunWithoutClearingItsLatch() { var capture = new FakeCapture(); var (dispatcher, _, _) = CreateDispatcher(); var source = CreateSource(capture); source.Bind(dispatcher); source.HandlePressedAction(InputAction.MovementRunLock); capture.DevToolsWantCaptureKeyboard = true; Assert.Equal(default, source.Capture()); Assert.True(source.AutoRunActive); capture.DevToolsWantCaptureKeyboard = false; Assert.True(source.Capture().Forward); } [Theory] [InlineData(InputAction.MovementBackup)] [InlineData(InputAction.MovementStop)] [InlineData(InputAction.MovementStrafeLeft)] [InlineData(InputAction.MovementStrafeRight)] public void RetailCancelActionsClearAutorun(InputAction action) { var source = CreateSource(); source.HandlePressedAction(InputAction.MovementRunLock); Assert.False(source.HandlePressedAction(action)); Assert.False(source.AutoRunActive); } [Fact] public void ForwardCancelsAutorun_AndResetClearsIt() { // Movement parity audit 2026-07-30 — retail // CommandInterpreter::HandleNewForwardMovement (0x006b3d60) is // literally `SetAutoRun(0, 1)`: EVERY fresh forward press disables // autorun and hands control back to the key. The previous pin // ("ForwardDoesNotCancelAutorun") codified the divergence. var source = CreateSource(); source.HandlePressedAction(InputAction.MovementRunLock); Assert.True(source.AutoRunActive); source.HandlePressedAction(InputAction.MovementForward); Assert.False(source.AutoRunActive); source.HandlePressedAction(InputAction.MovementRunLock); Assert.True(source.AutoRunActive); source.ResetSession(); Assert.False(source.AutoRunActive); } // ── S6 (OP4 review-fix round blast, 2026-08-11): the option × modifier // truth table for Run — previously pinned only at option-unbound // (⇒ true) + walk-held. ────────────────────────────────────────── [Theory] [InlineData(true, false, true)] // run-by-default, no walk modifier -> runs [InlineData(true, true, false)] // run-by-default, walk modifier held -> walks [InlineData(false, false, false)] // walk-by-default, no modifier -> walks [InlineData(false, true, true)] // walk-by-default, modifier held -> runs public void Capture_RunReflectsOptionXorWalkModifier( bool runAsDefault, bool walkModifierHeld, bool expectedRun) { var (dispatcher, _, _) = CreateDispatcher(); var movement = new RuntimeLocalPlayerMovementState { RunAsDefaultMovementSource = () => runAsDefault, }; var source = new DispatcherMovementInputSource(movement); source.Bind(dispatcher); dispatcher.TrySetAutomationActionHeld(InputAction.MovementForward, held: true); if (walkModifierHeld) dispatcher.TrySetAutomationActionHeld(InputAction.MovementWalkMode, held: true); MovementInput captured = source.Capture(); Assert.Equal(expectedRun, captured.Run); } [Fact] public void Capture_AutoRunActive_ForcesRun_EvenWhenOptionIsOff() { // The `|| AutoRunActive` clause predates OP4 but was unobservable // while the default was hardcoded true. Pinned as a // CURRENT-BEHAVIOR test (not a retail-correctness claim — see // mechanism review N6, which flags this as a possibly-divergent // "|| AutoRunActive" that retail's SetAutoRun does not force) so a // future change to this clause is deliberate, not accidental. var (dispatcher, _, _) = CreateDispatcher(); var movement = new RuntimeLocalPlayerMovementState { RunAsDefaultMovementSource = () => false, }; var source = new DispatcherMovementInputSource(movement); source.Bind(dispatcher); source.HandlePressedAction(InputAction.MovementRunLock); // arm autorun Assert.True(source.AutoRunActive); MovementInput captured = source.Capture(); Assert.True(captured.Run); } [Fact] public void BindingIsIdempotentOnlyForTheSameDispatcher() { var source = CreateSource(); var (first, _, _) = CreateDispatcher(); var (second, _, _) = CreateDispatcher(); source.Bind(first); source.Bind(first); Assert.Throws(() => source.Bind(second)); } [Fact] public void UnbindRequiresExactDispatcherAndRestoresNeutralCapture() { var source = CreateSource(); var (first, _, _) = CreateDispatcher(); var (other, _, _) = CreateDispatcher(); source.Bind(first); first.TrySetAutomationActionHeld(InputAction.MovementForward, held: true); source.Unbind(other); Assert.True(source.Capture().Forward); source.Unbind(first); Assert.False(source.IsAvailable); Assert.Equal(default, source.Capture()); } private static (InputDispatcher Dispatcher, FakeKeyboard Keyboard, FakeMouse Mouse) CreateDispatcher() { var keyboard = new FakeKeyboard(); var mouse = new FakeMouse(); var dispatcher = InputDispatcher.CreateDetached( keyboard, mouse, new KeyBindings()); dispatcher.Attach(); return (dispatcher, keyboard, mouse); } private static DispatcherMovementInputSource CreateSource( IInputCaptureSource? capture = null) => new(new RuntimeLocalPlayerMovementState(), capture); private sealed class FakeKeyboard : IKeyboardSource { #pragma warning disable CS0067 public event Action? KeyDown; public event Action? KeyUp; #pragma warning restore CS0067 public bool IsHeld(Key key) => false; public ModifierMask CurrentModifiers => ModifierMask.None; } private sealed class FakeMouse : IMouseSource { #pragma warning disable CS0067 public event Action? MouseDown; public event Action? MouseUp; public event Action? MouseMove; public event Action? Scroll; #pragma warning restore CS0067 public bool WantCaptureKeyboard { get; set; } public bool WantCaptureMouse { get; set; } public bool IsHeld(MouseButton button) => false; } private sealed class FakeCapture : IInputCaptureSource { public bool WantCaptureMouse { get; set; } public bool WantCaptureKeyboard { get; set; } public bool DevToolsWantCaptureKeyboard { get; set; } } }