New input pipeline: sources feed an InputRouter (VRio.Core.Input) that calls the same VRioDevice press/release/axis entry points the mouse canvas uses, so routed input is indistinguishable on the wire. - Bindings live in %APPDATA%\vRIO\bindings.txt (plain text, commented defaults written on first run; Reload/Edit buttons in the new Input group). Keys and pad buttons press any RIO address, momentary or toggle; axes bind in normalized units of each axis realistic travel window with deflect (spring-back), rate (position holds), deadzone, and invert options - RioAxisRange supplies the wire signs. - Router suppresses key auto-repeat, edge-detects pad buttons, and hold-counts per address so overlapping sources press once and release last; axes only write when the composed value changes, so mouse drags keep working while sources idle. - Xbox pad via a zero-dependency XInput P/Invoke poller (xinput1_4, fallback xinput9_1_0), throttled rescan while disconnected. - MainForm intercepts bound keys in ProcessCmdKey so arrows/space reach the panel instead of moving focus; keys release on focus loss; center-axes and host ResetRequest reset the rate integrators. - Canvas highlights router-held addresses like clicks. - Defaults: arrows=stick, W/S=throttle, Q/E=pedals, numpad=internal keypad, IJKL/Space=hat+main; pad left stick/triggers/right stick = stick/pedals/throttle-rate, ABXY/dpad/shoulders = named buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
121 lines
3.6 KiB
C#
121 lines
3.6 KiB
C#
using System.Runtime.InteropServices;
|
||
using VRio.Core.Input;
|
||
|
||
namespace VRio.App;
|
||
|
||
/// <summary>
|
||
/// Polls the first connected XInput (Xbox) controller and converts its state
|
||
/// to the Core's normalized <see cref="PadState"/>. Uses the in-box
|
||
/// xinput1_4.dll (Windows 8+), falling back to xinput9_1_0.dll, so no
|
||
/// redistributable is needed. Polling a disconnected user index is expensive,
|
||
/// so while no pad is present the 0–3 scan only runs every ~1 s.
|
||
/// </summary>
|
||
internal sealed class XInputGamepad
|
||
{
|
||
private const int MaxUsers = 4;
|
||
private const uint ErrorSuccess = 0;
|
||
private const int ScanEveryNPolls = 60; // ≈1 s at the 16 ms input tick
|
||
|
||
private static bool _tryXInput14 = true;
|
||
|
||
private int _user = -1;
|
||
private int _scanCountdown;
|
||
|
||
/// <summary>True while a controller is connected and being polled.</summary>
|
||
public bool Connected => _user >= 0;
|
||
|
||
/// <summary>XInput user index of the connected controller (0-based).</summary>
|
||
public int UserIndex => _user;
|
||
|
||
/// <summary>
|
||
/// Poll the pad. False (with a rest-state snapshot) while disconnected.
|
||
/// </summary>
|
||
public bool TryRead(out PadState state)
|
||
{
|
||
if (_user >= 0)
|
||
{
|
||
if (GetState(_user, out XINPUT_STATE s) == ErrorSuccess)
|
||
{
|
||
state = Convert(s.Gamepad);
|
||
return true;
|
||
}
|
||
_user = -1; // unplugged; fall through to (throttled) rescan
|
||
_scanCountdown = 0;
|
||
}
|
||
|
||
if (--_scanCountdown <= 0)
|
||
{
|
||
_scanCountdown = ScanEveryNPolls;
|
||
for (int i = 0; i < MaxUsers; i++)
|
||
{
|
||
if (GetState(i, out XINPUT_STATE s) == ErrorSuccess)
|
||
{
|
||
_user = i;
|
||
state = Convert(s.Gamepad);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
state = default;
|
||
return false;
|
||
}
|
||
|
||
private static PadState Convert(in XINPUT_GAMEPAD g) => new(
|
||
(PadButtons)g.wButtons,
|
||
Thumb(g.sThumbLX), Thumb(g.sThumbLY),
|
||
Thumb(g.sThumbRX), Thumb(g.sThumbRY),
|
||
g.bLeftTrigger / 255f, g.bRightTrigger / 255f);
|
||
|
||
private static float Thumb(short v) => Math.Max(-1f, v / 32767f);
|
||
|
||
private static uint GetState(int user, out XINPUT_STATE state)
|
||
{
|
||
if (_tryXInput14)
|
||
{
|
||
try
|
||
{
|
||
return XInputGetState14(user, out state);
|
||
}
|
||
catch (DllNotFoundException)
|
||
{
|
||
_tryXInput14 = false;
|
||
}
|
||
}
|
||
try
|
||
{
|
||
return XInputGetState910(user, out state);
|
||
}
|
||
catch (DllNotFoundException)
|
||
{
|
||
state = default;
|
||
return uint.MaxValue; // no XInput on this system — behaves as "no pad"
|
||
}
|
||
}
|
||
|
||
[DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")]
|
||
private static extern uint XInputGetState14(int dwUserIndex, out XINPUT_STATE state);
|
||
|
||
[DllImport("xinput9_1_0.dll", EntryPoint = "XInputGetState")]
|
||
private static extern uint XInputGetState910(int dwUserIndex, out XINPUT_STATE state);
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct XINPUT_STATE
|
||
{
|
||
public uint dwPacketNumber;
|
||
public XINPUT_GAMEPAD Gamepad;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct XINPUT_GAMEPAD
|
||
{
|
||
public ushort wButtons;
|
||
public byte bLeftTrigger;
|
||
public byte bRightTrigger;
|
||
public short sThumbLX;
|
||
public short sThumbLY;
|
||
public short sThumbRX;
|
||
public short sThumbRY;
|
||
}
|
||
}
|