Phase 3: input mapping + output routing (RioJoy.Core.Mapping)
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>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
using RioJoy.Core.Protocol;
|
||||
|
||||
namespace RioJoy.Core.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Decodes RIO button/keypad press & release events through a
|
||||
/// <see cref="RioInputMap"/> and routes them to the keyboard/mouse, joystick,
|
||||
/// lamp, and RIO-command sinks. Faithful port of <c>Press_V2</c>/<c>Release_V2</c>
|
||||
/// (riovjoy2.cpp#L1944); see docs/PROTOCOL.md §5.
|
||||
///
|
||||
/// <para>Routing precedence and lamp feedback follow the legacy exactly: a RIO
|
||||
/// command runs and returns (no lamp); keyboard/joystick/hat/mouse all apply lamp
|
||||
/// feedback when the entry has the lamp flag (bright on press, dim on release).
|
||||
/// Modifier keys are pressed before the key and released after it, in reverse.</para>
|
||||
/// </summary>
|
||||
public sealed class InputRouter
|
||||
{
|
||||
private readonly RioInputMap _map;
|
||||
private readonly IInputSink _input;
|
||||
private readonly IJoystickSink _joystick;
|
||||
private readonly ILampSink _lamp;
|
||||
private readonly IRioCommandSink _commands;
|
||||
private readonly int _mouseStep;
|
||||
|
||||
public InputRouter(
|
||||
RioInputMap map,
|
||||
IInputSink input,
|
||||
IJoystickSink joystick,
|
||||
ILampSink lamp,
|
||||
IRioCommandSink commands,
|
||||
int mouseMoveStep = 50)
|
||||
{
|
||||
_map = map ?? throw new ArgumentNullException(nameof(map));
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_joystick = joystick ?? throw new ArgumentNullException(nameof(joystick));
|
||||
_lamp = lamp ?? throw new ArgumentNullException(nameof(lamp));
|
||||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||
_mouseStep = mouseMoveStep;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every lighted button to its idle (dim) state. The legacy app does this
|
||||
/// at startup for all entries with the lamp flag; call it on profile load.
|
||||
/// </summary>
|
||||
public void InitializeLamps()
|
||||
{
|
||||
for (int address = 0; address < _map.Count; address++)
|
||||
{
|
||||
if (_map[address].HasLamp)
|
||||
_lamp.SetLamp(address, RioLampState.SolidDim);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handle a button/keypad press at the given RIO address.</summary>
|
||||
public void Press(int address)
|
||||
{
|
||||
RioMapEntry entry = _map[address];
|
||||
if (entry.IsUnmapped)
|
||||
return;
|
||||
|
||||
switch (entry.Kind)
|
||||
{
|
||||
case RioRouteKind.Keyboard:
|
||||
if (entry.Shift) _input.KeyDown(VirtualKey.Shift, extended: false);
|
||||
if (entry.Ctrl) _input.KeyDown(VirtualKey.Control, extended: false);
|
||||
if (entry.Alt) _input.KeyDown(VirtualKey.Menu, extended: false);
|
||||
_input.KeyDown(entry.Value, entry.Extended);
|
||||
break;
|
||||
|
||||
case RioRouteKind.RioCommand:
|
||||
_commands.Execute((RioCommandCode)entry.Value);
|
||||
return; // RIO commands are not outputs and carry no lamp feedback
|
||||
|
||||
case RioRouteKind.Joystick:
|
||||
_joystick.SetButton(entry.Value, pressed: true);
|
||||
break;
|
||||
|
||||
case RioRouteKind.Hat:
|
||||
_joystick.SetHat((RioHat)entry.Value);
|
||||
break;
|
||||
|
||||
case RioRouteKind.Mouse:
|
||||
MousePress((MouseAction)entry.Value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.HasLamp)
|
||||
_lamp.SetLamp(address, RioLampState.SolidBright);
|
||||
}
|
||||
|
||||
/// <summary>Handle a button/keypad release at the given RIO address.</summary>
|
||||
public void Release(int address)
|
||||
{
|
||||
RioMapEntry entry = _map[address];
|
||||
if (entry.IsUnmapped)
|
||||
return;
|
||||
|
||||
switch (entry.Kind)
|
||||
{
|
||||
case RioRouteKind.Keyboard:
|
||||
_input.KeyUp(entry.Value, entry.Extended);
|
||||
// Release modifiers in reverse order.
|
||||
if (entry.Alt) _input.KeyUp(VirtualKey.Menu, extended: false);
|
||||
if (entry.Ctrl) _input.KeyUp(VirtualKey.Control, extended: false);
|
||||
if (entry.Shift) _input.KeyUp(VirtualKey.Shift, extended: false);
|
||||
break;
|
||||
|
||||
case RioRouteKind.RioCommand:
|
||||
return; // nothing happens on release; no lamp
|
||||
|
||||
case RioRouteKind.Joystick:
|
||||
_joystick.SetButton(entry.Value, pressed: false);
|
||||
break;
|
||||
|
||||
case RioRouteKind.Hat:
|
||||
_joystick.SetHat(RioHat.Centered);
|
||||
break;
|
||||
|
||||
case RioRouteKind.Mouse:
|
||||
MouseRelease((MouseAction)entry.Value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.HasLamp)
|
||||
_lamp.SetLamp(address, RioLampState.SolidDim);
|
||||
}
|
||||
|
||||
// Mouse move deltas corrected vs. the legacy (which mixed dx/dy); the action
|
||||
// codes are the contract (docs/PROTOCOL.md §5 ⚠️). Moves act on press only.
|
||||
private void MousePress(MouseAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case MouseAction.MoveUp: _input.MouseMove(0, -_mouseStep); break;
|
||||
case MouseAction.MoveRight: _input.MouseMove(_mouseStep, 0); break;
|
||||
case MouseAction.MoveDown: _input.MouseMove(0, _mouseStep); break;
|
||||
case MouseAction.MoveLeft: _input.MouseMove(-_mouseStep, 0); break;
|
||||
case MouseAction.LeftClick: _input.MouseButton(MouseButton.Left, down: true); break;
|
||||
case MouseAction.RightClick: _input.MouseButton(MouseButton.Right, down: true); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void MouseRelease(MouseAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case MouseAction.LeftClick: _input.MouseButton(MouseButton.Left, down: false); break;
|
||||
case MouseAction.RightClick: _input.MouseButton(MouseButton.Right, down: false); break;
|
||||
// Move actions have no release behavior.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace RioJoy.Core.Mapping;
|
||||
|
||||
/// <summary>Mouse actions encoded in a mouse entry's low byte (docs/PROTOCOL.md §5).</summary>
|
||||
public enum MouseAction : byte
|
||||
{
|
||||
MoveUp = 0,
|
||||
MoveRight = 1,
|
||||
MoveDown = 2,
|
||||
MoveLeft = 3,
|
||||
LeftClick = 4,
|
||||
RightClick = 5,
|
||||
}
|
||||
|
||||
/// <summary>Which mouse button a click action targets.</summary>
|
||||
public enum MouseButton
|
||||
{
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// <summary>Discrete 4-way POV hat position. <see cref="Centered"/> = released.</summary>
|
||||
public enum RioHat
|
||||
{
|
||||
Centered = -1,
|
||||
Up = 0,
|
||||
Right = 1,
|
||||
Down = 2,
|
||||
Left = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal RIO commands (the low byte when an entry is a RIO command). Port of
|
||||
/// the <c>RIOcmd</c> switch (riovjoy2.cpp#L1852).
|
||||
/// </summary>
|
||||
public enum RioCommandCode : byte
|
||||
{
|
||||
ResetAllAxes = 0,
|
||||
ResetThrottle = 1,
|
||||
ResetLeftPedal = 2,
|
||||
ResetRightPedal = 3,
|
||||
ResetVerticalJoystick = 4,
|
||||
ResetHorizontalJoystick = 5,
|
||||
GeneralReset = 6,
|
||||
RequestVersionAndCheck = 7,
|
||||
ToggleRawAxesReadout = 8,
|
||||
TogglePollRateReadout = 9,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard + mouse output (the <c>SendInput</c> target). The router holds the
|
||||
/// ordering logic and emits virtual-key codes; a sink implementation maps VK to
|
||||
/// scancode and calls <c>SendInput</c>. Keys are addressed by Windows virtual-key
|
||||
/// code; modifiers are sent as ordinary keys (VK_SHIFT/CONTROL/MENU).
|
||||
/// </summary>
|
||||
public interface IInputSink
|
||||
{
|
||||
void KeyDown(byte virtualKey, bool extended);
|
||||
|
||||
void KeyUp(byte virtualKey, bool extended);
|
||||
|
||||
/// <summary>Relative mouse move by (<paramref name="dx"/>, <paramref name="dy"/>) pixels.</summary>
|
||||
void MouseMove(int dx, int dy);
|
||||
|
||||
void MouseButton(MouseButton button, bool down);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Virtual joystick output (the HID feeder → RioGamepad driver in Phase 1).
|
||||
/// Button numbers are 0-based into the 96-button report; the hat is the single POV.
|
||||
/// </summary>
|
||||
public interface IJoystickSink
|
||||
{
|
||||
void SetButton(int button, bool pressed);
|
||||
|
||||
void SetHat(RioHat position);
|
||||
}
|
||||
|
||||
/// <summary>Lamp (lighted-button) feedback, sent back to the RIO over serial.</summary>
|
||||
public interface ILampSink
|
||||
{
|
||||
void SetLamp(int address, byte lampState);
|
||||
}
|
||||
|
||||
/// <summary>Handles internal RIO commands routed from the input map.</summary>
|
||||
public interface IRioCommandSink
|
||||
{
|
||||
void Execute(RioCommandCode command);
|
||||
}
|
||||
|
||||
/// <summary>Well-known Windows virtual-key codes used by the router for modifiers.</summary>
|
||||
public static class VirtualKey
|
||||
{
|
||||
public const byte Shift = 0x10; // VK_SHIFT
|
||||
public const byte Control = 0x11; // VK_CONTROL
|
||||
public const byte Menu = 0x12; // VK_MENU (ALT)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace RioJoy.Core.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Translates raw RIO events into an <c>iRIO</c> table address. The 72 digital
|
||||
/// buttons map to <c>0x00–0x47</c> directly; keypad 0 is offset by <c>0x50</c>
|
||||
/// and keypad 1 by <c>0x60</c> (port of <c>KeypadEvent</c>, riovjoy2.cpp#L1185);
|
||||
/// see docs/PROTOCOL.md §5.
|
||||
/// </summary>
|
||||
public static class RioAddress
|
||||
{
|
||||
/// <summary>Number of digital button inputs (addresses 0x00–0x47).</summary>
|
||||
public const int ButtonCount = 72;
|
||||
|
||||
/// <summary>Address offset added to keypad-0 key indices.</summary>
|
||||
public const int Keypad0Base = 0x50;
|
||||
|
||||
/// <summary>Address offset added to keypad-1 key indices.</summary>
|
||||
public const int Keypad1Base = 0x60;
|
||||
|
||||
/// <summary>Highest valid address (keypad 1, index 0x0F).</summary>
|
||||
public const int MaxAddress = Keypad1Base + 0x0F; // 0x6F
|
||||
|
||||
/// <summary>Size of the <c>iRIO</c> table (addresses 0x00..0x6F inclusive).</summary>
|
||||
public const int TableSize = MaxAddress + 1; // 112
|
||||
|
||||
/// <summary>Address for a digital button event (<paramref name="index"/> 0x00–0x47).</summary>
|
||||
public static int FromButton(byte index)
|
||||
{
|
||||
if (index >= ButtonCount)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(index), $"Button index must be 0..{ButtonCount - 1}.");
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Address for a keypad key event: <paramref name="pad"/> 0 or 1,
|
||||
/// <paramref name="index"/> 0x00–0x0F. Mirrors the legacy offsets (+0x50 / +0x60).
|
||||
/// </summary>
|
||||
public static int FromKeypad(byte pad, byte index)
|
||||
{
|
||||
if (index > 0x0F)
|
||||
throw new ArgumentOutOfRangeException(nameof(index), "Keypad index must be 0..0x0F.");
|
||||
|
||||
return pad switch
|
||||
{
|
||||
0 => Keypad0Base + index,
|
||||
1 => Keypad1Base + index,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(pad), "Keypad must be 0 or 1."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace RioJoy.Core.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// The per-profile <c>iRIO</c> table: 112 entries indexed by RIO address
|
||||
/// (<c>0x00..0x6F</c>), each a <see cref="RioMapEntry"/>. Replaces the legacy
|
||||
/// hard-coded <c>iRIO[]</c> array; a profile supplies the values (the legacy
|
||||
/// defaults / <c>RIO.ini</c> become one importable profile in Phase 5/7).
|
||||
/// </summary>
|
||||
public sealed class RioInputMap
|
||||
{
|
||||
private readonly ushort[] _entries = new ushort[RioAddress.TableSize];
|
||||
|
||||
/// <summary>Create an empty map (all entries unmapped).</summary>
|
||||
public RioInputMap() { }
|
||||
|
||||
/// <summary>Create a map from raw 16-bit words indexed by address.</summary>
|
||||
public RioInputMap(IReadOnlyDictionary<int, ushort> entries)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entries);
|
||||
foreach ((int address, ushort raw) in entries)
|
||||
this[address] = new RioMapEntry(raw);
|
||||
}
|
||||
|
||||
/// <summary>Number of addressable slots (<see cref="RioAddress.TableSize"/>).</summary>
|
||||
public int Count => _entries.Length;
|
||||
|
||||
/// <summary>Get or set the entry at a RIO address (0x00..0x6F).</summary>
|
||||
public RioMapEntry this[int address]
|
||||
{
|
||||
get
|
||||
{
|
||||
ValidateAddress(address);
|
||||
return new RioMapEntry(_entries[address]);
|
||||
}
|
||||
set
|
||||
{
|
||||
ValidateAddress(address);
|
||||
_entries[address] = value.Raw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAddress(int address)
|
||||
{
|
||||
if (address < 0 || address >= RioAddress.TableSize)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(address), $"Address must be 0..0x{RioAddress.MaxAddress:X2}.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
namespace RioJoy.Core.Mapping;
|
||||
|
||||
/// <summary>How a decoded <see cref="RioMapEntry"/> should be routed.</summary>
|
||||
public enum RioRouteKind
|
||||
{
|
||||
/// <summary>Send a keyboard key (with optional SHIFT/CTRL/ALT modifiers).</summary>
|
||||
Keyboard,
|
||||
|
||||
/// <summary>Press a virtual joystick button.</summary>
|
||||
Joystick,
|
||||
|
||||
/// <summary>Set the virtual POV hat direction.</summary>
|
||||
Hat,
|
||||
|
||||
/// <summary>Perform a mouse action (move or click).</summary>
|
||||
Mouse,
|
||||
|
||||
/// <summary>
|
||||
/// An internal RIO command (axis reset, recalibrate, status, toggle) — not an
|
||||
/// output to the host. Encoded as joy+hat+mouse all set (<c>0x7000</c>).
|
||||
/// </summary>
|
||||
RioCommand,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One entry of the <c>iRIO</c> map: a 16-bit word whose high byte is routing
|
||||
/// flags and low byte is the payload (VK code / button# / hat direction / mouse
|
||||
/// action). Faithful decode of <c>Press_V2</c>/<c>Release_V2</c>
|
||||
/// (riovjoy2.cpp#L1944); see docs/PROTOCOL.md §5.
|
||||
/// </summary>
|
||||
public readonly struct RioMapEntry
|
||||
{
|
||||
// High-byte routing flags.
|
||||
private const ushort FlagHasLamp = 0x8000;
|
||||
private const ushort FlagMouse = 0x4000;
|
||||
private const ushort FlagHat = 0x2000;
|
||||
private const ushort FlagJoy = 0x1000;
|
||||
private const ushort FlagExtended = 0x0800;
|
||||
private const ushort FlagAlt = 0x0400;
|
||||
private const ushort FlagCtrl = 0x0200;
|
||||
private const ushort FlagShift = 0x0100;
|
||||
|
||||
private const ushort RioCommandMask = FlagJoy | FlagHat | FlagMouse; // 0x7000
|
||||
|
||||
/// <summary>The raw 16-bit map word.</summary>
|
||||
public ushort Raw { get; }
|
||||
|
||||
public RioMapEntry(ushort raw) => Raw = raw;
|
||||
|
||||
/// <summary>This input drives a lighted button (lamp feedback on press/release).</summary>
|
||||
public bool HasLamp => (Raw & FlagHasLamp) != 0;
|
||||
|
||||
/// <summary>Keyboard: apply <c>KEYEVENTF_EXTENDEDKEY</c> to the key.</summary>
|
||||
public bool Extended => (Raw & FlagExtended) != 0;
|
||||
|
||||
public bool Alt => (Raw & FlagAlt) != 0;
|
||||
public bool Ctrl => (Raw & FlagCtrl) != 0;
|
||||
public bool Shift => (Raw & FlagShift) != 0;
|
||||
|
||||
/// <summary>Low byte: VK code, joystick button, hat direction, or mouse action.</summary>
|
||||
public byte Value => (byte)(Raw & 0xFF);
|
||||
|
||||
private bool IsJoyFlag => (Raw & FlagJoy) != 0;
|
||||
private bool IsHatFlag => (Raw & FlagHat) != 0;
|
||||
private bool IsMouseFlag => (Raw & FlagMouse) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// The routing destination, applying the legacy precedence: all three of
|
||||
/// joy/hat/mouse set ⇒ RIO command; none set ⇒ keyboard; otherwise joy, then
|
||||
/// hat, then mouse.
|
||||
/// </summary>
|
||||
public RioRouteKind Kind
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((Raw & RioCommandMask) == RioCommandMask)
|
||||
return RioRouteKind.RioCommand;
|
||||
if (!IsJoyFlag && !IsHatFlag && !IsMouseFlag)
|
||||
return RioRouteKind.Keyboard;
|
||||
if (IsJoyFlag)
|
||||
return RioRouteKind.Joystick;
|
||||
if (IsHatFlag)
|
||||
return RioRouteKind.Hat;
|
||||
return RioRouteKind.Mouse;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>True if this entry has no mapping (a "do nothing" slot).</summary>
|
||||
public bool IsUnmapped => Raw == 0;
|
||||
|
||||
public override string ToString() => $"0x{Raw:X4} ({Kind} {Value})";
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user