Port the iRIO decode and Press_V2/Release_V2 routing into testable C#: - Mapping/: RioMapEntry decodes the 16-bit map word (lamp/mouse/hat/joy/extended/ alt/ctrl/shift flags + value) and resolves the routing Kind by the legacy precedence (joy+hat+mouse => RIO command; none => keyboard; else joy>hat>mouse). RioAddress translates button/keypad events to table addresses (+0x50/+0x60 keypad offsets); RioInputMap is the 112-entry per-profile table replacing the hard-coded iRIO[]. - InputRouter ports Press_V2/Release_V2: modifier press/release ordering, scancode keys, joystick buttons, POV hat, mouse move/click (deltas corrected per PROTOCOL.md), RIO-command dispatch, and lamp feedback (bright on press, dim on release; RIO commands carry none). InitializeLamps() dims all lamp entries. - Output is split behind sink interfaces (IInputSink/IJoystickSink/ILampSink/ IRioCommandSink) so routing is pure + unit-tested; Output/SendInputSink is the real SendInput keyboard/mouse adapter (scancode injection). - tests: 30 new xUnit tests (84 total) for entry decode, address translation, and router routing/precedence/lamp/modifier ordering via a recording sink. The joystick sink's real adapter (HID feeder -> RioGamepad via DeviceIoControl) is blocked on the Phase 1 driver; routing already targets IJoystickSink. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
5.0 KiB
C#
169 lines
5.0 KiB
C#
using RioJoy.Core.Mapping;
|
|
using Xunit;
|
|
|
|
namespace RioJoy.Core.Tests.Mapping;
|
|
|
|
public class InputRouterTests
|
|
{
|
|
private const byte SolidDim = 0x14;
|
|
private const byte SolidBright = 0x3C;
|
|
|
|
private static (InputRouter router, RecordingSink sink) Build(
|
|
int address, ushort raw, int mouseStep = 50)
|
|
{
|
|
var map = new RioInputMap { [address] = new RioMapEntry(raw) };
|
|
var sink = new RecordingSink();
|
|
var router = new InputRouter(map, sink, sink, sink, sink, mouseStep);
|
|
return (router, sink);
|
|
}
|
|
|
|
[Fact]
|
|
public void Keyboard_WithModifiers_PressesInOrderAndLights()
|
|
{
|
|
// 0x8749 = hasLamp + alt + ctrl + shift, VK 0x49
|
|
(InputRouter router, RecordingSink sink) = Build(0x05, 0x8749);
|
|
|
|
router.Press(0x05);
|
|
Assert.Equal(new[]
|
|
{
|
|
"KeyDown(0x10,ext=False)", // SHIFT
|
|
"KeyDown(0x11,ext=False)", // CTRL
|
|
"KeyDown(0x12,ext=False)", // ALT
|
|
"KeyDown(0x49,ext=False)", // key
|
|
$"Lamp(0x05,0x{SolidBright:X2})",
|
|
}, sink.Log);
|
|
|
|
sink.Log.Clear();
|
|
router.Release(0x05);
|
|
Assert.Equal(new[]
|
|
{
|
|
"KeyUp(0x49,ext=False)", // key first
|
|
"KeyUp(0x12,ext=False)", // then ALT, CTRL, SHIFT (reverse)
|
|
"KeyUp(0x11,ext=False)",
|
|
"KeyUp(0x10,ext=False)",
|
|
$"Lamp(0x05,0x{SolidDim:X2})",
|
|
}, sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void Keyboard_Extended_NoLamp()
|
|
{
|
|
// 0x08BE = extended, VK 0xBE, no lamp
|
|
(InputRouter router, RecordingSink sink) = Build(0x10, 0x08BE);
|
|
|
|
router.Press(0x10);
|
|
Assert.Equal(new[] { "KeyDown(0xBE,ext=True)" }, sink.Log);
|
|
|
|
sink.Log.Clear();
|
|
router.Release(0x10);
|
|
Assert.Equal(new[] { "KeyUp(0xBE,ext=True)" }, sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void Joystick_PressReleaseWithLamp()
|
|
{
|
|
// 0x9009 = hasLamp + joy, button 9
|
|
(InputRouter router, RecordingSink sink) = Build(0x03, 0x9009);
|
|
|
|
router.Press(0x03);
|
|
Assert.Equal(new[] { "Joy(9,True)", $"Lamp(0x03,0x{SolidBright:X2})" }, sink.Log);
|
|
|
|
sink.Log.Clear();
|
|
router.Release(0x03);
|
|
Assert.Equal(new[] { "Joy(9,False)", $"Lamp(0x03,0x{SolidDim:X2})" }, sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void Hat_PressSetsDirection_ReleaseCenters()
|
|
{
|
|
// 0x2002 = hat, value 2 = Down
|
|
(InputRouter router, RecordingSink sink) = Build(0x04, 0x2002);
|
|
|
|
router.Press(0x04);
|
|
Assert.Equal(new[] { "Hat(Down)" }, sink.Log);
|
|
|
|
sink.Log.Clear();
|
|
router.Release(0x04);
|
|
Assert.Equal(new[] { "Hat(Centered)" }, sink.Log);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x4000, "MouseMove(0,-50)")] // up
|
|
[InlineData(0x4001, "MouseMove(50,0)")] // right
|
|
[InlineData(0x4002, "MouseMove(0,50)")] // down
|
|
[InlineData(0x4003, "MouseMove(-50,0)")] // left
|
|
public void Mouse_MovePressOnly(ushort raw, string expectedMove)
|
|
{
|
|
(InputRouter router, RecordingSink sink) = Build(0x06, raw);
|
|
|
|
router.Press(0x06);
|
|
Assert.Equal(new[] { expectedMove }, sink.Log);
|
|
|
|
sink.Log.Clear();
|
|
router.Release(0x06); // moves do nothing on release
|
|
Assert.Empty(sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mouse_ClickDownAndUp()
|
|
{
|
|
(InputRouter router, RecordingSink sink) = Build(0x06, 0x4004); // left click
|
|
|
|
router.Press(0x06);
|
|
router.Release(0x06);
|
|
Assert.Equal(new[] { "MouseButton(Left,True)", "MouseButton(Left,False)" }, sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mouse_MoveStepIsConfigurable()
|
|
{
|
|
(InputRouter router, RecordingSink sink) = Build(0x06, 0x4001, mouseStep: 10);
|
|
router.Press(0x06);
|
|
Assert.Equal(new[] { "MouseMove(10,0)" }, sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void RioCommand_Executes_AndSkipsLampEvenIfFlagged()
|
|
{
|
|
// 0xF006 = hasLamp + joy + hat + mouse (command), value 6 = GeneralReset
|
|
(InputRouter router, RecordingSink sink) = Build(0x07, 0xF006);
|
|
|
|
router.Press(0x07);
|
|
Assert.Equal(new[] { "Cmd(GeneralReset)" }, sink.Log); // no lamp
|
|
|
|
sink.Log.Clear();
|
|
router.Release(0x07);
|
|
Assert.Empty(sink.Log); // release does nothing
|
|
}
|
|
|
|
[Fact]
|
|
public void Unmapped_Entry_DoesNothing()
|
|
{
|
|
(InputRouter router, RecordingSink sink) = Build(0x08, 0x0000);
|
|
router.Press(0x08);
|
|
router.Release(0x08);
|
|
Assert.Empty(sink.Log);
|
|
}
|
|
|
|
[Fact]
|
|
public void InitializeLamps_DimsOnlyLampEntries()
|
|
{
|
|
var map = new RioInputMap
|
|
{
|
|
[0x00] = new RioMapEntry(0x8041), // hasLamp keyboard
|
|
[0x01] = new RioMapEntry(0x0041), // no lamp
|
|
[0x02] = new RioMapEntry(0x9009), // hasLamp joystick
|
|
};
|
|
var sink = new RecordingSink();
|
|
var router = new InputRouter(map, sink, sink, sink, sink);
|
|
|
|
router.InitializeLamps();
|
|
|
|
Assert.Equal(new[]
|
|
{
|
|
$"Lamp(0x00,0x{SolidDim:X2})",
|
|
$"Lamp(0x02,0x{SolidDim:X2})",
|
|
}, sink.Log);
|
|
}
|
|
}
|