using System.Runtime.InteropServices; using System.Runtime.Versioning; using RioJoy.Core.Mapping; using MouseButtonId = RioJoy.Core.Mapping.MouseButton; namespace RioJoy.Core.Output; /// /// backed by the Win32 SendInput API. Keys are /// sent by scancode (VK → scancode via MapVirtualKey), matching the /// legacy Key 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). /// [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 inputs = stackalloc INPUT[1]; inputs[0] = input; SendInput(1, inputs, Marshal.SizeOf()); } // --- 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 pInputs, int cbSize); [DllImport("user32.dll")] private static extern uint MapVirtualKey(uint uCode, uint uMapType); }