Ports CMotionInterp::get_adjusted_max_speed (0x00527D00, byte-decoded: bare rate unless RunForward; forward_speed x 4.0 when running; current_speed_factor proven a ctor-constant 1.0 at 0x00528C34) and swaps all five interpolation catch-up call sites to it - retail's fUseAdjustedSpeed_ static (.data 0x0081F418 = 1) makes this the live branch, so standing/walking remotes now catch up at ~2x runRate instead of 4x too fast (the #41/#165 presentation family). Autorun now hard- forces Run for its duration and cancels on every fresh forward press (CommandInterpreter::HandleNewForwardMovement 0x006b3d60 is literally SetAutoRun(0,1)); the old test pin codified the divergence. AP-30 retired: retail Frame::is_equal genuinely uses the 0.0002 epsilon - the row recorded a non-divergence. Three catch-up test pins re-baselined to retail semantics with citations. Full Release suite 9,983/0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
6 KiB
C#
186 lines
6 KiB
C#
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void BindingIsIdempotentOnlyForTheSameDispatcher()
|
|
{
|
|
var source = CreateSource();
|
|
var (first, _, _) = CreateDispatcher();
|
|
var (second, _, _) = CreateDispatcher();
|
|
|
|
source.Bind(first);
|
|
source.Bind(first);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => 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<Key, ModifierMask>? KeyDown;
|
|
public event Action<Key, ModifierMask>? 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<MouseButton, ModifierMask>? MouseDown;
|
|
public event Action<MouseButton, ModifierMask>? MouseUp;
|
|
public event Action<float, float>? MouseMove;
|
|
public event Action<float>? 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; }
|
|
}
|
|
}
|