From cdf455f494420105a894e37868a27f171f564711 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 15 Mar 2019 17:33:29 -0500 Subject: [PATCH] XInput support (for XBOX controllers). --- src/JsPie.Cli/Program.cs | 204 ++++++------- src/JsPie.Plugins/JsPie.Plugins.csproj | 230 +++++++-------- src/JsPie.Plugins/XInput/XInputApi.cs | 269 ++++++++++++++++++ .../XInput/XInputButtonControl.cs | 21 ++ src/JsPie.Plugins/XInput/XInputControl.cs | 27 ++ src/JsPie.Plugins/XInput/XInputControlInfo.cs | 18 ++ src/JsPie.Plugins/XInput/XInputControlSet.cs | 79 +++++ src/JsPie.Plugins/XInput/XInputPlugin.cs | 137 +++++++++ .../XInput/XInputThumbControl.cs | 18 ++ .../XInput/XInputTriggerControl.cs | 17 ++ 10 files changed, 810 insertions(+), 210 deletions(-) create mode 100644 src/JsPie.Plugins/XInput/XInputApi.cs create mode 100644 src/JsPie.Plugins/XInput/XInputButtonControl.cs create mode 100644 src/JsPie.Plugins/XInput/XInputControl.cs create mode 100644 src/JsPie.Plugins/XInput/XInputControlInfo.cs create mode 100644 src/JsPie.Plugins/XInput/XInputControlSet.cs create mode 100644 src/JsPie.Plugins/XInput/XInputPlugin.cs create mode 100644 src/JsPie.Plugins/XInput/XInputThumbControl.cs create mode 100644 src/JsPie.Plugins/XInput/XInputTriggerControl.cs diff --git a/src/JsPie.Cli/Program.cs b/src/JsPie.Cli/Program.cs index a99bbd4..7e04140 100644 --- a/src/JsPie.Cli/Program.cs +++ b/src/JsPie.Cli/Program.cs @@ -1,99 +1,105 @@ -using JsPie.Plugins.Keyboard; -using System.Windows.Forms; -using JsPie.Core; -using JsPie.Scripting; -using JsPie.Runtime; -using System.Collections.Generic; -using JsPie.Plugins.Ps3; -using System.Linq; -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using JsPie.Plugins.VJoy; - -namespace JsPie.Cli -{ - class Program - { - static KeyboardPlugin _keyboardPlugin; - static Ps3Plugin _ps3Plugin; - static VJoyPlugin _vJoyPlugin; - static ConcurrentQueue _queue; - static AutoResetEvent _event; - static IJsPieServiceProvider _serviceProvider; - static Task _scriptTask; - - static object _lockObject; - - static void Main(string[] args) - { - _lockObject = new object(); - _queue = new ConcurrentQueue(); - _event = new AutoResetEvent(false); - - _queue.Enqueue(new ScriptInput(new ControlEvent[0])); - - using (_keyboardPlugin = new KeyboardPlugin()) - using (_ps3Plugin = new Ps3Plugin()) - using (_vJoyPlugin = new VJoyPlugin()) - { - _keyboardPlugin.ControlEvent += OnControlEvent; - _keyboardPlugin.ControlEvents += OnControlEvents; - - _ps3Plugin.ControlEvent += OnControlEvent; - _ps3Plugin.ControlEvents += OnControlEvents; - - var serviceProvider = new DefaultJsPieServiceProvider(); - serviceProvider.Register(() => new ScriptConsole(ScriptSeverity.Info)); - var settings = new ScriptEngineSettings(args.Length > 0 ? args[0] : "D:\\Development\\JsPie\\test.js"); - serviceProvider.Register(() => settings); - var directory = new ControllerDirectory( - new IInputPlugin[] { _keyboardPlugin, _ps3Plugin }.SelectMany(p => p.GetControllers()), - new IOutputPlugin[] { _keyboardPlugin, _vJoyPlugin }.SelectMany(p => p.GetControllers())); - serviceProvider.Register(() => directory); - _serviceProvider = serviceProvider; - _scriptTask = Task.Factory.StartNew(Run); - - Application.Run(); - } - } - - private static void OnControlEvent(object sender, ControlEvent e) - { - _queue.Enqueue(new ScriptInput(e)); - _event.Set(); - } - - private static void OnControlEvents(object sender, IReadOnlyList e) - { - _queue.Enqueue(new ScriptInput(e)); - _event.Set(); - } - - private static void Run() - { - using (var engine = _serviceProvider.GetRequiredService()) - { - engine.Initialize(); - - while (true) - { - IScriptInput input; - if (_queue.TryDequeue(out input)) - { - var output = engine.Run(input); - if (output.WasSuccessful && output.HasValue) - { - _keyboardPlugin.ProcessEvents(output.Value.ControlEvents); - _vJoyPlugin.ProcessEvents(output.Value.ControlEvents); - } - continue; - } - - _event.WaitOne(); - } - } - } - } -} +using JsPie.Plugins.Keyboard; +using System.Windows.Forms; +using JsPie.Core; +using JsPie.Scripting; +using JsPie.Runtime; +using System.Collections.Generic; +using JsPie.Plugins.Ps3; +using System.Linq; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using JsPie.Plugins.VJoy; +using JsPie.Plugins.XInput; + +namespace JsPie.Cli +{ + class Program + { + static KeyboardPlugin _keyboardPlugin; + static Ps3Plugin _ps3Plugin; + static VJoyPlugin _vJoyPlugin; + static XInputPlugin _xInputPlugin; + static ConcurrentQueue _queue; + static AutoResetEvent _event; + static IJsPieServiceProvider _serviceProvider; + static Task _scriptTask; + + static object _lockObject; + + static void Main(string[] args) + { + _lockObject = new object(); + _queue = new ConcurrentQueue(); + _event = new AutoResetEvent(false); + + _queue.Enqueue(new ScriptInput(new ControlEvent[0])); + + using (_keyboardPlugin = new KeyboardPlugin()) + using (_ps3Plugin = new Ps3Plugin()) + using (_vJoyPlugin = new VJoyPlugin()) + using (_xInputPlugin = new XInputPlugin()) + { + _keyboardPlugin.ControlEvent += OnControlEvent; + _keyboardPlugin.ControlEvents += OnControlEvents; + + _ps3Plugin.ControlEvent += OnControlEvent; + _ps3Plugin.ControlEvents += OnControlEvents; + + _xInputPlugin.ControlEvent += OnControlEvent; + _xInputPlugin.ControlEvents += OnControlEvents; + + var serviceProvider = new DefaultJsPieServiceProvider(); + serviceProvider.Register(() => new ScriptConsole(ScriptSeverity.Info)); + var settings = new ScriptEngineSettings(args.Length > 0 ? args[0] : "D:\\Development\\JsPie\\test.js"); + serviceProvider.Register(() => settings); + var directory = new ControllerDirectory( + new IInputPlugin[] { _keyboardPlugin, _ps3Plugin, _xInputPlugin }.SelectMany(p => p.GetControllers()), + new IOutputPlugin[] { _keyboardPlugin, _vJoyPlugin }.SelectMany(p => p.GetControllers())); + serviceProvider.Register(() => directory); + _serviceProvider = serviceProvider; + _scriptTask = Task.Factory.StartNew(Run); + + Application.Run(); + } + } + + private static void OnControlEvent(object sender, ControlEvent e) + { + _queue.Enqueue(new ScriptInput(e)); + _event.Set(); + } + + private static void OnControlEvents(object sender, IReadOnlyList e) + { + _queue.Enqueue(new ScriptInput(e)); + _event.Set(); + } + + private static void Run() + { + using (var engine = _serviceProvider.GetRequiredService()) + { + engine.Initialize(); + + while (true) + { + IScriptInput input; + if (_queue.TryDequeue(out input)) + { + var output = engine.Run(input); + if (output.WasSuccessful && output.HasValue) + { + _keyboardPlugin.ProcessEvents(output.Value.ControlEvents); + _vJoyPlugin.ProcessEvents(output.Value.ControlEvents); + } + continue; + } + + _event.WaitOne(); + } + } + } + } +} diff --git a/src/JsPie.Plugins/JsPie.Plugins.csproj b/src/JsPie.Plugins/JsPie.Plugins.csproj index 6727909..1f8727a 100644 --- a/src/JsPie.Plugins/JsPie.Plugins.csproj +++ b/src/JsPie.Plugins/JsPie.Plugins.csproj @@ -1,112 +1,120 @@ - - - - - Debug - AnyCPU - {E10DD29E-CE54-47C0-9D64-902844183A96} - Library - Properties - JsPie.Plugins - JsPie.Plugins - v4.6.1 - 512 - - - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - prompt - MinimumRecommendedRules.ruleset - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - prompt - MinimumRecommendedRules.ruleset - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - prompt - MinimumRecommendedRules.ruleset - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - prompt - MinimumRecommendedRules.ruleset - - - - - - - - - ..\..\external\vJoy\$(PlatformTarget)\vJoyInterfaceWrap.dll - - - - - Properties\CommonAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {fbaa8045-f4e7-49cb-bd05-589d5e894745} - JsPie.Core - - - - - - - Always - - + + + + + Debug + AnyCPU + {E10DD29E-CE54-47C0-9D64-902844183A96} + Library + Properties + JsPie.Plugins + JsPie.Plugins + v4.6.1 + 512 + + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + + + + + + ..\..\external\vJoy\$(PlatformTarget)\vJoyInterfaceWrap.dll + + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {fbaa8045-f4e7-49cb-bd05-589d5e894745} + JsPie.Core + + + + + + + Always + + \ No newline at end of file diff --git a/src/JsPie.Plugins/XInput/XInputApi.cs b/src/JsPie.Plugins/XInput/XInputApi.cs new file mode 100644 index 0000000..dc505eb --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputApi.cs @@ -0,0 +1,269 @@ +using System; +using System.Runtime.InteropServices; + +namespace JsPie.Plugins.XInput +{ + public static class XInputApi + { + public const uint ERROR_SUCCESS = 0; + public const uint ERROR_DEVICE_NOT_CONNECTED = 1167; + + // + // Device types available in XINPUT_CAPABILITIES + // + public const byte XINPUT_DEVTYPE_GAMEPAD = 0x01; + + // + // Device subtypes available in XINPUT_CAPABILITIES + // + public const byte XINPUT_DEVSUBTYPE_GAMEPAD = 0x01; + + public const byte XINPUT_DEVSUBTYPE_UNKNOWN = 0x00; + public const byte XINPUT_DEVSUBTYPE_WHEEL = 0x02; + public const byte XINPUT_DEVSUBTYPE_ARCADE_STICK = 0x03; + public const byte XINPUT_DEVSUBTYPE_FLIGHT_STICK = 0x04; + public const byte XINPUT_DEVSUBTYPE_DANCE_PAD = 0x05; + public const byte XINPUT_DEVSUBTYPE_GUITAR = 0x06; + public const byte XINPUT_DEVSUBTYPE_GUITAR_ALTERNATE = 0x07; + public const byte XINPUT_DEVSUBTYPE_DRUM_KIT = 0x08; + public const byte XINPUT_DEVSUBTYPE_GUITAR_BASS = 0x0B; + public const byte XINPUT_DEVSUBTYPE_ARCADE_PAD = 0x13; + + // + // Flags for XINPUT_CAPABILITIES + // + public const ushort XINPUT_CAPS_VOICE_SUPPORTED = 0x0004; + + public const ushort XINPUT_CAPS_FFB_SUPPORTED = 0x0001; + public const ushort XINPUT_CAPS_WIRELESS = 0x0002; + public const ushort XINPUT_CAPS_PMD_SUPPORTED = 0x0008; + public const ushort XINPUT_CAPS_NO_NAVIGATION = 0x0010; + + // + // Constants for gamepad buttons + // + public const ushort XINPUT_GAMEPAD_DPAD_UP = 0x0001; + public const ushort XINPUT_GAMEPAD_DPAD_DOWN = 0x0002; + public const ushort XINPUT_GAMEPAD_DPAD_LEFT = 0x0004; + public const ushort XINPUT_GAMEPAD_DPAD_RIGHT = 0x0008; + public const ushort XINPUT_GAMEPAD_START = 0x0010; + public const ushort XINPUT_GAMEPAD_BACK = 0x0020; + public const ushort XINPUT_GAMEPAD_LEFT_THUMB = 0x0040; + public const ushort XINPUT_GAMEPAD_RIGHT_THUMB = 0x0080; + public const ushort XINPUT_GAMEPAD_LEFT_SHOULDER = 0x0100; + public const ushort XINPUT_GAMEPAD_RIGHT_SHOULDER = 0x0200; + public const ushort XINPUT_GAMEPAD_A = 0x1000; + public const ushort XINPUT_GAMEPAD_B = 0x2000; + public const ushort XINPUT_GAMEPAD_X = 0x4000; + public const ushort XINPUT_GAMEPAD_Y = 0x8000; + + // + // Gamepad thresholds + // + public const int XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE = 7849; + public const int XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE = 8689; + public const int XINPUT_GAMEPAD_TRIGGER_THRESHOLD = 30; + + // + // Flags to pass to XInputGetCapabilities + // + public const uint XINPUT_FLAG_GAMEPAD = 0x00000001; + + // + // Devices that support batteries + // + public const byte BATTERY_DEVTYPE_GAMEPAD = 0x00; + public const byte BATTERY_DEVTYPE_HEADSET = 0x01; + + // + // Flags for battery status level + // + public const byte BATTERY_TYPE_DISCONNECTED = 0x00; // This device is not connected + public const byte BATTERY_TYPE_WIRED = 0x01; // Wired device, no battery + public const byte BATTERY_TYPE_ALKALINE = 0x02; // Alkaline battery source + public const byte BATTERY_TYPE_NIMH = 0x03; // Nickel Metal Hydride battery source + public const byte BATTERY_TYPE_UNKNOWN = 0xFF; // Cannot determine the battery type + + // These are only valid for wireless, connected devices, with known battery types + // The amount of use time remaining depends on the type of device. + public const byte BATTERY_LEVEL_EMPTY = 0x00; + public const byte BATTERY_LEVEL_LOW = 0x01; + public const byte BATTERY_LEVEL_MEDIUM = 0x02; + public const byte BATTERY_LEVEL_FULL = 0x03; + + // User index definitions + public const uint XUSER_MAX_COUNT = 4; + + public const uint XUSER_INDEX_ANY = 0x000000FF; + + // + // Codes returned for the gamepad keystroke + // + + public const ushort VK_PAD_A = 0x5800; + public const ushort VK_PAD_B = 0x5801; + public const ushort VK_PAD_X = 0x5802; + public const ushort VK_PAD_Y = 0x5803; + public const ushort VK_PAD_RSHOULDER = 0x5804; + public const ushort VK_PAD_LSHOULDER = 0x5805; + public const ushort VK_PAD_LTRIGGER = 0x5806; + public const ushort VK_PAD_RTRIGGER = 0x5807; + + public const ushort VK_PAD_DPAD_UP = 0x5810; + public const ushort VK_PAD_DPAD_DOWN = 0x5811; + public const ushort VK_PAD_DPAD_LEFT = 0x5812; + public const ushort VK_PAD_DPAD_RIGHT = 0x5813; + public const ushort VK_PAD_START = 0x5814; + public const ushort VK_PAD_BACK = 0x5815; + public const ushort VK_PAD_LTHUMB_PRESS = 0x5816; + public const ushort VK_PAD_RTHUMB_PRESS = 0x5817; + + public const ushort VK_PAD_LTHUMB_UP = 0x5820; + public const ushort VK_PAD_LTHUMB_DOWN = 0x5821; + public const ushort VK_PAD_LTHUMB_RIGHT = 0x5822; + public const ushort VK_PAD_LTHUMB_LEFT = 0x5823; + public const ushort VK_PAD_LTHUMB_UPLEFT = 0x5824; + public const ushort VK_PAD_LTHUMB_UPRIGHT = 0x5825; + public const ushort VK_PAD_LTHUMB_DOWNRIGHT = 0x5826; + public const ushort VK_PAD_LTHUMB_DOWNLEFT = 0x5827; + + public const ushort VK_PAD_RTHUMB_UP = 0x5830; + public const ushort VK_PAD_RTHUMB_DOWN = 0x5831; + public const ushort VK_PAD_RTHUMB_RIGHT = 0x5832; + public const ushort VK_PAD_RTHUMB_LEFT = 0x5833; + public const ushort VK_PAD_RTHUMB_UPLEFT = 0x5834; + public const ushort VK_PAD_RTHUMB_UPRIGHT = 0x5835; + public const ushort VK_PAD_RTHUMB_DOWNRIGHT = 0x5836; + public const ushort VK_PAD_RTHUMB_DOWNLEFT = 0x5837; + + // + // Flags used in XINPUT_KEYSTROKE + // + public const ushort XINPUT_KEYSTROKE_KEYDOWN = 0x0001; + public const ushort XINPUT_KEYSTROKE_KEYUP = 0x0002; + public const ushort XINPUT_KEYSTROKE_REPEAT = 0x0004; + + + // + // Structures used by XInput APIs + // + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public class XINPUT_GAMEPAD + { + public ushort wButtons; + public byte bLeftTrigger; + public byte bRightTrigger; + public short sThumbLX; + public short sThumbLY; + public short sThumbRX; + public short sThumbRY; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public class XINPUT_STATE + { + public uint dwPacketNumber; + public XINPUT_GAMEPAD Gamepad; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public class XINPUT_VIBRATION + { + public ushort wLeftMotorSpeed; + public ushort wRightMotorSpeed; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public class XINPUT_CAPABILITIES + { + public byte Type; + public byte SubType; + public short Flags; + XINPUT_GAMEPAD Gamepad; + XINPUT_VIBRATION Vibration; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public class XINPUT_BATTERY_INFORMATION + { + public byte BatteryType; + public byte BatteryLevel; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public class XINPUT_KEYSTROKE + { + public ushort VirtualKey; + public char Unicode; + public ushort Flags; + public byte UserIndex; + public byte HidCode; + } + + // + // XInput APIs + // + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputGetState + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + [Out] XINPUT_STATE pState // Receives the current state + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputSetState + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + [In] XINPUT_VIBRATION pVibration // The vibration information to send to the controller + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputGetCapabilities + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + [In] uint dwFlags, // Input flags that identify the device type + [Out] XINPUT_CAPABILITIES pCapabilities // Receives the capabilities + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern void XInputEnable + ( + [In] bool enable // [in] Indicates whether xinput is enabled or disabled. + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputGetAudioDeviceIds + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + [Out, Optional, MarshalAs(UnmanagedType.LPTStr)] string pRenderDeviceId, // Windows Core Audio device ID string for render (speakers) + [In, Out, Optional] ref uint pRenderCount, // Size of render device ID string buffer (in wide-chars) + [Out, Optional, MarshalAs(UnmanagedType.LPTStr)] string pCaptureDeviceId, // Windows Core Audio device ID string for capture (microphone) + [In, Out, Optional] ref uint pCaptureCount // Size of capture device ID string buffer (in wide-chars) + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputGetBatteryInformation + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + [In] byte devType, // Which device on this user index + [Out] XINPUT_BATTERY_INFORMATION pBatteryInformation // Contains the level and types of batteries + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputGetKeystroke + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + uint dwReserved, // Reserved for future use + [Out] XINPUT_KEYSTROKE pKeystroke // Pointer to an XINPUT_KEYSTROKE structure that receives an input event. + ); + + [DllImport("xinput1_4.dll", CharSet = CharSet.Auto)] + public static extern uint XInputGetDSoundAudioDeviceGuids + ( + [In] uint dwUserIndex, // Index of the gamer associated with the device + [Out, MarshalAs(UnmanagedType.LPStruct)] out Guid pDSoundRenderGuid, // DSound device ID for render (speakers) + [Out, MarshalAs(UnmanagedType.LPStruct)] out Guid pDSoundCaptureGuid // DSound device ID for capture (microphone) + ); + } +} diff --git a/src/JsPie.Plugins/XInput/XInputButtonControl.cs b/src/JsPie.Plugins/XInput/XInputButtonControl.cs new file mode 100644 index 0000000..d1f9294 --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputButtonControl.cs @@ -0,0 +1,21 @@ +using JsPie.Core; + +namespace JsPie.Plugins.XInput +{ + public class XInputButtonControl : XInputControl + { + private readonly ushort _buttonMask; + + public XInputButtonControl(XInputControlInfo xInputControlInfo, ushort buttonMask) + : base(xInputControlInfo) + { + _buttonMask = buttonMask; + } + + public ControlEvent UpdateValue(ushort buttons) + { + var newValue = ((buttons & _buttonMask) != 0) ? 1f : 0f; + return UpdateValue(newValue); + } + } +} diff --git a/src/JsPie.Plugins/XInput/XInputControl.cs b/src/JsPie.Plugins/XInput/XInputControl.cs new file mode 100644 index 0000000..854f7bf --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputControl.cs @@ -0,0 +1,27 @@ +using JsPie.Core; +using JsPie.Core.Util; + +namespace JsPie.Plugins.XInput +{ + public class XInputControl + { + public XInputControlInfo XInputControlInfo { get; } + public float Value { get; private set; } + + public XInputControl(XInputControlInfo xInputControlInfo) + { + XInputControlInfo = Guard.NotNull(xInputControlInfo, nameof(xInputControlInfo)); + } + + public ControlEvent UpdateValue(float newValue) + { + if (newValue == Value) + { + return null; + } + + Value = newValue; + return new ControlEvent(XInputControlInfo.ControlId, newValue); + } + } +} diff --git a/src/JsPie.Plugins/XInput/XInputControlInfo.cs b/src/JsPie.Plugins/XInput/XInputControlInfo.cs new file mode 100644 index 0000000..e8ded46 --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputControlInfo.cs @@ -0,0 +1,18 @@ +using JsPie.Core; +using JsPie.Core.Util; + +namespace JsPie.Plugins.XInput +{ + public class XInputControlInfo + { + public ControlInfo ControlInfo { get; } + public ControlId ControlId { get; } + + public XInputControlInfo(ControllerId controllerId, ControlInfo controlInfo) + { + Guard.NotNull(controllerId, nameof(controllerId)); + ControlInfo = Guard.NotNull(controlInfo, nameof(controlInfo)); + ControlId = new ControlId(controllerId, controlInfo.Name); + } + } +} diff --git a/src/JsPie.Plugins/XInput/XInputControlSet.cs b/src/JsPie.Plugins/XInput/XInputControlSet.cs new file mode 100644 index 0000000..349dcb2 --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputControlSet.cs @@ -0,0 +1,79 @@ +using JsPie.Core; +using JsPie.Core.Util; +using System.Collections.Generic; +using System.Linq; + +namespace JsPie.Plugins.XInput +{ + public class XInputControlSet + { + private readonly ControllerId _controllerId; + + public XInputControlSet(ControllerId controllerId) + { + _controllerId = Guard.NotNull(controllerId, nameof(controllerId)); + + ButtonControls = new[] + { + Button("up", XInputApi.XINPUT_GAMEPAD_DPAD_UP), + Button("down", XInputApi.XINPUT_GAMEPAD_DPAD_DOWN), + Button("left", XInputApi.XINPUT_GAMEPAD_DPAD_LEFT), + Button("right", XInputApi.XINPUT_GAMEPAD_DPAD_RIGHT), + Button("start", XInputApi.XINPUT_GAMEPAD_START), + Button("back", XInputApi.XINPUT_GAMEPAD_BACK), + Button("leftThumb", XInputApi.XINPUT_GAMEPAD_LEFT_THUMB), + Button("rightThumb", XInputApi.XINPUT_GAMEPAD_RIGHT_THUMB), + Button("leftShoulder", XInputApi.XINPUT_GAMEPAD_LEFT_SHOULDER), + Button("rightShoulder", XInputApi.XINPUT_GAMEPAD_RIGHT_SHOULDER), + Button("a", XInputApi.XINPUT_GAMEPAD_A), + Button("b", XInputApi.XINPUT_GAMEPAD_B), + Button("x", XInputApi.XINPUT_GAMEPAD_X), + Button("y", XInputApi.XINPUT_GAMEPAD_Y) + }; + + LeftTriggerControl = Trigger("leftTrigger"); + RightTriggerControl = Trigger("rightTrigger"); + LeftThumbXControl = Thumb("leftThumbX"); + LeftThumbYControl = Thumb("leftThumbY"); + RightThumbXControl = Thumb("rightThumbX"); + RightThumbYControl = Thumb("rightThumbY"); + } + + public IReadOnlyList ButtonControls { get; private set; } + public XInputTriggerControl LeftTriggerControl { get; private set; } + public XInputTriggerControl RightTriggerControl { get; private set; } + public XInputThumbControl LeftThumbXControl { get; private set; } + public XInputThumbControl LeftThumbYControl { get; private set; } + public XInputThumbControl RightThumbXControl { get; private set; } + public XInputThumbControl RightThumbYControl { get; private set; } + + public IReadOnlyList Controls { get + { + return ButtonControls.Concat(new XInputControl[] + { + LeftTriggerControl, + RightTriggerControl, + LeftThumbXControl, + LeftThumbYControl, + RightThumbXControl, + RightThumbYControl + }).ToList(); + } + } + + private XInputButtonControl Button(string name, ushort buttonMask, string description = null) + { + return new XInputButtonControl(new XInputControlInfo(_controllerId, new ControlInfo(name, description)), buttonMask); + } + + private XInputTriggerControl Trigger(string name, string description = null) + { + return new XInputTriggerControl(new XInputControlInfo(_controllerId, new ControlInfo(name, description))); + } + + private XInputThumbControl Thumb(string name, string description = null) + { + return new XInputThumbControl(new XInputControlInfo(_controllerId, new ControlInfo(name, description))); + } + } +} diff --git a/src/JsPie.Plugins/XInput/XInputPlugin.cs b/src/JsPie.Plugins/XInput/XInputPlugin.cs new file mode 100644 index 0000000..d5aa23a --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputPlugin.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using JsPie.Core; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; + +namespace JsPie.Plugins.XInput +{ + public class XInputPlugin : IInputPlugin, IDisposable + { + private static readonly ControllerId ControllerId = new ControllerId("xinput"); + + private readonly XInputControlSet _controlSet; + private readonly ControllerInfo _controllerInfo; + + private readonly Task _pollTask; + + private uint _lastResult; + private uint _lastPacketNumber; + private bool _isDisposed; + + public XInputPlugin() + { + _controlSet = new XInputControlSet(ControllerId); + _controllerInfo = new ControllerInfo(ControllerId.Name, 1, "XInput gamepad", _controlSet.Controls.Select(c => c.XInputControlInfo.ControlInfo)); + + // TODO: Multiple controllers and detect when controllers are connected / disconnected + + _lastResult = uint.MaxValue; + + _pollTask = Task.Factory.StartNew(Poll, TaskCreationOptions.LongRunning); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + + _pollTask.Wait(); + } + + public event ControlEventHandler ControlEvent; + public event ControlEventsHandler ControlEvents; + + public IEnumerable GetControllers() + { + yield return _controllerInfo; + } + + private void Poll() + { + while (!_isDisposed) + { + var state = new XInputApi.XINPUT_STATE(); + var result = XInputApi.XInputGetState(0, state); + + var lastResult = _lastResult; + _lastResult = result; + + if (_isDisposed) + break; + + if (result != XInputApi.ERROR_SUCCESS) + { + if (result != lastResult) + { + if (result == XInputApi.ERROR_DEVICE_NOT_CONNECTED) + { + Console.WriteLine("No XInput gamepad connected."); + } + else + { + Console.WriteLine("Unexpected error result from XInputGetState: " + result); + } + } + + Thread.Sleep(1000); + continue; + } + + if (result != lastResult) + { + Console.WriteLine("XInput gamepad connected."); + + // Wait a second after connection to give JsPie a chance to initialize so that the queue does not back up + Thread.Sleep(1000); + continue; + } + + if (_lastPacketNumber == state.dwPacketNumber) + { + Thread.Sleep(5); + continue; + } + + _lastPacketNumber = state.dwPacketNumber; + + var events = (List)null; + var gamepad = state.Gamepad; + + foreach (var button in _controlSet.ButtonControls) + { + ProcessEvent(button.UpdateValue(gamepad.wButtons), ref events); + } + + ProcessEvent(_controlSet.LeftTriggerControl.UpdateValue(gamepad.bLeftTrigger), ref events); + ProcessEvent(_controlSet.RightTriggerControl.UpdateValue(gamepad.bRightTrigger), ref events); + ProcessEvent(_controlSet.LeftThumbXControl.UpdateValue(gamepad.sThumbLX), ref events); + ProcessEvent(_controlSet.LeftThumbYControl.UpdateValue(gamepad.sThumbLY), ref events); + ProcessEvent(_controlSet.RightThumbXControl.UpdateValue(gamepad.sThumbRX), ref events); + ProcessEvent(_controlSet.RightThumbYControl.UpdateValue(gamepad.sThumbRY), ref events); + + if (events != null) + { + ControlEvents?.Invoke(this, events); + } + + Thread.Sleep(5); + } + } + + private void ProcessEvent(ControlEvent @event, ref List events) + { + if (@event == null) return; + + if (events == null) + { + events = new List(); + } + + events.Add(@event); + } + } +} diff --git a/src/JsPie.Plugins/XInput/XInputThumbControl.cs b/src/JsPie.Plugins/XInput/XInputThumbControl.cs new file mode 100644 index 0000000..bd0abc7 --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputThumbControl.cs @@ -0,0 +1,18 @@ +using JsPie.Core; +using System; + +namespace JsPie.Plugins.XInput +{ + public class XInputThumbControl : XInputControl + { + public XInputThumbControl(XInputControlInfo xInputControlInfo) + : base(xInputControlInfo) + { } + + public ControlEvent UpdateValue(short value) + { + var newValue = (float)Math.Max((short)-32767, value) / 32767f; + return UpdateValue(newValue); + } + } +} diff --git a/src/JsPie.Plugins/XInput/XInputTriggerControl.cs b/src/JsPie.Plugins/XInput/XInputTriggerControl.cs new file mode 100644 index 0000000..78eac6b --- /dev/null +++ b/src/JsPie.Plugins/XInput/XInputTriggerControl.cs @@ -0,0 +1,17 @@ +using JsPie.Core; + +namespace JsPie.Plugins.XInput +{ + public class XInputTriggerControl : XInputControl + { + public XInputTriggerControl(XInputControlInfo xInputControlInfo) + : base(xInputControlInfo) + { } + + public ControlEvent UpdateValue(byte value) + { + var newValue = (float)value / 255f; + return UpdateValue(newValue); + } + } +}