diff --git a/src/VRio.App/GamepadDeviceLocator.cs b/src/VRio.App/GamepadDeviceLocator.cs
new file mode 100644
index 0000000..38b0570
--- /dev/null
+++ b/src/VRio.App/GamepadDeviceLocator.cs
@@ -0,0 +1,173 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace VRio.App;
+
+///
+/// Finds the PnP device instance IDs behind every attached gamepad/joystick,
+/// for handing to . Enumeration rides on Raw
+/// Input's HID list (usage page 1, usages 4/5/8 — joystick, gamepad,
+/// multi-axis), the same census games take.
+///
+/// For XInput-class pads the HID collection is only the DirectInput
+/// face; XInput itself reads the parent XUSB/GIP devnode. Those pads
+/// are recognizable by the IG_ marker Microsoft puts in their HID
+/// hardware IDs, and both the HID instance and its parent are returned —
+/// cloaking just one of them leaves the other visible to half the world.
+/// Plain HID pads (DualShock and friends) contribute only their HID instance;
+/// their parent is often a composite device with audio and such that must not
+/// be hidden.
+///
+internal static class GamepadDeviceLocator
+{
+ // --- Win32 interop -------------------------------------------------------
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RAWINPUTDEVICELIST
+ {
+ public IntPtr hDevice;
+ public uint dwType;
+ }
+
+ // The RID_DEVICE_INFO union flattened to its HID arm; the keyboard arm is
+ // the largest (24 bytes), so the tail pad keeps cbSize honest at 32.
+ [StructLayout(LayoutKind.Explicit)]
+ private struct RID_DEVICE_INFO
+ {
+ [FieldOffset(0)] public uint cbSize;
+ [FieldOffset(4)] public uint dwType;
+ [FieldOffset(8)] public uint hidVendorId;
+ [FieldOffset(12)] public uint hidProductId;
+ [FieldOffset(16)] public uint hidVersionNumber;
+ [FieldOffset(20)] public ushort hidUsagePage;
+ [FieldOffset(22)] public ushort hidUsage;
+ [FieldOffset(28)] public uint KeyboardArmTail; // pads the union to its true 24-byte size
+ }
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern uint GetRawInputDeviceList(
+ [In, Out] RAWINPUTDEVICELIST[]? pRawInputDeviceList, ref uint puiNumDevices, uint cbSize);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern uint GetRawInputDeviceInfoW(
+ IntPtr hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern uint GetRawInputDeviceInfoW(
+ IntPtr hDevice, uint uiCommand, ref RID_DEVICE_INFO pData, ref uint pcbSize);
+
+ [DllImport("cfgmgr32.dll", CharSet = CharSet.Unicode)]
+ private static extern int CM_Locate_DevNodeW(out uint pdnDevInst, string pDeviceID, uint ulFlags);
+
+ [DllImport("cfgmgr32.dll")]
+ private static extern int CM_Get_Parent(out uint pdnDevInst, uint dnDevInst, uint ulFlags);
+
+ [DllImport("cfgmgr32.dll", CharSet = CharSet.Unicode)]
+ private static extern int CM_Get_Device_IDW(uint dnDevInst, StringBuilder buffer, int bufferLen, uint ulFlags);
+
+ private const uint Err = 0xFFFFFFFF;
+ private const uint RIM_TYPEHID = 2;
+ private const uint RIDI_DEVICENAME = 0x20000007;
+ private const uint RIDI_DEVICEINFO = 0x2000000B;
+ private const ushort UsagePageGeneric = 0x01;
+ private const ushort UsageJoystick = 0x04;
+ private const ushort UsageGamepad = 0x05;
+ private const ushort UsageMultiAxis = 0x08;
+ private const int CrSuccess = 0;
+ private const int MaxDeviceIdLen = 200;
+
+ // --- Enumeration ---------------------------------------------------------
+
+ ///
+ /// Device instance IDs of every attached gamepad/joystick: the HID
+ /// instances, plus the parent devnode of each IG_ (XInput) pad.
+ /// Empty when no pad is attached.
+ ///
+ public static List FindGamepadInstances()
+ {
+ var result = new List();
+ uint listSize = (uint)Marshal.SizeOf();
+ uint count = 0;
+ if (GetRawInputDeviceList(null, ref count, listSize) == Err || count == 0)
+ return result;
+
+ var devices = new RAWINPUTDEVICELIST[count];
+ uint got = GetRawInputDeviceList(devices, ref count, listSize);
+ if (got == Err)
+ return result;
+
+ for (int i = 0; i < got; i++)
+ {
+ if (devices[i].dwType != RIM_TYPEHID || !IsGamepad(devices[i].hDevice))
+ continue;
+
+ string? path = InterfacePath(devices[i].hDevice);
+ string? instance = path is null ? null : InstanceIdFromInterfacePath(path);
+ if (instance is null || result.Contains(instance, StringComparer.OrdinalIgnoreCase))
+ continue;
+
+ result.Add(instance);
+ if (instance.IndexOf("IG_", StringComparison.OrdinalIgnoreCase) >= 0
+ && ParentInstanceId(instance) is { } parent
+ && !result.Contains(parent, StringComparer.OrdinalIgnoreCase))
+ {
+ result.Add(parent);
+ }
+ }
+ return result;
+ }
+
+ private static bool IsGamepad(IntPtr hDevice)
+ {
+ var info = new RID_DEVICE_INFO { cbSize = (uint)Marshal.SizeOf() };
+ uint size = info.cbSize;
+ if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICEINFO, ref info, ref size) == Err)
+ return false;
+ return info.dwType == RIM_TYPEHID
+ && info.hidUsagePage == UsagePageGeneric
+ && info.hidUsage is UsageJoystick or UsageGamepad or UsageMultiAxis;
+ }
+
+ private static string? InterfacePath(IntPtr hDevice)
+ {
+ uint chars = 0;
+ if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref chars) == Err || chars == 0)
+ return null;
+
+ IntPtr buffer = Marshal.AllocHGlobal((int)chars * sizeof(char));
+ try
+ {
+ if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, buffer, ref chars) == Err)
+ return null;
+ return Marshal.PtrToStringUni(buffer);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ ///
+ /// \\?\HID#VID_x&PID_y#instance#{guid} → HID\VID_x&PID_y\instance.
+ ///
+ private static string InstanceIdFromInterfacePath(string path)
+ {
+ string s = path;
+ if (s.StartsWith(@"\\?\", StringComparison.Ordinal) || s.StartsWith(@"\\.\", StringComparison.Ordinal))
+ s = s.Substring(4);
+ int guid = s.LastIndexOf("#{", StringComparison.Ordinal);
+ if (guid >= 0)
+ s = s.Substring(0, guid);
+ return s.Replace('#', '\\');
+ }
+
+ private static string? ParentInstanceId(string instanceId)
+ {
+ if (CM_Locate_DevNodeW(out uint node, instanceId, 0) != CrSuccess)
+ return null;
+ if (CM_Get_Parent(out uint parent, node, 0) != CrSuccess)
+ return null;
+ var buffer = new StringBuilder(MaxDeviceIdLen);
+ return CM_Get_Device_IDW(parent, buffer, buffer.Capacity, 0) == CrSuccess ? buffer.ToString() : null;
+ }
+}
diff --git a/src/VRio.App/HidHideControl.cs b/src/VRio.App/HidHideControl.cs
new file mode 100644
index 0000000..676edc1
--- /dev/null
+++ b/src/VRio.App/HidHideControl.cs
@@ -0,0 +1,186 @@
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using System.Text;
+using Microsoft.Win32.SafeHandles;
+
+namespace VRio.App;
+
+///
+/// Minimal client for the HidHide filter driver's control device
+/// (\\.\HidHide) — the same wire protocol Nefarius's
+/// HidHideControlService speaks, reduced to what the exclusive-gamepad
+/// toggle needs. HidHide cloaks blocked device instances from every process
+/// except the allowlisted applications, which is the only robust way to keep
+/// a second app (DOSBox-X, Steam, the sim itself) from also reacting to the
+/// cockpit gamepad: XInput has no exclusive mode of its own.
+///
+/// Every operation opens the control device fresh, mirroring the
+/// reference implementation; the driver treats each set as a full list
+/// replace, so add/remove is read-modify-write here. All methods throw
+/// (driver refused / not reachable) or
+/// (unconvertible path) — callers
+/// surface the message and carry on.
+///
+internal static class HidHideControl
+{
+ private const string ControlDevice = @"\\.\HidHide";
+
+ // CTL_CODE(0x8001, 2048+n, METHOD_BUFFERED, FILE_READ_DATA) — the
+ // device type / function numbers HidHide has used since v1.0.
+ private const uint IoctlGetWhitelist = 0x80016000;
+ private const uint IoctlSetWhitelist = 0x80016004;
+ private const uint IoctlGetBlacklist = 0x80016008;
+ private const uint IoctlSetBlacklist = 0x8001600C;
+ private const uint IoctlGetActive = 0x80016010;
+ private const uint IoctlSetActive = 0x80016014;
+
+ private const uint GenericRead = 0x80000000;
+ private const uint GenericWrite = 0x40000000;
+ private const uint ShareReadWrite = 0x00000003;
+ private const uint OpenExisting = 3;
+ private const int ErrorFileNotFound = 2;
+ private const int ErrorPathNotFound = 3;
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern SafeFileHandle CreateFileW(
+ string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes,
+ uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool DeviceIoControl(
+ SafeFileHandle hDevice, uint dwIoControlCode,
+ byte[]? lpInBuffer, uint nInBufferSize,
+ byte[]? lpOutBuffer, uint nOutBufferSize,
+ out uint lpBytesReturned, IntPtr lpOverlapped);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern uint QueryDosDeviceW(string lpDeviceName, StringBuilder lpTargetPath, int ucchMax);
+
+ ///
+ /// True when the HidHide driver is present (its control device exists),
+ /// even if a later operation might still be refused by its ACL.
+ ///
+ public static bool IsInstalled()
+ {
+ using SafeFileHandle handle = CreateFileW(ControlDevice, GenericRead | GenericWrite,
+ ShareReadWrite, IntPtr.Zero, OpenExisting, 0, IntPtr.Zero);
+ if (!handle.IsInvalid)
+ return true;
+ return Marshal.GetLastWin32Error() is not (ErrorFileNotFound or ErrorPathNotFound);
+ }
+
+ /// Whether the cloak is currently up (blocked devices hidden).
+ public static bool GetActive()
+ {
+ using SafeFileHandle handle = Open();
+ var buffer = new byte[1];
+ Ioctl(handle, IoctlGetActive, null, buffer);
+ return buffer[0] != 0;
+ }
+
+ public static void SetActive(bool active)
+ {
+ using SafeFileHandle handle = Open();
+ Ioctl(handle, IoctlSetActive, new[] { active ? (byte)1 : (byte)0 }, null);
+ }
+
+ /// Device instance IDs currently cloaked (e.g. HID\VID_045E…\…).
+ public static List GetBlockedInstances() => GetList(IoctlGetBlacklist);
+
+ /// Replace the cloaked-instance list wholesale.
+ public static void SetBlockedInstances(IReadOnlyCollection instanceIds) =>
+ SetList(IoctlSetBlacklist, instanceIds);
+
+ /// Allowlisted applications, in dos-device form (\Device\HarddiskVolumeN\…).
+ public static List GetAllowedApps() => GetList(IoctlGetWhitelist);
+
+ /// Replace the application allowlist wholesale.
+ public static void SetAllowedApps(IReadOnlyCollection dosDevicePaths) =>
+ SetList(IoctlSetWhitelist, dosDevicePaths);
+
+ ///
+ /// Convert a normal path (C:\dir\app.exe) to the dos-device form
+ /// the driver stores allowlist entries in
+ /// (\Device\HarddiskVolume3\dir\app.exe). Drive-letter paths only —
+ /// the driver compares against the kernel's image path, which never has a
+ /// letter.
+ ///
+ public static string ToDosDevicePath(string path)
+ {
+ string full = Path.GetFullPath(path);
+ string? root = Path.GetPathRoot(full);
+ if (root is null || root.Length < 2 || root[1] != ':')
+ throw new InvalidOperationException($"cannot allowlist \"{full}\" — only local drive paths are supported.");
+
+ string drive = root.Substring(0, 2); // "C:"
+ var target = new StringBuilder(512);
+ if (QueryDosDeviceW(drive, target, target.Capacity) == 0)
+ throw new Win32Exception();
+ return target.ToString() + full.Substring(drive.Length);
+ }
+
+ // --- Wire helpers --------------------------------------------------------
+
+ private static SafeFileHandle Open()
+ {
+ SafeFileHandle handle = CreateFileW(ControlDevice, GenericRead | GenericWrite,
+ ShareReadWrite, IntPtr.Zero, OpenExisting, 0, IntPtr.Zero);
+ if (handle.IsInvalid)
+ throw new Win32Exception();
+ return handle;
+ }
+
+ private static uint Ioctl(SafeFileHandle handle, uint code, byte[]? input, byte[]? output)
+ {
+ if (!DeviceIoControl(handle, code, input, (uint)(input?.Length ?? 0),
+ output, (uint)(output?.Length ?? 0), out uint returned, IntPtr.Zero))
+ throw new Win32Exception();
+ return returned;
+ }
+
+ private static List GetList(uint code)
+ {
+ using SafeFileHandle handle = Open();
+ // Called with no output buffer the driver reports the required size
+ // (in bytes) instead of failing; a second call fetches the data.
+ uint needed = Ioctl(handle, code, null, null);
+ if (needed == 0)
+ return new List();
+ var buffer = new byte[needed];
+ uint got = Ioctl(handle, code, null, buffer);
+ return DecodeMultiSz(buffer, (int)Math.Min(got, (uint)buffer.Length));
+ }
+
+ private static void SetList(uint code, IEnumerable items)
+ {
+ using SafeFileHandle handle = Open();
+ Ioctl(handle, code, EncodeMultiSz(items), null);
+ }
+
+ /// UTF-16 multi-sz: each string null-terminated, double null at the end.
+ private static byte[] EncodeMultiSz(IEnumerable items)
+ {
+ var sb = new StringBuilder();
+ foreach (string item in items)
+ {
+ sb.Append(item);
+ sb.Append('\0');
+ }
+ sb.Append('\0');
+ if (sb.Length == 1)
+ sb.Append('\0'); // an empty multi-sz is two terminators
+ return Encoding.Unicode.GetBytes(sb.ToString());
+ }
+
+ private static List DecodeMultiSz(byte[] buffer, int length)
+ {
+ string text = Encoding.Unicode.GetString(buffer, 0, length);
+ var result = new List();
+ foreach (string s in text.Split('\0'))
+ {
+ if (s.Length > 0)
+ result.Add(s);
+ }
+ return result;
+ }
+}
diff --git a/src/VRio.App/MainForm.cs b/src/VRio.App/MainForm.cs
index 9aee109..fd2f739 100644
--- a/src/VRio.App/MainForm.cs
+++ b/src/VRio.App/MainForm.cs
@@ -129,6 +129,23 @@ internal sealed class MainForm : Form
Width = 286,
DropDownStyle = ComboBoxStyle.DropDownList,
};
+ private readonly CheckBox _padExclusive = new()
+ {
+ Text = "Exclusive gamepad (needs HidHide)",
+ Location = new Point(10, 190),
+ AutoSize = true,
+ };
+
+ // Exclusive-gamepad state: the instance IDs we cloaked (null = off), the
+ // driver's active flag before we touched it, a re-entrancy guard for
+ // programmatic checkbox updates, a post-enable countdown proving vRIO can
+ // still see the pad through the cloak, and last tick's connect state for
+ // hot-plug detection.
+ private List? _hiddenPadInstances;
+ private bool _hidHideWasActive;
+ private bool _padExclusiveUpdating;
+ private int _exclusiveGraceTicks;
+ private bool _padWasConnected;
/// A keyboard choice in the lamp-mirror picker (null id = all).
private sealed record KbChoice(string? Id, string Name)
@@ -138,14 +155,14 @@ internal sealed class MainForm : Form
private readonly Label _counters = new()
{
- Location = new Point(12, 372),
+ Location = new Point(12, 386),
AutoSize = true,
Font = new Font("Consolas", 8f),
};
private readonly Label _help = new()
{
- Location = new Point(12, 428),
+ Location = new Point(12, 442),
MaximumSize = new Size(306, 0),
AutoSize = true,
ForeColor = Color.Gray,
@@ -156,7 +173,7 @@ internal sealed class MainForm : Form
private readonly TextBox _logBox = new()
{
- Location = new Point(12, 498),
+ Location = new Point(12, 512),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both, // long wire lines don't wrap — scroll to read
@@ -186,8 +203,8 @@ internal sealed class MainForm : Form
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
// Fit the window to its content: the cockpit canvas plus the 330px
// control strip, with just enough height for the strip's log area.
- ClientSize = new Size(_canvas.Width + 332, Math.Max(_canvas.Height, 640));
- MinimumSize = new Size(1000, 620);
+ ClientSize = new Size(_canvas.Width + 332, Math.Max(_canvas.Height, 654));
+ MinimumSize = new Size(1000, 634);
StartPosition = FormStartPosition.CenterScreen;
KeyPreview = true; // form-level key routing for the input bindings
@@ -295,6 +312,11 @@ internal sealed class MainForm : Form
_rawKeyboard.KeyUp += name => RunOnUi(() => _router.KeyUp(name));
_rawKeyboard.Logged += line => RunOnUi(() => PrependLog(line));
_rawKeyboard.KeyboardsChanged += list => RunOnUi(() => RebuildInputKeyboardPicker(list));
+ _padExclusive.CheckedChanged += (_, _) =>
+ {
+ if (!_padExclusiveUpdating)
+ SetPadExclusive(_padExclusive.Checked);
+ };
_kbInputTarget.SelectedIndexChanged += (_, _) => ApplyInputSource();
_kbInputTarget.DropDown += (_, _) => _rawKeyboard.RefreshKeyboards();
_kbInputTarget.Items.Add(new KbChoice(null, "All keyboards (focus only)"));
@@ -310,6 +332,11 @@ internal sealed class MainForm : Form
FormClosed += (_, _) =>
{
+ // Drop the cloak on the way out — a hidden controller must not
+ // outlive the app that could still see it. Best effort: if the
+ // driver refuses, HidHide's own client can clean up.
+ try { DisablePadExclusive(); }
+ catch { }
_uiTimer.Dispose();
_inputTimer.Dispose();
_lampMirror.Dispose();
@@ -355,13 +382,13 @@ internal sealed class MainForm : Form
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff });
panel.Controls.Add(device);
- var input = new GroupBox { Text = "Input", Location = new Point(12, 162), Size = new Size(306, 202) };
- input.Controls.AddRange(new Control[] { _kbInput, _padInput, _invertY, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget, _kbInputLabel, _kbInputTarget });
+ var input = new GroupBox { Text = "Input", Location = new Point(12, 162), Size = new Size(306, 216) };
+ input.Controls.AddRange(new Control[] { _kbInput, _padInput, _invertY, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget, _kbInputLabel, _kbInputTarget, _padExclusive });
panel.Controls.Add(input);
panel.Controls.Add(_counters);
panel.Controls.Add(_help);
- panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 480), AutoSize = true });
+ panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 494), AutoSize = true });
_logBox.Size = new Size(306, ClientSize.Height - _logBox.Top - 44);
panel.Controls.Add(_logBox);
@@ -620,8 +647,14 @@ internal sealed class MainForm : Form
_router.Tick(dt);
+ if (_hiddenPadInstances is not null && _padInput.Checked)
+ MaintainPadExclusive();
+ _padWasConnected = _gamepad.Connected;
+
string status = _padInput.Checked
- ? _gamepad.Connected ? $"Controller #{_gamepad.UserIndex + 1} connected." : "No controller detected."
+ ? _gamepad.Connected
+ ? $"Controller #{_gamepad.UserIndex + 1} connected." + (_hiddenPadInstances is not null ? " Exclusive." : "")
+ : "No controller detected."
: "Gamepad input off.";
if (_padStatus.Text != status)
{
@@ -630,6 +663,149 @@ internal sealed class MainForm : Form
}
}
+ // ---- Exclusive gamepad (HidHide) ----------------------------------------
+ // XInput has no exclusive acquisition, so "only vRIO gets the pad" is done
+ // one level down: the HidHide filter driver cloaks the pad's device
+ // instances from every process except allowlisted ones. vRIO allowlists
+ // itself, cloaks the pad (both the HID face and, for IG_/XInput pads, the
+ // parent devnode XInput actually reads), and reverts on toggle-off or
+ // exit. Not persisted across runs on purpose — an invisible controller
+ // with nobody holding the reveal switch is a support call.
+
+ private void SetPadExclusive(bool on)
+ {
+ try
+ {
+ if (on)
+ EnablePadExclusive();
+ else
+ DisablePadExclusive();
+ }
+ catch (Exception ex)
+ {
+ PrependLog($"HidHide: {ex.Message}");
+ SetPadExclusiveChecked(_hiddenPadInstances is not null); // reflect what actually happened
+ MessageBox.Show(this, $"Exclusive gamepad mode failed:\n{ex.Message}", "vRIO",
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ }
+ }
+
+ private void EnablePadExclusive()
+ {
+ if (_hiddenPadInstances is not null)
+ return;
+
+ if (!HidHideControl.IsInstalled())
+ throw new InvalidOperationException(
+ "the HidHide filter driver is not installed. Install it from " +
+ "github.com/nefarius/HidHide/releases and reconnect the pad, then try again.");
+
+ List pads = GamepadDeviceLocator.FindGamepadInstances();
+ if (pads.Count == 0)
+ throw new InvalidOperationException(
+ "no gamepad or joystick device found to cloak — connect the controller first.");
+
+ // vRIO must be able to see through the cloak before it goes up.
+ string self = HidHideControl.ToDosDevicePath(Application.ExecutablePath);
+ List apps = HidHideControl.GetAllowedApps();
+ if (!apps.Contains(self, StringComparer.OrdinalIgnoreCase))
+ {
+ apps.Add(self);
+ HidHideControl.SetAllowedApps(apps);
+ }
+
+ // Track only the instances *we* add: toggle-off must not strip
+ // entries some other tool (DS4Windows, say) is relying on.
+ bool wasActive = HidHideControl.GetActive();
+ List blocked = HidHideControl.GetBlockedInstances();
+ var added = pads.Where(p => !blocked.Contains(p, StringComparer.OrdinalIgnoreCase)).ToList();
+ if (added.Count > 0)
+ HidHideControl.SetBlockedInstances(blocked.Concat(added).ToList());
+ if (!wasActive)
+ HidHideControl.SetActive(true);
+
+ _hidHideWasActive = wasActive;
+ _hiddenPadInstances = added;
+ // The cloak filters device *opens*, so vRIO's already-open XInput
+ // handle rides through — but so does everyone else's until the pad is
+ // replugged. Watch our own visibility for a few seconds regardless:
+ // if the pad drops out, the allowlist didn't take.
+ _exclusiveGraceTicks = _gamepad.Connected ? 180 : 0;
+
+ PrependLog($"HidHide: cloaked {pads.Count} gamepad device instance(s); vRIO is allowlisted.");
+ foreach (string id in pads)
+ PrependLog($"HidHide: {id}");
+ PrependLog("HidHide: apps already holding the pad keep it until it's replugged.");
+ }
+
+ private void DisablePadExclusive()
+ {
+ if (_hiddenPadInstances is null)
+ return;
+
+ List blocked = HidHideControl.GetBlockedInstances();
+ List keep = blocked
+ .Where(b => !_hiddenPadInstances.Contains(b, StringComparer.OrdinalIgnoreCase)).ToList();
+ if (keep.Count != blocked.Count)
+ HidHideControl.SetBlockedInstances(keep);
+ if (!_hidHideWasActive)
+ HidHideControl.SetActive(false);
+
+ _hiddenPadInstances = null;
+ _exclusiveGraceTicks = 0;
+ PrependLog("HidHide: gamepad visible to all applications again.");
+ }
+
+ ///
+ /// Per-tick upkeep while exclusive mode is on: the post-enable visibility
+ /// watch, then cloaking any pad that hot-plugs in later (a fresh pad is a
+ /// fresh instance ID — without this it would bypass the cloak entirely).
+ ///
+ private void MaintainPadExclusive()
+ {
+ if (_exclusiveGraceTicks > 0)
+ {
+ if (_gamepad.Connected)
+ {
+ _exclusiveGraceTicks--;
+ return;
+ }
+ PrependLog("HidHide: vRIO lost sight of the pad right after cloaking — " +
+ "the allowlist didn't take. Reverting.");
+ _exclusiveGraceTicks = 0;
+ SetPadExclusiveChecked(false);
+ SetPadExclusive(false);
+ return;
+ }
+
+ if (_gamepad.Connected && !_padWasConnected)
+ {
+ try
+ {
+ List pads = GamepadDeviceLocator.FindGamepadInstances();
+ List blocked = HidHideControl.GetBlockedInstances();
+ var fresh = pads.Where(p => !blocked.Contains(p, StringComparer.OrdinalIgnoreCase)).ToList();
+ if (fresh.Count == 0)
+ return;
+ HidHideControl.SetBlockedInstances(blocked.Concat(fresh).ToList());
+ _hiddenPadInstances!.AddRange(fresh);
+ PrependLog($"HidHide: cloaked {fresh.Count} newly connected gamepad device instance(s).");
+ }
+ catch (Exception ex)
+ {
+ PrependLog($"HidHide: could not cloak the new pad — {ex.Message}");
+ }
+ }
+ }
+
+ /// Move the checkbox without tripping its change handler.
+ private void SetPadExclusiveChecked(bool value)
+ {
+ _padExclusiveUpdating = true;
+ _padExclusive.Checked = value;
+ _padExclusiveUpdating = false;
+ }
+
private void LoadBindings()
{
try