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>
138 lines
4.1 KiB
C#
138 lines
4.1 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Runtime.Versioning;
|
|
using RioJoy.Core.Mapping;
|
|
using MouseButtonId = RioJoy.Core.Mapping.MouseButton;
|
|
|
|
namespace RioJoy.Core.Output;
|
|
|
|
/// <summary>
|
|
/// <see cref="IInputSink"/> backed by the Win32 <c>SendInput</c> API. Keys are
|
|
/// sent by <b>scancode</b> (VK → scancode via <c>MapVirtualKey</c>), matching the
|
|
/// legacy <c>Key</c> function (riovjoy2.cpp#L2055), so games that read raw
|
|
/// scancodes (most do) see the keypress. Targets the interactive session, which
|
|
/// is why RIOJoy is a tray app rather than a service (see docs/PLAN.md risks).
|
|
/// </summary>
|
|
[SupportedOSPlatform("windows")]
|
|
public sealed class SendInputSink : IInputSink
|
|
{
|
|
public void KeyDown(byte virtualKey, bool extended) => SendKey(virtualKey, extended, keyUp: false);
|
|
|
|
public void KeyUp(byte virtualKey, bool extended) => SendKey(virtualKey, extended, keyUp: true);
|
|
|
|
public void MouseMove(int dx, int dy)
|
|
{
|
|
var input = new INPUT
|
|
{
|
|
type = INPUT_MOUSE,
|
|
U = { mi = { dx = dx, dy = dy, dwFlags = MOUSEEVENTF_MOVE } },
|
|
};
|
|
Send(input);
|
|
}
|
|
|
|
public void MouseButton(MouseButton button, bool down)
|
|
{
|
|
uint flags = (button, down) switch
|
|
{
|
|
(MouseButtonId.Left, true) => MOUSEEVENTF_LEFTDOWN,
|
|
(MouseButtonId.Left, false) => MOUSEEVENTF_LEFTUP,
|
|
(MouseButtonId.Right, true) => MOUSEEVENTF_RIGHTDOWN,
|
|
(MouseButtonId.Right, false) => MOUSEEVENTF_RIGHTUP,
|
|
_ => 0,
|
|
};
|
|
|
|
var input = new INPUT
|
|
{
|
|
type = INPUT_MOUSE,
|
|
U = { mi = { dwFlags = flags } },
|
|
};
|
|
Send(input);
|
|
}
|
|
|
|
private static void SendKey(byte virtualKey, bool extended, bool keyUp)
|
|
{
|
|
uint flags = KEYEVENTF_SCANCODE;
|
|
if (keyUp) flags |= KEYEVENTF_KEYUP;
|
|
if (extended) flags |= KEYEVENTF_EXTENDEDKEY;
|
|
|
|
var input = new INPUT
|
|
{
|
|
type = INPUT_KEYBOARD,
|
|
U =
|
|
{
|
|
ki = new KEYBDINPUT
|
|
{
|
|
wVk = 0,
|
|
wScan = (ushort)MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC),
|
|
dwFlags = flags,
|
|
},
|
|
},
|
|
};
|
|
Send(input);
|
|
}
|
|
|
|
private static void Send(INPUT input)
|
|
{
|
|
Span<INPUT> inputs = stackalloc INPUT[1];
|
|
inputs[0] = input;
|
|
SendInput(1, inputs, Marshal.SizeOf<INPUT>());
|
|
}
|
|
|
|
// --- Win32 interop --------------------------------------------------------
|
|
|
|
private const uint INPUT_MOUSE = 0;
|
|
private const uint INPUT_KEYBOARD = 1;
|
|
|
|
private const uint KEYEVENTF_EXTENDEDKEY = 0x0001;
|
|
private const uint KEYEVENTF_KEYUP = 0x0002;
|
|
private const uint KEYEVENTF_SCANCODE = 0x0008;
|
|
|
|
private const uint MOUSEEVENTF_MOVE = 0x0001;
|
|
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
|
|
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
|
|
private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
|
|
private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
|
|
|
|
private const uint MAPVK_VK_TO_VSC = 0;
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct INPUT
|
|
{
|
|
public uint type;
|
|
public InputUnion U;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Explicit)]
|
|
private struct InputUnion
|
|
{
|
|
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MOUSEINPUT
|
|
{
|
|
public int dx;
|
|
public int dy;
|
|
public uint mouseData;
|
|
public uint dwFlags;
|
|
public uint time;
|
|
public nuint dwExtraInfo;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct KEYBDINPUT
|
|
{
|
|
public ushort wVk;
|
|
public ushort wScan;
|
|
public uint dwFlags;
|
|
public uint time;
|
|
public nuint dwExtraInfo;
|
|
}
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern uint SendInput(uint nInputs, Span<INPUT> pInputs, int cbSize);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint MapVirtualKey(uint uCode, uint uMapType);
|
|
}
|