Add signed ViGEmBus virtual-joystick support (Xbox 360)
Replace the test-signed RioGamepad driver as the default joystick output with a ViGEmBus virtual Xbox 360 controller (Nefarius.ViGEm.Client) — properly signed, so it installs with no test-signing / Secure Boot change / reboot. The coordinator now selects ViGEm first, then the RioGamepad HID feeder, then a no-op. The XInput layout caps joystick buttons at 11, so the editor's joystick picker lists the named Xbox buttons (A, B, X, Y, LB, RB, Back, Start, L3, R3, Guide); the hat maps to the D-pad and the six axes to the sticks + triggers. Bind anything beyond 11 to the keyboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Runtime.Versioning;
|
||||||
|
using Nefarius.ViGEm.Client;
|
||||||
|
using Nefarius.ViGEm.Client.Targets;
|
||||||
|
using Nefarius.ViGEm.Client.Targets.Xbox360;
|
||||||
|
using RioJoy.Core.Calibration;
|
||||||
|
using RioJoy.Core.Mapping;
|
||||||
|
|
||||||
|
namespace RioJoy.Core.Output;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="IJoystickSink"/> backed by a <b>signed ViGEmBus</b> virtual Xbox 360
|
||||||
|
/// controller. Properly signed — installs with no test-signing / Secure-Boot
|
||||||
|
/// changes — at the cost of the XInput layout: 11 buttons, two sticks + two
|
||||||
|
/// triggers, and the D-pad for the hat. RIO joystick buttons beyond
|
||||||
|
/// <see cref="MappableButtonCount"/> are dropped (map those to the keyboard).
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
|
||||||
|
{
|
||||||
|
// RIO joystick button (1-based) -> Xbox 360 button, in a natural order.
|
||||||
|
private static readonly Xbox360Button[] ButtonMap =
|
||||||
|
{
|
||||||
|
Xbox360Button.A, Xbox360Button.B, Xbox360Button.X, Xbox360Button.Y,
|
||||||
|
Xbox360Button.LeftShoulder, Xbox360Button.RightShoulder,
|
||||||
|
Xbox360Button.Back, Xbox360Button.Start,
|
||||||
|
Xbox360Button.LeftThumb, Xbox360Button.RightThumb,
|
||||||
|
Xbox360Button.Guide,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Display names for the mappable buttons, aligned with <see cref="ButtonMap"/>.</summary>
|
||||||
|
public static IReadOnlyList<string> ButtonNames { get; } = new[]
|
||||||
|
{
|
||||||
|
"A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>How many RIO joystick buttons map onto the Xbox 360 pad (11).</summary>
|
||||||
|
public static int MappableButtonCount => ButtonMap.Length;
|
||||||
|
|
||||||
|
private readonly ViGEmClient _client;
|
||||||
|
private readonly IXbox360Controller _pad;
|
||||||
|
private readonly object _gate = new();
|
||||||
|
|
||||||
|
private ViGEmJoystickSink(ViGEmClient client, IXbox360Controller pad)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_pad = pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to connect to ViGEmBus and create a virtual Xbox 360 controller. Returns
|
||||||
|
/// <see langword="false"/> if ViGEmBus is not installed, so callers can fall back
|
||||||
|
/// to another joystick sink.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryCreate(out ViGEmJoystickSink? sink)
|
||||||
|
{
|
||||||
|
sink = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = new ViGEmClient();
|
||||||
|
IXbox360Controller pad = client.CreateXbox360Controller();
|
||||||
|
pad.AutoSubmitReport = false; // submit once per logical update
|
||||||
|
pad.Connect();
|
||||||
|
sink = new ViGEmJoystickSink(client, pad);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// VigemBusNotFoundException (driver absent) or any connect failure.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetButton(int button, bool pressed)
|
||||||
|
{
|
||||||
|
int index = button - 1; // RIO joystick buttons are 1-based
|
||||||
|
if (index < 0 || index >= ButtonMap.Length)
|
||||||
|
return; // beyond the Xbox 360 layout — bind these to the keyboard instead
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_pad.SetButtonState(ButtonMap[index], pressed);
|
||||||
|
_pad.SubmitReport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHat(RioHat position)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_pad.SetButtonState(Xbox360Button.Up, position == RioHat.Up);
|
||||||
|
_pad.SetButtonState(Xbox360Button.Right, position == RioHat.Right);
|
||||||
|
_pad.SetButtonState(Xbox360Button.Down, position == RioHat.Down);
|
||||||
|
_pad.SetButtonState(Xbox360Button.Left, position == RioHat.Left);
|
||||||
|
_pad.SubmitReport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetAxis(JoyAxis axis, int value)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
switch (axis)
|
||||||
|
{
|
||||||
|
case JoyAxis.X: _pad.SetAxisValue(Xbox360Axis.LeftThumbX, ToThumb(value)); break;
|
||||||
|
case JoyAxis.Y: _pad.SetAxisValue(Xbox360Axis.LeftThumbY, ToThumb(value)); break;
|
||||||
|
case JoyAxis.Rx: _pad.SetAxisValue(Xbox360Axis.RightThumbX, ToThumb(value)); break;
|
||||||
|
case JoyAxis.Ry: _pad.SetAxisValue(Xbox360Axis.RightThumbY, ToThumb(value)); break;
|
||||||
|
case JoyAxis.Z: _pad.SetSliderValue(Xbox360Slider.LeftTrigger, ToTrigger(value)); break;
|
||||||
|
case JoyAxis.Rz: _pad.SetSliderValue(Xbox360Slider.RightTrigger, ToTrigger(value)); break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
_pad.SubmitReport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIO axis 0..32766 (centre 16383) -> Xbox thumb short -32768..32767.
|
||||||
|
private static short ToThumb(int value) =>
|
||||||
|
(short)Math.Clamp((value - AxisOutputs.Center) * 2, short.MinValue, short.MaxValue);
|
||||||
|
|
||||||
|
// RIO axis 0..32766 -> Xbox trigger byte 0..255.
|
||||||
|
private static byte ToTrigger(int value) =>
|
||||||
|
(byte)Math.Clamp(value * 255 / AxisOutputs.Max, 0, 255);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { _pad.Disconnect(); } catch { /* already disconnected / bus gone */ }
|
||||||
|
_client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
|
||||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using RioJoy.Core.Editing;
|
using RioJoy.Core.Editing;
|
||||||
using RioJoy.Core.Hid;
|
using RioJoy.Core.Output;
|
||||||
using RioJoy.Core.Mapping;
|
using RioJoy.Core.Mapping;
|
||||||
using RioJoy.Core.Profiles;
|
using RioJoy.Core.Profiles;
|
||||||
using RioJoy.Core.Protocol;
|
using RioJoy.Core.Protocol;
|
||||||
@@ -262,8 +262,9 @@ public sealed class ProfileEditorForm : Form
|
|||||||
_valueCombo.Items.Add(new ValueItem(k.Name, k.Value));
|
_valueCombo.Items.Add(new ValueItem(k.Name, k.Value));
|
||||||
break;
|
break;
|
||||||
case RioRouteKind.Joystick:
|
case RioRouteKind.Joystick:
|
||||||
for (int btn = 1; btn <= RioHidReport.ButtonCount; btn++)
|
// The signed ViGEmBus Xbox 360 pad exposes 11 buttons; show their names.
|
||||||
_valueCombo.Items.Add(new ValueItem($"Button {btn}", (byte)btn));
|
for (int btn = 1; btn <= ViGEmJoystickSink.MappableButtonCount; btn++)
|
||||||
|
_valueCombo.Items.Add(new ValueItem($"Button {btn} ({ViGEmJoystickSink.ButtonNames[btn - 1]})", (byte)btn));
|
||||||
break;
|
break;
|
||||||
case RioRouteKind.Hat:
|
case RioRouteKind.Hat:
|
||||||
foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left })
|
foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left })
|
||||||
|
|||||||
@@ -146,6 +146,30 @@ public sealed class RioCoordinator : IDisposable
|
|||||||
GoDormant("Dormant"); // the next watcher poll re-activates if a profiled game is foreground
|
GoDormant("Dormant"); // the next watcher poll re-activates if a profiled game is foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick the best available virtual-joystick sink and record it for disposal: the
|
||||||
|
// signed ViGEmBus Xbox 360 pad first, then the (test-signed) RioGamepad HID
|
||||||
|
// feeder, then a no-op when neither driver is present.
|
||||||
|
private IJoystickSink CreateJoystickSink(out string note)
|
||||||
|
{
|
||||||
|
if (ViGEmJoystickSink.TryCreate(out ViGEmJoystickSink? vigem))
|
||||||
|
{
|
||||||
|
_joystick = vigem;
|
||||||
|
note = string.Empty;
|
||||||
|
return vigem!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
|
||||||
|
{
|
||||||
|
_joystick = feeder;
|
||||||
|
note = string.Empty;
|
||||||
|
return feeder!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_joystick = null;
|
||||||
|
note = " [no joystick driver]";
|
||||||
|
return new NullJoystickSink();
|
||||||
|
}
|
||||||
|
|
||||||
// routeInput=false holds the RIO link (and lamp feedback) but sends keyboard/
|
// routeInput=false holds the RIO link (and lamp feedback) but sends keyboard/
|
||||||
// mouse/joystick output to no-op sinks — used for the editor's live button check.
|
// mouse/joystick output to no-op sinks — used for the editor's live button check.
|
||||||
private void Activate(RioProfile profile, bool routeInput = true)
|
private void Activate(RioProfile profile, bool routeInput = true)
|
||||||
@@ -166,41 +190,22 @@ public sealed class RioCoordinator : IDisposable
|
|||||||
IInputSink input;
|
IInputSink input;
|
||||||
IJoystickSink joystick;
|
IJoystickSink joystick;
|
||||||
string note;
|
string note;
|
||||||
|
IJoystickSink realJoystick = CreateJoystickSink(out string joystickNote);
|
||||||
if (!routeInput)
|
if (!routeInput)
|
||||||
{
|
{
|
||||||
IJoystickSink realJoystick;
|
// Keyboard/mouse + joystick are gated off while editing so the RIO can
|
||||||
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? editorFeeder))
|
// be exercised without injecting input (the editor toggle opens them).
|
||||||
{
|
|
||||||
realJoystick = editorFeeder!;
|
|
||||||
_joystick = editorFeeder;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
realJoystick = new NullJoystickSink();
|
|
||||||
_joystick = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_editorInput = new GatedInputSink(new SendInputSink());
|
_editorInput = new GatedInputSink(new SendInputSink());
|
||||||
_editorJoystick = new GatedJoystickSink(realJoystick);
|
_editorJoystick = new GatedJoystickSink(realJoystick);
|
||||||
input = _editorInput;
|
input = _editorInput;
|
||||||
joystick = _editorJoystick;
|
joystick = _editorJoystick;
|
||||||
note = " [editing — outputs off]";
|
note = " [editing — outputs off]";
|
||||||
}
|
}
|
||||||
else if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
|
|
||||||
{
|
|
||||||
// Use the real virtual-joystick driver if installed.
|
|
||||||
input = new SendInputSink();
|
|
||||||
joystick = feeder!;
|
|
||||||
_joystick = feeder;
|
|
||||||
note = string.Empty;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Otherwise fall back to a no-op joystick (keyboard/mouse still work).
|
|
||||||
input = new SendInputSink();
|
input = new SendInputSink();
|
||||||
joystick = new NullJoystickSink();
|
joystick = realJoystick;
|
||||||
_joystick = null;
|
note = joystickNote;
|
||||||
note = " [no joystick driver]";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_transport = _transportFactory(port);
|
_transport = _transportFactory(port);
|
||||||
|
|||||||
Reference in New Issue
Block a user