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:
parent
d5081e3f93
commit
ad7d22777f
4 changed files with 344 additions and 116 deletions
17
Ryujinx/Ui/Input/ButtonAssigner.cs
Normal file
17
Ryujinx/Ui/Input/ButtonAssigner.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
227
Ryujinx/Ui/Input/JoystickButtonAssigner.cs
Normal file
227
Ryujinx/Ui/Input/JoystickButtonAssigner.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
Ryujinx/Ui/Input/KeyboardKeyAssigner.cs
Normal file
51
Ryujinx/Ui/Input/KeyboardKeyAssigner.cs
Normal 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 : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string pressedButton = assigner.GetPressedButton();
|
||||||
|
|
||||||
|
ToggleButton button = (ToggleButton) sender;
|
||||||
|
|
||||||
Application.Invoke(delegate
|
Application.Invoke(delegate
|
||||||
{
|
{
|
||||||
button.Label = pressedKey.ToString();
|
if (pressedButton != "")
|
||||||
button.SetStateFlags(StateFlags.Normal, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (_inputDevice.ActiveId.StartsWith("controller"))
|
|
||||||
{
|
{
|
||||||
ControllerInputId pressedButton;
|
button.Label = 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.Active = false;
|
||||||
_isWaitingForInput = false;
|
_isWaitingForInput = false;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
inputThread.Name = "GUI.InputThread";
|
inputThread.Name = "GUI.InputThread";
|
||||||
inputThread.IsBackground = true;
|
inputThread.IsBackground = true;
|
||||||
inputThread.Start();
|
inputThread.Start();
|
||||||
|
|
Reference in a new issue