using System;
using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Physics;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
using Xunit;
namespace AcDream.Core.Tests.Input;
///
/// K.1b integration: drive the via the
/// public API a fake keyboard would, build a
/// from IsActionHeld queries (the new K.1b code path), and confirm
/// produces the same result
/// as feeding it the equivalent MovementInput directly. This is
/// the regression-prevention test for the "preserve the boundary" rule
/// in the K.1b plan: MovementInput stays as the contract; only the
/// SOURCE of input changes.
///
public class DispatcherToMovementIntegrationTests
{
/// Test double — the same fake the UI.Abstractions tests use,
/// duplicated here because it's internal in that assembly.
private sealed class FakeKb : IKeyboardSource
{
public event Action? KeyDown;
public event Action? KeyUp;
private readonly System.Collections.Generic.HashSet _held = new();
public ModifierMask CurrentModifiers { get; set; } = ModifierMask.None;
public bool IsHeld(Key k) => _held.Contains(k);
public void Press(Key k, ModifierMask mods = ModifierMask.None)
{
CurrentModifiers = mods;
_held.Add(k);
KeyDown?.Invoke(k, mods);
}
public void Release(Key k, ModifierMask mods = ModifierMask.None)
{
CurrentModifiers = mods;
_held.Remove(k);
KeyUp?.Invoke(k, mods);
}
}
#pragma warning disable CS0067 // events declared on the interface but unused in this fake
private sealed class FakeMouse : IMouseSource
{
public event Action? MouseDown;
public event Action? MouseUp;
public event Action? MouseMove;
public event Action? Scroll;
public bool IsHeld(MouseButton b) => false;
public bool WantCaptureMouse { get; set; }
public bool WantCaptureKeyboard { get; set; }
}
#pragma warning restore CS0067
private static PhysicsEngine MakeFlatEngine()
{
var engine = new PhysicsEngine();
var heights = new byte[81];
Array.Fill(heights, (byte)50);
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = i * 1f;
var terrain = new TerrainSurface(heights, heightTable);
engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty(),
Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f);
return engine;
}
private static MovementInput BuildInputFromDispatcher(InputDispatcher d) =>
new MovementInput(
Forward: d.IsActionHeld(InputAction.MovementForward),
Backward: d.IsActionHeld(InputAction.MovementBackup),
StrafeLeft: d.IsActionHeld(InputAction.MovementStrafeLeft),
StrafeRight: d.IsActionHeld(InputAction.MovementStrafeRight),
TurnLeft: d.IsActionHeld(InputAction.MovementTurnLeft),
TurnRight: d.IsActionHeld(InputAction.MovementTurnRight),
Run: d.IsActionHeld(InputAction.MovementRunLock),
MouseDeltaX: 0f, // K.1b: mouse never drives character yaw
Jump: d.IsActionHeld(InputAction.MovementJump));
[Fact]
public void Dispatcher_W_held_produces_forward_motion()
{
var engine = MakeFlatEngine();
var controller = new PlayerMovementController(engine);
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
controller.Yaw = 0f; // facing +X
var kb = new FakeKb();
var mouse = new FakeMouse();
var bindings = KeyBindings.AcdreamCurrentDefaults();
var dispatcher = new InputDispatcher(kb, mouse, bindings);
kb.Press(Key.W);
var input = BuildInputFromDispatcher(dispatcher);
Assert.True(input.Forward);
Assert.False(input.Run);
var result = controller.Update(1.0f, input);
Assert.True(result.Position.X > 96f + 2f, $"X={result.Position.X} should have moved forward");
}
[Fact]
public void Dispatcher_W_then_Shift_gives_running_motion()
{
var engine = MakeFlatEngine();
var controller = new PlayerMovementController(engine);
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
controller.Yaw = 0f;
var kb = new FakeKb();
var mouse = new FakeMouse();
var bindings = KeyBindings.AcdreamCurrentDefaults();
var dispatcher = new InputDispatcher(kb, mouse, bindings);
kb.Press(Key.W);
// Shift pressed alongside W — real keyboard delivers KeyDown(Shift,
// mods=Shift) and CurrentModifiers reflects Shift held.
kb.Press(Key.ShiftLeft, ModifierMask.Shift);
var input = BuildInputFromDispatcher(dispatcher);
Assert.True(input.Forward); // duplicate (W, Shift) binding catches this
Assert.True(input.Run); // (ShiftLeft, Shift) binding
}
[Fact]
public void Dispatcher_W_release_clears_forward()
{
var kb = new FakeKb();
var mouse = new FakeMouse();
var bindings = KeyBindings.AcdreamCurrentDefaults();
var dispatcher = new InputDispatcher(kb, mouse, bindings);
kb.Press(Key.W);
Assert.True(BuildInputFromDispatcher(dispatcher).Forward);
kb.Release(Key.W);
Assert.False(BuildInputFromDispatcher(dispatcher).Forward);
}
[Fact]
public void MovementInput_with_mouse_delta_zero_matches_input_with_mouse_delta_nonzero()
{
// K.1b regression-prevention: mouse delta no longer drives character
// yaw. Two MovementInputs identical except for MouseDeltaX produce
// identical motion-command bytes (ForwardCommand / ForwardSpeed /
// SidestepCommand / TurnCommand). Yaw still changes — but only by
// a hair from MouseDeltaX, which is dropped in K.1b.
//
// We construct the controller twice (separate state) so the previous
// frame's MouseDeltaX doesn't leak into the second run via Yaw.
var engineA = MakeFlatEngine();
var ctrlA = new PlayerMovementController(engineA);
ctrlA.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
ctrlA.Yaw = 0f;
var engineB = MakeFlatEngine();
var ctrlB = new PlayerMovementController(engineB);
ctrlB.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
ctrlB.Yaw = 0f;
var inputZero = new MovementInput(Forward: true, MouseDeltaX: 0f);
var inputJittered = new MovementInput(Forward: true, MouseDeltaX: 47.3f);
var rA = ctrlA.Update(0.05f, inputZero);
var rB = ctrlB.Update(0.05f, inputJittered);
Assert.Equal(rA.ForwardCommand, rB.ForwardCommand);
Assert.Equal(rA.SidestepCommand, rB.SidestepCommand);
Assert.Equal(rA.TurnCommand, rB.TurnCommand);
Assert.Equal(rA.ForwardSpeed, rB.ForwardSpeed);
}
[Fact]
public void Dispatcher_no_keys_held_produces_idle_input()
{
var kb = new FakeKb();
var mouse = new FakeMouse();
var bindings = KeyBindings.AcdreamCurrentDefaults();
var dispatcher = new InputDispatcher(kb, mouse, bindings);
var input = BuildInputFromDispatcher(dispatcher);
Assert.False(input.Forward);
Assert.False(input.Backward);
Assert.False(input.StrafeLeft);
Assert.False(input.StrafeRight);
Assert.False(input.TurnLeft);
Assert.False(input.TurnRight);
Assert.False(input.Run);
Assert.False(input.Jump);
Assert.Equal(0f, input.MouseDeltaX);
}
}