diff --git a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj index 86dcd49b0c..3b94629eba 100644 --- a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj +++ b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj @@ -84,7 +84,7 @@ ..\output\dll\nlua\NLua.dll False - + False ..\References\OpenTK.dll diff --git a/BizHawk.Client.EmuHawk/Input/Input.cs b/BizHawk.Client.EmuHawk/Input/Input.cs index a890029682..c1d873ad76 100644 --- a/BizHawk.Client.EmuHawk/Input/Input.cs +++ b/BizHawk.Client.EmuHawk/Input/Input.cs @@ -133,7 +133,7 @@ namespace BizHawk.Client.EmuHawk else { OTK_Keyboard.Initialize(); -// OTK_Gamepad.Initialize(); + OTK_GamePad.Initialize(); } Instance = new Input(); } @@ -341,7 +341,7 @@ namespace BizHawk.Client.EmuHawk } else { - //TODO + OTK_GamePad.UpdateAll(); } //this block is going to massively modify data structures that the binding method uses, so we have to lock it all @@ -357,6 +357,22 @@ namespace BizHawk.Client.EmuHawk { //FloatValues.Clear(); + // analyse OpenTK xinput (or is it libinput?) + foreach (var pad in OTK_GamePad.EnumerateDevices()) + { + foreach (var but in pad.buttonObjects) + { + HandleButton(pad.InputNamePrefix + but.ButtonName, but.ButtonAction()); + } + foreach (var sv in pad.GetFloats()) + { + var n = $"{pad.InputNamePrefix}{sv.Item1} Axis"; + var f = sv.Item2; + if (trackdeltas) FloatDeltas[n] += Math.Abs(f - FloatValues[n]); + FloatValues[n] = f; + } + } + //analyze xinput foreach (var pad in GamePad360.EnumerateDevices()) { diff --git a/BizHawk.Client.EmuHawk/Input/OTK_Gamepad.cs b/BizHawk.Client.EmuHawk/Input/OTK_Gamepad.cs index da73c5e2ac..e9109a3d87 100644 --- a/BizHawk.Client.EmuHawk/Input/OTK_Gamepad.cs +++ b/BizHawk.Client.EmuHawk/Input/OTK_Gamepad.cs @@ -1,148 +1,367 @@ using System; using System.Collections.Generic; +using System.Diagnostics; + using OpenTK.Input; +using OpenTKGamePad = OpenTK.Input.GamePad; + namespace BizHawk.Client.EmuHawk { + /// + /// Modified OpenTK Gamepad Handler
+ /// The jump from OpenTK 1.x to 3.x broke the original OpenTK.Input.Joystick implementation, but we gain OpenTK.Input.GamePad support on Unix. However, the gamepad auto-mapping is a little suspect, so we use both methods.
+ /// As a side-effect, it should make it easier to implement virtual→host haptics in the future. + ///
public class OTK_GamePad { - //Note: OpenTK has both Gamepad and Joystick classes. An OpenTK Gamepad is a simplified version of Joystick - //with pre-defined features that match an XInput controller. They did this to mimic XNA's controller API. - //We're going to use Joystick directly, because it gives us full access to all possible buttons. - //And it looks like GamePad itself isn't supported on OpenTK OS X. + #region Static Members - public static List Devices; - private const int MAX_JOYSTICKS = 4; //They don't have a way to query this for some reason. 4 is the minimum promised. + /// They don't have a way to query this for some reason. 4 is the minimum promised. + private const int MAX_GAMEPADS = 4; + private static readonly object _syncObj = new object(); + + private static readonly List Devices = new List(); + + /// Initialization is only called once when MainForm loads public static void Initialize() { - Devices = new List(); - - for (int i = 0; i < MAX_JOYSTICKS; i++) + CloseAll(); + var playerCount = 0; + for (var i = 0; i < MAX_GAMEPADS; i++) { - JoystickState jss = Joystick.GetState(i); - if (jss.IsConnected) + if (OpenTKGamePad.GetState(i).IsConnected || Joystick.GetState(i).IsConnected) { - Console.WriteLine($"joydevice index: {i}"); //OpenTK doesn't expose the GUID, even though it stores it internally... - - OTK_GamePad ogp = new OTK_GamePad(i); - Devices.Add(ogp); + Console.WriteLine($"OTK GamePad/Joystick index: {i}"); + Devices.Add(new OTK_GamePad(i, ++playerCount)); } } + } + public static IEnumerable EnumerateDevices() + { + lock (_syncObj) + { + foreach (var device in Devices) yield return device; + } } public static void UpdateAll() { - foreach (var device in Devices) - device.Update(); + lock (_syncObj) + { + foreach (var device in Devices) device.Update(); + } } public static void CloseAll() { - if (Devices != null) + lock (_syncObj) { Devices.Clear(); } } - // ********************************** Instance Members ********************************** - - readonly Guid _guid; - readonly int _stickIdx; - JoystickState state = new JoystickState(); - - OTK_GamePad(int index) + /// The things that use (analog controls) appear to require values -10000.0..10000.0 rather than the -1.0..1.0 that OpenTK returns (although even then the results may be slightly outside of these bounds) + private static float ConstrainFloatInput(float num) { - _guid = Guid.NewGuid(); - _stickIdx = index; + if (num > 1) return 10000.0f; + if (num < -1) return -10000.0f; + return num * 10000.0f; + } + + #endregion + + #region Instance Members + + /// The GUID as detected by OpenTK.Input.Joystick (or if that failed, a random one generated on construction) + public readonly Guid Guid; + + /// Signals whether OpenTK returned a GUID for this device + private readonly bool _guidObtained; + + /// The OpenTK device index + private readonly int _deviceIndex; + + /// The index to lookup into Devices + private readonly int _playerIndex; + + /// The name (if any) that OpenTK GamePad has resolved via its internal mapping database + private readonly string _name; + + /// The object returned by + private readonly GamePadCapabilities? _gamePadCapabilities; + + /// The object returned by + private readonly JoystickCapabilities? _joystickCapabilities; + + /// For use in keybind boxes + public readonly string InputNamePrefix; + + /// Public check on whether mapped gamepad config is being used + public bool MappedGamePad => _gamePadCapabilities?.IsMapped == true; + + /// Gamepad Device state information - updated constantly + private GamePadState state; + + /// Joystick Device state information - updated constantly + private JoystickState jState; + + private OTK_GamePad(int index, int playerIndex) + { + _deviceIndex = index; + _playerIndex = playerIndex; + + if (Joystick.GetState(_deviceIndex).IsConnected) + { + Guid = Joystick.GetGuid(_deviceIndex); + _guidObtained = true; + _joystickCapabilities = Joystick.GetCapabilities(_deviceIndex); + } + else + { + Guid = Guid.NewGuid(); + _guidObtained = false; + } + + if (OpenTKGamePad.GetState(_deviceIndex).IsConnected) + { + _name = OpenTKGamePad.GetName(_deviceIndex); + _gamePadCapabilities = OpenTKGamePad.GetCapabilities(_deviceIndex); + } + else + { + _name = "OTK GamePad Undetermined Name"; + } + + InputNamePrefix = $"{(MappedGamePad ? "X" : "J")}{_playerIndex} "; + Update(); - InitializeCallbacks(); + + Console.WriteLine($"Initialising OpenTK GamePad: {Guid}"); + Console.WriteLine($"OpenTK Mapping: {_name}"); + + InitializeMappings(); } public void Update() { - state = Joystick.GetState(_stickIdx); + // update both here just in case + var tmpState = OpenTKGamePad.GetState(_deviceIndex); + DebugState(tmpState); + state = tmpState; + var tmpJstate = Joystick.GetState(_deviceIndex); + DebugState(tmpJstate); + jState = tmpJstate; + } + + [Conditional("DEBUG")] + private void DebugState(GamePadState tmpState) + { + if (!tmpState.Equals(state)) Debug.WriteLine($"GamePad State:\t{tmpState}"); + } + + [Conditional("DEBUG")] + private void DebugState(JoystickState tmpJstate) + { + if (!tmpJstate.Equals(jState)) Debug.WriteLine($"Joystick State:\t{tmpJstate}"); } public IEnumerable> GetFloats() { - for (int pi = 0; pi < 64; pi++) - yield return new Tuple(pi.ToString(), 10.0f * state.GetAxis(pi)); - } - - /// FOR DEBUGGING ONLY - public JoystickState GetInternalState() - { - return state; - } - - public string Name { get { return $"Joystick {_stickIdx}"; } } - public Guid Guid { get { return _guid; } } - - - public string ButtonName(int index) - { - return names[index]; - } - public bool Pressed(int index) - { - return actions[index](); - } - public int NumButtons { get; private set; } - - private readonly List names = new List(); - private readonly List> actions = new List>(); - - void AddItem(string _name, Func callback) - { - names.Add(_name); - actions.Add(callback); - NumButtons++; - } - - void InitializeCallbacks() - { - const int dzp = 400; - const int dzn = -400; - - names.Clear(); - actions.Clear(); - NumButtons = 0; - - AddItem("X+", () => state.GetAxis(0) >= dzp); - AddItem("X-", () => state.GetAxis(0) <= dzn); - AddItem("Y+", () => state.GetAxis(1) >= dzp); - AddItem("Y-", () => state.GetAxis(1) <= dzn); - AddItem("Z+", () => state.GetAxis(2) >= dzp); - AddItem("Z-", () => state.GetAxis(2) <= dzn); - - // Enjoy our delicious sliders. They're smaller than regular burgers but cost more. - - int jb = 1; - for (int i = 0; i < 64; i++) + if (MappedGamePad) { - AddItem($"B{jb}", () => state.GetButton(i)==ButtonState.Pressed); - jb++; + // automapping identified - use OpenTKGamePad + yield return new Tuple("LeftThumbX", ConstrainFloatInput(state.ThumbSticks.Left.X)); + yield return new Tuple("LeftThumbY", ConstrainFloatInput(state.ThumbSticks.Left.Y)); + yield return new Tuple("RightThumbX", ConstrainFloatInput(state.ThumbSticks.Right.X)); + yield return new Tuple("RightThumbY", ConstrainFloatInput(state.ThumbSticks.Right.Y)); + yield return new Tuple("LeftTrigger", ConstrainFloatInput(state.Triggers.Left)); + yield return new Tuple("RightTrigger", ConstrainFloatInput(state.Triggers.Right)); + yield break; } - - jb = 1; - foreach (JoystickHat enval in Enum.GetValues(typeof(JoystickHat))) + else { - AddItem($"POV{jb}U", () => state.GetHat(enval).IsUp); - AddItem($"POV{jb}D", () => state.GetHat(enval).IsDown); - AddItem($"POV{jb}L", () => state.GetHat(enval).IsLeft); - AddItem($"POV{jb}R", () => state.GetHat(enval).IsRight); - jb++; + // use Joystick + yield return new Tuple("X", ConstrainFloatInput(jState.GetAxis(0))); + yield return new Tuple("Y", ConstrainFloatInput(jState.GetAxis(1))); + yield return new Tuple("Z", ConstrainFloatInput(jState.GetAxis(2))); + yield return new Tuple("W", ConstrainFloatInput(jState.GetAxis(3))); + yield return new Tuple("V", ConstrainFloatInput(jState.GetAxis(4))); + yield return new Tuple("S", ConstrainFloatInput(jState.GetAxis(5))); + yield return new Tuple("Q", ConstrainFloatInput(jState.GetAxis(6))); + yield return new Tuple("P", ConstrainFloatInput(jState.GetAxis(7))); + yield return new Tuple("N", ConstrainFloatInput(jState.GetAxis(8))); + + for (var i = 9; i < 64; i++) + { + var j = i; + yield return new Tuple($"Axis{j.ToString()}", ConstrainFloatInput(jState.GetAxis(j))); + } + + yield break; } } - // Note that this does not appear to work at this time. I probably need to have more infos. - public void SetVibration(int left, int right) + public string Name => $"Joystick {_playerIndex} ({_name})"; + + /// Contains name and delegate function for all buttons, hats and axis + public readonly List buttonObjects = new List(); + + private void AddItem(string name, Func pressed) => + buttonObjects.Add(new ButtonObject + { + ButtonName = name, + ButtonAction = pressed + }); + + public struct ButtonObject { - //Not supported in OTK Joystick. It is supported for OTK Gamepad, but I have to use Joystick for reasons mentioned above. + public string ButtonName; + public Func ButtonAction; } + /// + /// Setup mappings prior to button initialization.
+ /// This is also here in case in the future we want users to be able to supply their own mappings for a device, perhaps via an input form. Possibly wishful thinking/overly complex. + ///
+ private void InitializeMappings() + { + if (_guidObtained) + { + // placeholder for if/when we figure out how to supply OpenTK with custom GamePadConfigurationDatabase entries + } + + // currently OpenTK has an internal database of mappings for the GamePad class: https://github.com/opentk/opentk/blob/master/src/OpenTK/Input/GamePadConfigurationDatabase.cs + if (MappedGamePad) + { + // internal map detected - use OpenTKGamePad + InitializeGamePadControls(); + } + else + { + // no internal map detected - use Joystick + InitializeJoystickControls(); + } + } + + private void InitializeJoystickControls() + { + // OpenTK's GetAxis returns float values (as opposed to the shorts of SlimDX) + const float ConversionFactor = 1.0f / short.MaxValue; + const float dzp = (short)4000 * ConversionFactor; + const float dzn = (short)-4000 * ConversionFactor; + //const float dzt = 0.6f; + + // axis + AddItem("X+", () => jState.GetAxis(0) >= dzp); + AddItem("X-", () => jState.GetAxis(0) <= dzn); + AddItem("Y+", () => jState.GetAxis(1) >= dzp); + AddItem("Y-", () => jState.GetAxis(1) <= dzn); + AddItem("Z+", () => jState.GetAxis(2) >= dzp); + AddItem("Z-", () => jState.GetAxis(2) <= dzn); + AddItem("W+", () => jState.GetAxis(3) >= dzp); + AddItem("W-", () => jState.GetAxis(3) <= dzn); + AddItem("V+", () => jState.GetAxis(4) >= dzp); + AddItem("V-", () => jState.GetAxis(4) <= dzn); + AddItem("S+", () => jState.GetAxis(5) >= dzp); + AddItem("S-", () => jState.GetAxis(5) <= dzn); + AddItem("Q+", () => jState.GetAxis(6) >= dzp); + AddItem("Q-", () => jState.GetAxis(6) <= dzn); + AddItem("P+", () => jState.GetAxis(7) >= dzp); + AddItem("P-", () => jState.GetAxis(7) <= dzn); + AddItem("N+", () => jState.GetAxis(8) >= dzp); + AddItem("N-", () => jState.GetAxis(8) <= dzn); + // should be enough axis, but just in case: + for (var i = 9; i < 64; i++) + { + var j = i; + AddItem($"Axis{j.ToString()}+", () => jState.GetAxis(j) >= dzp); + AddItem($"Axis{j.ToString()}-", () => jState.GetAxis(j) <= dzn); + } + + // buttons + for (var i = 0; i < (_joystickCapabilities?.ButtonCount ?? 0); i++) + { + var j = i; + AddItem($"B{i + 1}", () => jState.GetButton(j) == ButtonState.Pressed); + } + + // hats + AddItem("POV1U", () => jState.GetHat(JoystickHat.Hat0).IsUp); + AddItem("POV1D", () => jState.GetHat(JoystickHat.Hat0).IsDown); + AddItem("POV1L", () => jState.GetHat(JoystickHat.Hat0).IsLeft); + AddItem("POV1R", () => jState.GetHat(JoystickHat.Hat0).IsRight); + AddItem("POV2U", () => jState.GetHat(JoystickHat.Hat1).IsUp); + AddItem("POV2D", () => jState.GetHat(JoystickHat.Hat1).IsDown); + AddItem("POV2L", () => jState.GetHat(JoystickHat.Hat1).IsLeft); + AddItem("POV2R", () => jState.GetHat(JoystickHat.Hat1).IsRight); + AddItem("POV3U", () => jState.GetHat(JoystickHat.Hat2).IsUp); + AddItem("POV3D", () => jState.GetHat(JoystickHat.Hat2).IsDown); + AddItem("POV3L", () => jState.GetHat(JoystickHat.Hat2).IsLeft); + AddItem("POV3R", () => jState.GetHat(JoystickHat.Hat2).IsRight); + AddItem("POV4U", () => jState.GetHat(JoystickHat.Hat3).IsUp); + AddItem("POV4D", () => jState.GetHat(JoystickHat.Hat3).IsDown); + AddItem("POV4L", () => jState.GetHat(JoystickHat.Hat3).IsLeft); + AddItem("POV4R", () => jState.GetHat(JoystickHat.Hat3).IsRight); + } + + private void InitializeGamePadControls() + { + // OpenTK's ThumbSticks contain float values (as opposed to the shorts of SlimDX) + const float ConversionFactor = 1.0f / short.MaxValue; + const float dzp = (short)4000 * ConversionFactor; + const float dzn = (short)-4000 * ConversionFactor; + const float dzt = 0.6f; + + // buttons + AddItem("A", () => state.Buttons.A == ButtonState.Pressed); + AddItem("B", () => state.Buttons.B == ButtonState.Pressed); + AddItem("X", () => state.Buttons.X == ButtonState.Pressed); + AddItem("Y", () => state.Buttons.Y == ButtonState.Pressed); + AddItem("Guide", () => state.Buttons.BigButton == ButtonState.Pressed); + AddItem("Start", () => state.Buttons.Start == ButtonState.Pressed); + AddItem("Back", () => state.Buttons.Back == ButtonState.Pressed); + AddItem("LeftThumb", () => state.Buttons.LeftStick == ButtonState.Pressed); + AddItem("RightThumb", () => state.Buttons.RightStick == ButtonState.Pressed); + AddItem("LeftShoulder", () => state.Buttons.LeftShoulder == ButtonState.Pressed); + AddItem("RightShoulder", () => state.Buttons.RightShoulder == ButtonState.Pressed); + + // dpad + AddItem("DpadUp", () => state.DPad.Up == ButtonState.Pressed); + AddItem("DpadDown", () => state.DPad.Down == ButtonState.Pressed); + AddItem("DpadLeft", () => state.DPad.Left == ButtonState.Pressed); + AddItem("DpadRight", () => state.DPad.Right == ButtonState.Pressed); + + // sticks + AddItem("LStickUp", () => state.ThumbSticks.Left.Y >= dzp); + AddItem("LStickDown", () => state.ThumbSticks.Left.Y <= dzn); + AddItem("LStickLeft", () => state.ThumbSticks.Left.X <= dzn); + AddItem("LStickRight", () => state.ThumbSticks.Left.X >= dzp); + AddItem("RStickUp", () => state.ThumbSticks.Right.Y >= dzp); + AddItem("RStickDown", () => state.ThumbSticks.Right.Y <= dzn); + AddItem("RStickLeft", () => state.ThumbSticks.Right.X <= dzn); + AddItem("RStickRight", () => state.ThumbSticks.Right.X >= dzp); + + // triggers + AddItem("LeftTrigger", () => state.Triggers.Left > dzt); + AddItem("RightTrigger", () => state.Triggers.Right > dzt); + } + + /// + /// Sets the gamepad's left and right vibration + /// We don't currently use this in Bizhawk - do we have any cores that support this? + /// + /// + /// + public void SetVibration(float left, float right) => OpenTKGamePad.SetVibration( + _deviceIndex, + _gamePadCapabilities?.HasLeftVibrationMotor == true ? left : 0, + _gamePadCapabilities?.HasRightVibrationMotor == true ? right : 0 + ); + + #endregion } } diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index f589bfa790..80088fb826 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -68,10 +68,6 @@ ..\References\Newtonsoft.Json.dll - - False - ..\References\OpenTK.dll - ..\References\PeNet.dll diff --git a/Bizware/BizHawk.Bizware.BizwareGL.GdiPlus/BizHawk.Bizware.BizwareGL.GdiPlus.csproj b/Bizware/BizHawk.Bizware.BizwareGL.GdiPlus/BizHawk.Bizware.BizwareGL.GdiPlus.csproj index d672923fe1..e8c8f7bf56 100644 --- a/Bizware/BizHawk.Bizware.BizwareGL.GdiPlus/BizHawk.Bizware.BizwareGL.GdiPlus.csproj +++ b/Bizware/BizHawk.Bizware.BizwareGL.GdiPlus/BizHawk.Bizware.BizwareGL.GdiPlus.csproj @@ -37,7 +37,7 @@ MinimumRecommendedRules.ruleset - + False ..\..\References\OpenTK.dll diff --git a/Bizware/BizHawk.Bizware.BizwareGL.OpenTK/BizHawk.Bizware.BizwareGL.OpenTK.csproj b/Bizware/BizHawk.Bizware.BizwareGL.OpenTK/BizHawk.Bizware.BizwareGL.OpenTK.csproj index 2f55beb615..6fc816f4c6 100644 --- a/Bizware/BizHawk.Bizware.BizwareGL.OpenTK/BizHawk.Bizware.BizwareGL.OpenTK.csproj +++ b/Bizware/BizHawk.Bizware.BizwareGL.OpenTK/BizHawk.Bizware.BizwareGL.OpenTK.csproj @@ -41,11 +41,11 @@ false - + False ..\..\References\OpenTK.dll - + False ..\..\References\OpenTK.GLControl.dll diff --git a/Bizware/BizHawk.Bizware.BizwareGL.SlimDX/BizHawk.Bizware.BizwareGL.SlimDX.csproj b/Bizware/BizHawk.Bizware.BizwareGL.SlimDX/BizHawk.Bizware.BizwareGL.SlimDX.csproj index cf7d477acb..0e850c9838 100644 --- a/Bizware/BizHawk.Bizware.BizwareGL.SlimDX/BizHawk.Bizware.BizwareGL.SlimDX.csproj +++ b/Bizware/BizHawk.Bizware.BizwareGL.SlimDX/BizHawk.Bizware.BizwareGL.SlimDX.csproj @@ -37,7 +37,7 @@ MinimumRecommendedRules.ruleset - + False ..\..\References\OpenTK.dll diff --git a/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj b/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj index 91d0f9902d..f39ab75b16 100644 --- a/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj +++ b/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj @@ -41,7 +41,7 @@ false - + False ..\..\References\OpenTK.dll