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 <thog@protonmail.com>

* Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs

Co-authored-by: Mary <thog@protonmail.com>

* Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs

Co-authored-by: Mary <thog@protonmail.com>

* Update Ryujinx/Ui/Input/JoystickButtonAssigner.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Add a new empty line before

* Up

Co-authored-by: Jose Padilla <jose@prensalink.com>
Co-authored-by: Mary <thog@protonmail.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
This commit is contained in:
Jose Padilla 2021-02-21 00:22:55 +01:00 committed by GitHub
parent d5081e3f93
commit ad7d22777f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 344 additions and 116 deletions

View file

@ -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();
}
}

View file

@ -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<ControllerInputId> 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<ControllerInputId, InputSummary> _stats;
public JoystickButtonDetector()
{
_stats = new Dictionary<ControllerInputId, InputSummary>();
}
public bool HasAnyButtonPressed()
{
foreach (var inputSummary in _stats.Values)
{
if (checkButtonPressed(inputSummary))
{
return true;
}
}
return false;
}
public List<ControllerInputId> GetPressedButtons()
{
List<ControllerInputId> pressedButtons = new List<ControllerInputId>();
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}";
}
}
}
}

View file

@ -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 : "";
}
}
}

View file

@ -4,6 +4,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.Ui.Input;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -584,73 +585,6 @@ namespace Ryujinx.Ui.Windows
return null; 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() private string GetProfileBasePath()
{ {
string path = AppDataManager.ProfilesDirPath; string path = AppDataManager.ProfilesDirPath;
@ -690,6 +624,31 @@ namespace Ryujinx.Ui.Windows
_refreshInputDevicesButton.SetStateFlags(StateFlags.Normal, true); _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) private void Button_Pressed(object sender, EventArgs args)
{ {
if (_isWaitingForInput) if (_isWaitingForInput)
@ -697,67 +656,41 @@ namespace Ryujinx.Ui.Windows
return; return;
} }
ButtonAssigner assigner = CreateButtonAssigner();
_isWaitingForInput = true; _isWaitingForInput = true;
Thread inputThread = new Thread(() => 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]); if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel())
while (!IsAnyKeyPressed(out pressedKey, index))
{ {
if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsKeyDown(OpenTK.Input.Key.Escape)) break;
{
Application.Invoke(delegate
{
button.SetStateFlags(StateFlags.Normal, true);
});
_isWaitingForInput = false;
return;
}
} }
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.Name = "GUI.InputThread";
inputThread.IsBackground = true; inputThread.IsBackground = true;
inputThread.Start(); inputThread.Start();