diff --git a/src/RioJoy.Core/Output/ViGEmJoystickSink.cs b/src/RioJoy.Core/Output/ViGEmJoystickSink.cs new file mode 100644 index 0000000..a40849d --- /dev/null +++ b/src/RioJoy.Core/Output/ViGEmJoystickSink.cs @@ -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; + +/// +/// backed by a signed ViGEmBus 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 +/// are dropped (map those to the keyboard). +/// +[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, + }; + + /// Display names for the mappable buttons, aligned with . + public static IReadOnlyList ButtonNames { get; } = new[] + { + "A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide", + }; + + /// How many RIO joystick buttons map onto the Xbox 360 pad (11). + 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; + } + + /// + /// Try to connect to ViGEmBus and create a virtual Xbox 360 controller. Returns + /// if ViGEmBus is not installed, so callers can fall back + /// to another joystick sink. + /// + 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(); + } +} diff --git a/src/RioJoy.Core/RioJoy.Core.csproj b/src/RioJoy.Core/RioJoy.Core.csproj index 6420a8f..b2e5387 100644 --- a/src/RioJoy.Core/RioJoy.Core.csproj +++ b/src/RioJoy.Core/RioJoy.Core.csproj @@ -11,6 +11,7 @@ + diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs index 78d0795..ba45ced 100644 --- a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs +++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs @@ -1,7 +1,7 @@ using System.Runtime.Versioning; using System.Text; using RioJoy.Core.Editing; -using RioJoy.Core.Hid; +using RioJoy.Core.Output; using RioJoy.Core.Mapping; using RioJoy.Core.Profiles; using RioJoy.Core.Protocol; @@ -262,8 +262,9 @@ public sealed class ProfileEditorForm : Form _valueCombo.Items.Add(new ValueItem(k.Name, k.Value)); break; case RioRouteKind.Joystick: - for (int btn = 1; btn <= RioHidReport.ButtonCount; btn++) - _valueCombo.Items.Add(new ValueItem($"Button {btn}", (byte)btn)); + // The signed ViGEmBus Xbox 360 pad exposes 11 buttons; show their names. + for (int btn = 1; btn <= ViGEmJoystickSink.MappableButtonCount; btn++) + _valueCombo.Items.Add(new ValueItem($"Button {btn} ({ViGEmJoystickSink.ButtonNames[btn - 1]})", (byte)btn)); break; case RioRouteKind.Hat: foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left }) diff --git a/src/RioJoy.Tray/RioCoordinator.cs b/src/RioJoy.Tray/RioCoordinator.cs index 8cc8e93..4ce6da1 100644 --- a/src/RioJoy.Tray/RioCoordinator.cs +++ b/src/RioJoy.Tray/RioCoordinator.cs @@ -146,6 +146,30 @@ public sealed class RioCoordinator : IDisposable 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/ // mouse/joystick output to no-op sinks — used for the editor's live button check. private void Activate(RioProfile profile, bool routeInput = true) @@ -166,41 +190,22 @@ public sealed class RioCoordinator : IDisposable IInputSink input; IJoystickSink joystick; string note; + IJoystickSink realJoystick = CreateJoystickSink(out string joystickNote); if (!routeInput) { - IJoystickSink realJoystick; - if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? editorFeeder)) - { - realJoystick = editorFeeder!; - _joystick = editorFeeder; - } - else - { - realJoystick = new NullJoystickSink(); - _joystick = null; - } - + // Keyboard/mouse + joystick are gated off while editing so the RIO can + // be exercised without injecting input (the editor toggle opens them). _editorInput = new GatedInputSink(new SendInputSink()); _editorJoystick = new GatedJoystickSink(realJoystick); input = _editorInput; joystick = _editorJoystick; 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 { - // Otherwise fall back to a no-op joystick (keyboard/mouse still work). input = new SendInputSink(); - joystick = new NullJoystickSink(); - _joystick = null; - note = " [no joystick driver]"; + joystick = realJoystick; + note = joystickNote; } _transport = _transportFactory(port);