From ad7d22777f60f58f96d4f5bca879134b6924390a Mon Sep 17 00:00:00 2001 From: Jose Padilla Date: Sun, 21 Feb 2021 00:22:55 +0100 Subject: [PATCH] Controller Input handling refactoring (#1751) * This should fix issue #1374 in Linux Changes: - Bind buttons by detecting the transition from down to up. - Bind axis by detecting movement from value higher than 50% to a value lower than 50%. Caveats: - I have tested only with DS3 in Linux (Fedora 32). - ZL and ZR detection works by accident. This code doesn't take negative axis into account. The reason it works is because axis are managed in absolute value. So when pressing ZL/ZR axis value goes from -1 to 1 (or 1 to 0 and back to 1) and this hits the axis detector. - Likely I have broken all the other controllers xD (testing needed). * Assign keyboardPressed * Make a more robust detection of pressed buttons when using a controller * Add interface to bind buttons from Joystick and Keyboard * Fix style issues after code review by @AcK77 (Thanks!) * Move new classes to Ryujinx.Ui.Input namespace * Use explicit types instead of var * Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs Co-authored-by: Mary * Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs Co-authored-by: Mary * Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs Co-authored-by: Mary * Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs Co-authored-by: Ac_K * Add a new empty line before * Up Co-authored-by: Jose Padilla Co-authored-by: Mary Co-authored-by: Ac_K --- Ryujinx/Ui/Input/ButtonAssigner.cs | 17 ++ Ryujinx/Ui/Input/JoystickButtonAssigner.cs | 227 +++++++++++++++++++++ Ryujinx/Ui/Input/KeyboardKeyAssigner.cs | 51 +++++ Ryujinx/Ui/Windows/ControllerWindow.cs | 165 +++++---------- 4 files changed, 344 insertions(+), 116 deletions(-) create mode 100644 Ryujinx/Ui/Input/ButtonAssigner.cs create mode 100644 Ryujinx/Ui/Input/JoystickButtonAssigner.cs create mode 100644 Ryujinx/Ui/Input/KeyboardKeyAssigner.cs diff --git a/Ryujinx/Ui/Input/ButtonAssigner.cs b/Ryujinx/Ui/Input/ButtonAssigner.cs new file mode 100644 index 0000000000..ff32b10687 --- /dev/null +++ b/Ryujinx/Ui/Input/ButtonAssigner.cs @@ -0,0 +1,17 @@ +using Ryujinx.Common.Configuration.Hid; + +namespace Ryujinx.Ui.Input +{ + interface ButtonAssigner + { + void Init(); + + void ReadInput(); + + bool HasAnyButtonPressed(); + + bool ShouldCancel(); + + string GetPressedButton(); + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Input/JoystickButtonAssigner.cs b/Ryujinx/Ui/Input/JoystickButtonAssigner.cs new file mode 100644 index 0000000000..481221ac12 --- /dev/null +++ b/Ryujinx/Ui/Input/JoystickButtonAssigner.cs @@ -0,0 +1,227 @@ +using OpenTK.Input; +using Ryujinx.Common.Configuration.Hid; +using System.Collections.Generic; +using System; +using System.IO; + +namespace Ryujinx.Ui.Input +{ + class JoystickButtonAssigner : ButtonAssigner + { + private int _index; + + private double _triggerThreshold; + + private JoystickState _currState; + + private JoystickState _prevState; + + private JoystickButtonDetector _detector; + + public JoystickButtonAssigner(int index, double triggerThreshold) + { + _index = index; + _triggerThreshold = triggerThreshold; + _detector = new JoystickButtonDetector(); + } + + public void Init() + { + _currState = Joystick.GetState(_index); + _prevState = _currState; + } + + public void ReadInput() + { + _prevState = _currState; + _currState = Joystick.GetState(_index); + + CollectButtonStats(); + } + + public bool HasAnyButtonPressed() + { + return _detector.HasAnyButtonPressed(); + } + + public bool ShouldCancel() + { + return Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsAnyKeyDown; + } + + public string GetPressedButton() + { + List pressedButtons = _detector.GetPressedButtons(); + + // Reverse list so axis button take precedence when more than one button is recognized. + pressedButtons.Reverse(); + + return pressedButtons.Count > 0 ? pressedButtons[0].ToString() : ""; + } + + private void CollectButtonStats() + { + JoystickCapabilities capabilities = Joystick.GetCapabilities(_index); + + ControllerInputId pressedButton; + + // Buttons + for (int i = 0; i != capabilities.ButtonCount; i++) + { + if (_currState.IsButtonDown(i) && _prevState.IsButtonUp(i)) + { + Enum.TryParse($"Button{i}", out pressedButton); + _detector.AddInput(pressedButton, 1); + } + + if (_currState.IsButtonUp(i) && _prevState.IsButtonDown(i)) + { + Enum.TryParse($"Button{i}", out pressedButton); + _detector.AddInput(pressedButton, -1); + } + } + + // Axis + for (int i = 0; i != capabilities.AxisCount; i++) + { + float axisValue = _currState.GetAxis(i); + + Enum.TryParse($"Axis{i}", out pressedButton); + _detector.AddInput(pressedButton, axisValue); + } + + // Hats + for (int i = 0; i != capabilities.HatCount; i++) + { + string currPos = GetHatPosition(_currState.GetHat((JoystickHat)i)); + string prevPos = GetHatPosition(_prevState.GetHat((JoystickHat)i)); + + if (currPos == prevPos) + { + continue; + } + + if (currPos != "") + { + Enum.TryParse($"Hat{i}{currPos}", out pressedButton); + _detector.AddInput(pressedButton, 1); + } + + if (prevPos != "") + { + Enum.TryParse($"Hat{i}{prevPos}", out pressedButton); + _detector.AddInput(pressedButton, -1); + } + } + } + + private string GetHatPosition(JoystickHatState hatState) + { + if (hatState.IsUp) return "Up"; + if (hatState.IsDown) return "Down"; + if (hatState.IsLeft) return "Left"; + if (hatState.IsRight) return "Right"; + return ""; + } + + private class JoystickButtonDetector + { + private Dictionary _stats; + + public JoystickButtonDetector() + { + _stats = new Dictionary(); + } + + public bool HasAnyButtonPressed() + { + foreach (var inputSummary in _stats.Values) + { + if (checkButtonPressed(inputSummary)) + { + return true; + } + } + + return false; + } + + public List GetPressedButtons() + { + List pressedButtons = new List(); + + foreach (var kvp in _stats) + { + if (!checkButtonPressed(kvp.Value)) + { + continue; + } + pressedButtons.Add(kvp.Key); + } + + return pressedButtons; + } + + public void AddInput(ControllerInputId button, float value) + { + InputSummary inputSummary; + + if (!_stats.TryGetValue(button, out inputSummary)) + { + inputSummary = new InputSummary(); + _stats.Add(button, inputSummary); + } + + inputSummary.AddInput(value); + } + + public override string ToString() + { + TextWriter writer = new StringWriter(); + + foreach (var kvp in _stats) + { + writer.WriteLine($"Button {kvp.Key} -> {kvp.Value}"); + } + + return writer.ToString(); + } + + private bool checkButtonPressed(InputSummary sequence) + { + float distance = Math.Abs(sequence.Min - sequence.Avg) + Math.Abs(sequence.Max - sequence.Avg); + return distance > 1.5; // distance range [0, 2] + } + } + + private class InputSummary + { + public float Min, Max, Sum, Avg; + + public int NumSamples; + + public InputSummary() + { + Min = float.MaxValue; + Max = float.MinValue; + Sum = 0; + NumSamples = 0; + Avg = 0; + } + + public void AddInput(float value) + { + Min = Math.Min(Min, value); + Max = Math.Max(Max, value); + Sum += value; + NumSamples += 1; + Avg = Sum / NumSamples; + } + + public override string ToString() + { + return $"Avg: {Avg} Min: {Min} Max: {Max} Sum: {Sum} NumSamples: {NumSamples}"; + } + } + } +} diff --git a/Ryujinx/Ui/Input/KeyboardKeyAssigner.cs b/Ryujinx/Ui/Input/KeyboardKeyAssigner.cs new file mode 100644 index 0000000000..2a29c36696 --- /dev/null +++ b/Ryujinx/Ui/Input/KeyboardKeyAssigner.cs @@ -0,0 +1,51 @@ +using OpenTK.Input; +using System; +using Key = Ryujinx.Configuration.Hid.Key; + +namespace Ryujinx.Ui.Input +{ + class KeyboardKeyAssigner : ButtonAssigner + { + private int _index; + + private KeyboardState _keyboardState; + + public KeyboardKeyAssigner(int index) + { + _index = index; + } + + public void Init() { } + + public void ReadInput() + { + _keyboardState = KeyboardController.GetKeyboardState(_index); + } + + public bool HasAnyButtonPressed() + { + return _keyboardState.IsAnyKeyDown; + } + + public bool ShouldCancel() + { + return Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsKeyDown(OpenTK.Input.Key.Escape); + } + + public string GetPressedButton() + { + string keyPressed = ""; + + foreach (Key key in Enum.GetValues(typeof(Key))) + { + if (_keyboardState.IsKeyDown((OpenTK.Input.Key)key)) + { + keyPressed = key.ToString(); + break; + } + } + + return !ShouldCancel() ? keyPressed : ""; + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs index e06c6c6be3..67c02a0b12 100644 --- a/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -4,6 +4,7 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Utilities; using Ryujinx.Configuration; +using Ryujinx.Ui.Input; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; @@ -584,73 +585,6 @@ namespace Ryujinx.Ui.Windows return null; } - private static bool IsAnyKeyPressed(out Key pressedKey, int index) - { - KeyboardState keyboardState = KeyboardController.GetKeyboardState(index); - - foreach (Key key in Enum.GetValues(typeof(Key))) - { - if (keyboardState.IsKeyDown((OpenTK.Input.Key)key)) - { - pressedKey = key; - - return true; - } - } - - pressedKey = Key.Unbound; - - return false; - } - - private static bool IsAnyButtonPressed(out ControllerInputId pressedButton, int index, double triggerThreshold) - { - JoystickState joystickState = Joystick.GetState(index); - JoystickCapabilities joystickCapabilities = Joystick.GetCapabilities(index); - - //Buttons - for (int i = 0; i != joystickCapabilities.ButtonCount; i++) - { - if (joystickState.IsButtonDown(i)) - { - Enum.TryParse($"Button{i}", out pressedButton); - return true; - } - } - - //Axis - for (int i = 0; i != joystickCapabilities.AxisCount; i++) - { - if (joystickState.GetAxis(i) > 0.5f && joystickState.GetAxis(i) > triggerThreshold) - { - Enum.TryParse($"Axis{i}", out pressedButton); - - return true; - } - } - - //Hats - for (int i = 0; i != joystickCapabilities.HatCount; i++) - { - JoystickHatState hatState = joystickState.GetHat((JoystickHat)i); - string pos = null; - - if (hatState.IsUp) pos = "Up"; - if (hatState.IsDown) pos = "Down"; - if (hatState.IsLeft) pos = "Left"; - if (hatState.IsRight) pos = "Right"; - if (pos == null) continue; - - Enum.TryParse($"Hat{i}{pos}", out pressedButton); - - return true; - } - - pressedButton = ControllerInputId.Unbound; - - return false; - } - private string GetProfileBasePath() { string path = AppDataManager.ProfilesDirPath; @@ -690,6 +624,31 @@ namespace Ryujinx.Ui.Windows _refreshInputDevicesButton.SetStateFlags(StateFlags.Normal, true); } + private ButtonAssigner CreateButtonAssigner() + { + int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); + + ButtonAssigner assigner; + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + assigner = new KeyboardKeyAssigner(index); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + // TODO: triggerThresold is passed but not used by JoystickButtonAssigner. Should it be used for key binding?. + // Note that, like left and right sticks, ZL and ZR triggers are treated as axis. + // The problem is then how to decide which axis should use triggerThresold. + assigner = new JoystickButtonAssigner(index, _controllerTriggerThreshold.Value); + } + else + { + throw new Exception("Controller not supported"); + } + + return assigner; + } + private void Button_Pressed(object sender, EventArgs args) { if (_isWaitingForInput) @@ -697,67 +656,41 @@ namespace Ryujinx.Ui.Windows return; } + ButtonAssigner assigner = CreateButtonAssigner(); + _isWaitingForInput = true; Thread inputThread = new Thread(() => { - Button button = (ToggleButton)sender; + assigner.Init(); - if (_inputDevice.ActiveId.StartsWith("keyboard")) + while (true) { - Key pressedKey; + Thread.Sleep(10); + assigner.ReadInput(); - int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); - while (!IsAnyKeyPressed(out pressedKey, index)) + if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel()) { - if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsKeyDown(OpenTK.Input.Key.Escape)) - { - Application.Invoke(delegate - { - button.SetStateFlags(StateFlags.Normal, true); - }); - - _isWaitingForInput = false; - - return; - } + break; } - - Application.Invoke(delegate - { - button.Label = pressedKey.ToString(); - button.SetStateFlags(StateFlags.Normal, true); - }); - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - ControllerInputId pressedButton; - - int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); - while (!IsAnyButtonPressed(out pressedButton, index, _controllerTriggerThreshold.Value)) - { - if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsAnyKeyDown) - { - Application.Invoke(delegate - { - button.SetStateFlags(StateFlags.Normal, true); - }); - - _isWaitingForInput = false; - - return; - } - } - - Application.Invoke(delegate - { - button.Label = pressedButton.ToString(); - button.SetStateFlags(StateFlags.Normal, true); - }); } - _isWaitingForInput = false; + string pressedButton = assigner.GetPressedButton(); + + ToggleButton button = (ToggleButton) sender; + + Application.Invoke(delegate + { + if (pressedButton != "") + { + button.Label = pressedButton; + } + + button.Active = false; + _isWaitingForInput = false; + }); }); + inputThread.Name = "GUI.InputThread"; inputThread.IsBackground = true; inputThread.Start();