acdream/tests/AcDream.App.Tests/Input/DispatcherMovementInputSourceTests.cs
Erik c0afcacbb2 fix(physics): movement-parity fixes - adjusted catch-up cap, autorun retail semantics, AP-30 retired
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>
2026-07-30 15:04:17 +02:00

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; }
}
}