mirror of
https://github.com/Ryujinx/Ryujinx.git
synced 2025-06-28 12:40:47 -07:00
Move solution and projects to src
This commit is contained in:
198
src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs
Normal file
198
src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs
Normal file
@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Input.Assigner
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IButtonAssigner"/> implementation for regular <see cref="IGamepad"/>.
|
||||
/// </summary>
|
||||
public class GamepadButtonAssigner : IButtonAssigner
|
||||
{
|
||||
private IGamepad _gamepad;
|
||||
|
||||
private GamepadStateSnapshot _currState;
|
||||
|
||||
private GamepadStateSnapshot _prevState;
|
||||
|
||||
private JoystickButtonDetector _detector;
|
||||
|
||||
private bool _forStick;
|
||||
|
||||
public GamepadButtonAssigner(IGamepad gamepad, float triggerThreshold, bool forStick)
|
||||
{
|
||||
_gamepad = gamepad;
|
||||
_detector = new JoystickButtonDetector();
|
||||
_forStick = forStick;
|
||||
|
||||
_gamepad?.SetTriggerThreshold(triggerThreshold);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (_gamepad != null)
|
||||
{
|
||||
_currState = _gamepad.GetStateSnapshot();
|
||||
_prevState = _currState;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadInput()
|
||||
{
|
||||
if (_gamepad != null)
|
||||
{
|
||||
_prevState = _currState;
|
||||
_currState = _gamepad.GetStateSnapshot();
|
||||
}
|
||||
|
||||
CollectButtonStats();
|
||||
}
|
||||
|
||||
public bool HasAnyButtonPressed()
|
||||
{
|
||||
return _detector.HasAnyButtonPressed();
|
||||
}
|
||||
|
||||
public bool ShouldCancel()
|
||||
{
|
||||
return _gamepad == null || !_gamepad.IsConnected;
|
||||
}
|
||||
|
||||
public string GetPressedButton()
|
||||
{
|
||||
IEnumerable<GamepadButtonInputId> pressedButtons = _detector.GetPressedButtons();
|
||||
|
||||
if (pressedButtons.Any())
|
||||
{
|
||||
return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private void CollectButtonStats()
|
||||
{
|
||||
if (_forStick)
|
||||
{
|
||||
for (StickInputId inputId = StickInputId.Left; inputId < StickInputId.Count; inputId++)
|
||||
{
|
||||
(float x, float y) = _currState.GetStick(inputId);
|
||||
|
||||
float value;
|
||||
|
||||
if (x != 0.0f)
|
||||
{
|
||||
value = x;
|
||||
}
|
||||
else if (y != 0.0f)
|
||||
{
|
||||
value = y;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_detector.AddInput((GamepadButtonInputId)inputId, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
|
||||
{
|
||||
if (_currState.IsPressed(inputId) && !_prevState.IsPressed(inputId))
|
||||
{
|
||||
_detector.AddInput(inputId, 1);
|
||||
}
|
||||
|
||||
if (!_currState.IsPressed(inputId) && _prevState.IsPressed(inputId))
|
||||
{
|
||||
_detector.AddInput(inputId, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class JoystickButtonDetector
|
||||
{
|
||||
private Dictionary<GamepadButtonInputId, InputSummary> _stats;
|
||||
|
||||
public JoystickButtonDetector()
|
||||
{
|
||||
_stats = new Dictionary<GamepadButtonInputId, InputSummary>();
|
||||
}
|
||||
|
||||
public bool HasAnyButtonPressed()
|
||||
{
|
||||
return _stats.Values.Any(CheckButtonPressed);
|
||||
}
|
||||
|
||||
public IEnumerable<GamepadButtonInputId> GetPressedButtons()
|
||||
{
|
||||
return _stats.Where(kvp => CheckButtonPressed(kvp.Value)).Select(kvp => kvp.Key);
|
||||
}
|
||||
|
||||
public void AddInput(GamepadButtonInputId 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()
|
||||
{
|
||||
StringWriter 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/Ryujinx.Input/Assigner/IButtonAssigner.cs
Normal file
36
src/Ryujinx.Input/Assigner/IButtonAssigner.cs
Normal file
@ -0,0 +1,36 @@
|
||||
namespace Ryujinx.Input.Assigner
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface that allows to gather the driver input info to assign to a button on the UI.
|
||||
/// </summary>
|
||||
public interface IButtonAssigner
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize the button assigner.
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Read input.
|
||||
/// </summary>
|
||||
void ReadInput();
|
||||
|
||||
/// <summary>
|
||||
/// Check if a button was pressed.
|
||||
/// </summary>
|
||||
/// <returns>True if a button was pressed</returns>
|
||||
bool HasAnyButtonPressed();
|
||||
|
||||
/// <summary>
|
||||
/// Indicate if the user of this API should cancel operations. This is triggered for example when a gamepad get disconnected or when a user cancel assignation operations.
|
||||
/// </summary>
|
||||
/// <returns>True if the user of this API should cancel operations</returns>
|
||||
bool ShouldCancel();
|
||||
|
||||
/// <summary>
|
||||
/// Get the pressed button that was read in <see cref="ReadInput"/> by the button assigner.
|
||||
/// </summary>
|
||||
/// <returns>The pressed button that was read</returns>
|
||||
string GetPressedButton();
|
||||
}
|
||||
}
|
50
src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs
Normal file
50
src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs
Normal file
@ -0,0 +1,50 @@
|
||||
namespace Ryujinx.Input.Assigner
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IButtonAssigner"/> implementation for <see cref="IKeyboard"/>.
|
||||
/// </summary>
|
||||
public class KeyboardKeyAssigner : IButtonAssigner
|
||||
{
|
||||
private IKeyboard _keyboard;
|
||||
|
||||
private KeyboardStateSnapshot _keyboardState;
|
||||
|
||||
public KeyboardKeyAssigner(IKeyboard keyboard)
|
||||
{
|
||||
_keyboard = keyboard;
|
||||
}
|
||||
|
||||
public void Initialize() { }
|
||||
|
||||
public void ReadInput()
|
||||
{
|
||||
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
|
||||
}
|
||||
|
||||
public bool HasAnyButtonPressed()
|
||||
{
|
||||
return GetPressedButton().Length != 0;
|
||||
}
|
||||
|
||||
public bool ShouldCancel()
|
||||
{
|
||||
return _keyboardState.IsPressed(Key.Escape);
|
||||
}
|
||||
|
||||
public string GetPressedButton()
|
||||
{
|
||||
string keyPressed = "";
|
||||
|
||||
for (Key key = Key.Unknown; key < Key.Count; key++)
|
||||
{
|
||||
if (_keyboardState.IsPressed(key))
|
||||
{
|
||||
keyPressed = key.ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return !ShouldCancel() ? keyPressed : "";
|
||||
}
|
||||
}
|
||||
}
|
57
src/Ryujinx.Input/GamepadButtonInputId.cs
Normal file
57
src/Ryujinx.Input/GamepadButtonInputId.cs
Normal file
@ -0,0 +1,57 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a button from a gamepad.
|
||||
/// </summary>
|
||||
public enum GamepadButtonInputId : byte
|
||||
{
|
||||
Unbound,
|
||||
A,
|
||||
B,
|
||||
X,
|
||||
Y,
|
||||
LeftStick,
|
||||
RightStick,
|
||||
LeftShoulder,
|
||||
RightShoulder,
|
||||
|
||||
// Likely axis
|
||||
LeftTrigger,
|
||||
// Likely axis
|
||||
RightTrigger,
|
||||
|
||||
DpadUp,
|
||||
DpadDown,
|
||||
DpadLeft,
|
||||
DpadRight,
|
||||
|
||||
// Special buttons
|
||||
|
||||
Minus,
|
||||
Plus,
|
||||
|
||||
Back = Minus,
|
||||
Start = Plus,
|
||||
|
||||
Guide,
|
||||
Misc1,
|
||||
|
||||
// Xbox Elite paddle
|
||||
Paddle1,
|
||||
Paddle2,
|
||||
Paddle3,
|
||||
Paddle4,
|
||||
|
||||
// PS5 touchpad button
|
||||
Touchpad,
|
||||
|
||||
// Virtual buttons for single joycon
|
||||
SingleLeftTrigger0,
|
||||
SingleRightTrigger0,
|
||||
|
||||
SingleLeftTrigger1,
|
||||
SingleRightTrigger1,
|
||||
|
||||
Count
|
||||
}
|
||||
}
|
28
src/Ryujinx.Input/GamepadFeaturesFlag.cs
Normal file
28
src/Ryujinx.Input/GamepadFeaturesFlag.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent features supported by a <see cref="IGamepad"/>.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum GamepadFeaturesFlag
|
||||
{
|
||||
/// <summary>
|
||||
/// No features are supported
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Rumble
|
||||
/// </summary>
|
||||
/// <remarks>Also named haptic</remarks>
|
||||
Rumble,
|
||||
|
||||
/// <summary>
|
||||
/// Motion
|
||||
/// <remarks>Also named sixaxis</remarks>
|
||||
/// </summary>
|
||||
Motion
|
||||
}
|
||||
}
|
70
src/Ryujinx.Input/GamepadStateSnapshot.cs
Normal file
70
src/Ryujinx.Input/GamepadStateSnapshot.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A snapshot of a <see cref="IGamepad"/>.
|
||||
/// </summary>
|
||||
public struct GamepadStateSnapshot
|
||||
{
|
||||
// NOTE: Update Array size if JoystickInputId is changed.
|
||||
private Array3<Array2<float>> _joysticksState;
|
||||
// NOTE: Update Array size if GamepadInputId is changed.
|
||||
private Array28<bool> _buttonsState;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="GamepadStateSnapshot"/>.
|
||||
/// </summary>
|
||||
/// <param name="joysticksState">The joysticks state</param>
|
||||
/// <param name="buttonsState">The buttons state</param>
|
||||
public GamepadStateSnapshot(Array3<Array2<float>> joysticksState, Array28<bool> buttonsState)
|
||||
{
|
||||
_joysticksState = joysticksState;
|
||||
_buttonsState = buttonsState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given input button is pressed.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The button id</param>
|
||||
/// <returns>True if the given button is pressed</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsPressed(GamepadButtonInputId inputId) => _buttonsState[(int)inputId];
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Set the state of a given button.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The button id</param>
|
||||
/// <param name="value">The state to assign for the given button.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetPressed(GamepadButtonInputId inputId, bool value) => _buttonsState[(int)inputId] = value;
|
||||
|
||||
/// <summary>
|
||||
/// Get the values of a given input joystick.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The stick id</param>
|
||||
/// <returns>The values of the given input joystick</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
var result = _joysticksState[(int)inputId];
|
||||
|
||||
return (result[0], result[1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the values of a given input joystick.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The stick id</param>
|
||||
/// <param name="x">The x axis value</param>
|
||||
/// <param name="y">The y axis value</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetStick(StickInputId inputId, float x, float y)
|
||||
{
|
||||
_joysticksState[(int)inputId][0] = x;
|
||||
_joysticksState[(int)inputId][1] = y;
|
||||
}
|
||||
}
|
||||
}
|
54
src/Ryujinx.Input/HLE/InputManager.cs
Normal file
54
src/Ryujinx.Input/HLE/InputManager.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Input.HLE
|
||||
{
|
||||
public class InputManager : IDisposable
|
||||
{
|
||||
public IGamepadDriver KeyboardDriver { get; private set; }
|
||||
public IGamepadDriver GamepadDriver { get; private set; }
|
||||
public IGamepadDriver MouseDriver { get; private set; }
|
||||
|
||||
public InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver)
|
||||
{
|
||||
KeyboardDriver = keyboardDriver;
|
||||
GamepadDriver = gamepadDriver;
|
||||
}
|
||||
|
||||
public void SetMouseDriver(IGamepadDriver mouseDriver)
|
||||
{
|
||||
MouseDriver?.Dispose();
|
||||
|
||||
MouseDriver = mouseDriver;
|
||||
}
|
||||
|
||||
public NpadManager CreateNpadManager()
|
||||
{
|
||||
return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver);
|
||||
}
|
||||
|
||||
public TouchScreenManager CreateTouchScreenManager()
|
||||
{
|
||||
if (MouseDriver == null)
|
||||
{
|
||||
throw new InvalidOperationException("Mouse Driver has not been initialized.");
|
||||
}
|
||||
|
||||
return new TouchScreenManager(MouseDriver.GetGamepad("0") as IMouse);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
KeyboardDriver?.Dispose();
|
||||
GamepadDriver?.Dispose();
|
||||
MouseDriver?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
}
|
||||
}
|
569
src/Ryujinx.Input/HLE/NpadController.cs
Normal file
569
src/Ryujinx.Input/HLE/NpadController.cs
Normal file
@ -0,0 +1,569 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
|
||||
using ConfigControllerType = Ryujinx.Common.Configuration.Hid.ControllerType;
|
||||
|
||||
namespace Ryujinx.Input.HLE
|
||||
{
|
||||
public class NpadController : IDisposable
|
||||
{
|
||||
private class HLEButtonMappingEntry
|
||||
{
|
||||
public readonly GamepadButtonInputId DriverInputId;
|
||||
public readonly ControllerKeys HLEInput;
|
||||
|
||||
public HLEButtonMappingEntry(GamepadButtonInputId driverInputId, ControllerKeys hleInput)
|
||||
{
|
||||
DriverInputId = driverInputId;
|
||||
HLEInput = hleInput;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HLEButtonMappingEntry[] _hleButtonMapping = new HLEButtonMappingEntry[]
|
||||
{
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.A, ControllerKeys.A),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.B, ControllerKeys.B),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.X, ControllerKeys.X),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.Y, ControllerKeys.Y),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.LeftStick, ControllerKeys.LStick),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.RightStick, ControllerKeys.RStick),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.LeftShoulder, ControllerKeys.L),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.RightShoulder, ControllerKeys.R),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.LeftTrigger, ControllerKeys.Zl),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.RightTrigger, ControllerKeys.Zr),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.DpadUp, ControllerKeys.DpadUp),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.DpadDown, ControllerKeys.DpadDown),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.DpadLeft, ControllerKeys.DpadLeft),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.DpadRight, ControllerKeys.DpadRight),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.Minus, ControllerKeys.Minus),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.Plus, ControllerKeys.Plus),
|
||||
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, ControllerKeys.SlLeft),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, ControllerKeys.SrLeft),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, ControllerKeys.SlRight),
|
||||
new HLEButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, ControllerKeys.SrRight),
|
||||
};
|
||||
|
||||
private class HLEKeyboardMappingEntry
|
||||
{
|
||||
public readonly Key TargetKey;
|
||||
public readonly byte Target;
|
||||
|
||||
public HLEKeyboardMappingEntry(Key targetKey, byte target)
|
||||
{
|
||||
TargetKey = targetKey;
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HLEKeyboardMappingEntry[] KeyMapping = new HLEKeyboardMappingEntry[]
|
||||
{
|
||||
new HLEKeyboardMappingEntry(Key.A, 0x4),
|
||||
new HLEKeyboardMappingEntry(Key.B, 0x5),
|
||||
new HLEKeyboardMappingEntry(Key.C, 0x6),
|
||||
new HLEKeyboardMappingEntry(Key.D, 0x7),
|
||||
new HLEKeyboardMappingEntry(Key.E, 0x8),
|
||||
new HLEKeyboardMappingEntry(Key.F, 0x9),
|
||||
new HLEKeyboardMappingEntry(Key.G, 0xA),
|
||||
new HLEKeyboardMappingEntry(Key.H, 0xB),
|
||||
new HLEKeyboardMappingEntry(Key.I, 0xC),
|
||||
new HLEKeyboardMappingEntry(Key.J, 0xD),
|
||||
new HLEKeyboardMappingEntry(Key.K, 0xE),
|
||||
new HLEKeyboardMappingEntry(Key.L, 0xF),
|
||||
new HLEKeyboardMappingEntry(Key.M, 0x10),
|
||||
new HLEKeyboardMappingEntry(Key.N, 0x11),
|
||||
new HLEKeyboardMappingEntry(Key.O, 0x12),
|
||||
new HLEKeyboardMappingEntry(Key.P, 0x13),
|
||||
new HLEKeyboardMappingEntry(Key.Q, 0x14),
|
||||
new HLEKeyboardMappingEntry(Key.R, 0x15),
|
||||
new HLEKeyboardMappingEntry(Key.S, 0x16),
|
||||
new HLEKeyboardMappingEntry(Key.T, 0x17),
|
||||
new HLEKeyboardMappingEntry(Key.U, 0x18),
|
||||
new HLEKeyboardMappingEntry(Key.V, 0x19),
|
||||
new HLEKeyboardMappingEntry(Key.W, 0x1A),
|
||||
new HLEKeyboardMappingEntry(Key.X, 0x1B),
|
||||
new HLEKeyboardMappingEntry(Key.Y, 0x1C),
|
||||
new HLEKeyboardMappingEntry(Key.Z, 0x1D),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.Number1, 0x1E),
|
||||
new HLEKeyboardMappingEntry(Key.Number2, 0x1F),
|
||||
new HLEKeyboardMappingEntry(Key.Number3, 0x20),
|
||||
new HLEKeyboardMappingEntry(Key.Number4, 0x21),
|
||||
new HLEKeyboardMappingEntry(Key.Number5, 0x22),
|
||||
new HLEKeyboardMappingEntry(Key.Number6, 0x23),
|
||||
new HLEKeyboardMappingEntry(Key.Number7, 0x24),
|
||||
new HLEKeyboardMappingEntry(Key.Number8, 0x25),
|
||||
new HLEKeyboardMappingEntry(Key.Number9, 0x26),
|
||||
new HLEKeyboardMappingEntry(Key.Number0, 0x27),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.Enter, 0x28),
|
||||
new HLEKeyboardMappingEntry(Key.Escape, 0x29),
|
||||
new HLEKeyboardMappingEntry(Key.BackSpace, 0x2A),
|
||||
new HLEKeyboardMappingEntry(Key.Tab, 0x2B),
|
||||
new HLEKeyboardMappingEntry(Key.Space, 0x2C),
|
||||
new HLEKeyboardMappingEntry(Key.Minus, 0x2D),
|
||||
new HLEKeyboardMappingEntry(Key.Plus, 0x2E),
|
||||
new HLEKeyboardMappingEntry(Key.BracketLeft, 0x2F),
|
||||
new HLEKeyboardMappingEntry(Key.BracketRight, 0x30),
|
||||
new HLEKeyboardMappingEntry(Key.BackSlash, 0x31),
|
||||
new HLEKeyboardMappingEntry(Key.Tilde, 0x32),
|
||||
new HLEKeyboardMappingEntry(Key.Semicolon, 0x33),
|
||||
new HLEKeyboardMappingEntry(Key.Quote, 0x34),
|
||||
new HLEKeyboardMappingEntry(Key.Grave, 0x35),
|
||||
new HLEKeyboardMappingEntry(Key.Comma, 0x36),
|
||||
new HLEKeyboardMappingEntry(Key.Period, 0x37),
|
||||
new HLEKeyboardMappingEntry(Key.Slash, 0x38),
|
||||
new HLEKeyboardMappingEntry(Key.CapsLock, 0x39),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.F1, 0x3a),
|
||||
new HLEKeyboardMappingEntry(Key.F2, 0x3b),
|
||||
new HLEKeyboardMappingEntry(Key.F3, 0x3c),
|
||||
new HLEKeyboardMappingEntry(Key.F4, 0x3d),
|
||||
new HLEKeyboardMappingEntry(Key.F5, 0x3e),
|
||||
new HLEKeyboardMappingEntry(Key.F6, 0x3f),
|
||||
new HLEKeyboardMappingEntry(Key.F7, 0x40),
|
||||
new HLEKeyboardMappingEntry(Key.F8, 0x41),
|
||||
new HLEKeyboardMappingEntry(Key.F9, 0x42),
|
||||
new HLEKeyboardMappingEntry(Key.F10, 0x43),
|
||||
new HLEKeyboardMappingEntry(Key.F11, 0x44),
|
||||
new HLEKeyboardMappingEntry(Key.F12, 0x45),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.PrintScreen, 0x46),
|
||||
new HLEKeyboardMappingEntry(Key.ScrollLock, 0x47),
|
||||
new HLEKeyboardMappingEntry(Key.Pause, 0x48),
|
||||
new HLEKeyboardMappingEntry(Key.Insert, 0x49),
|
||||
new HLEKeyboardMappingEntry(Key.Home, 0x4A),
|
||||
new HLEKeyboardMappingEntry(Key.PageUp, 0x4B),
|
||||
new HLEKeyboardMappingEntry(Key.Delete, 0x4C),
|
||||
new HLEKeyboardMappingEntry(Key.End, 0x4D),
|
||||
new HLEKeyboardMappingEntry(Key.PageDown, 0x4E),
|
||||
new HLEKeyboardMappingEntry(Key.Right, 0x4F),
|
||||
new HLEKeyboardMappingEntry(Key.Left, 0x50),
|
||||
new HLEKeyboardMappingEntry(Key.Down, 0x51),
|
||||
new HLEKeyboardMappingEntry(Key.Up, 0x52),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.NumLock, 0x53),
|
||||
new HLEKeyboardMappingEntry(Key.KeypadDivide, 0x54),
|
||||
new HLEKeyboardMappingEntry(Key.KeypadMultiply, 0x55),
|
||||
new HLEKeyboardMappingEntry(Key.KeypadSubtract, 0x56),
|
||||
new HLEKeyboardMappingEntry(Key.KeypadAdd, 0x57),
|
||||
new HLEKeyboardMappingEntry(Key.KeypadEnter, 0x58),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad1, 0x59),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad2, 0x5A),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad3, 0x5B),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad4, 0x5C),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad5, 0x5D),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad6, 0x5E),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad7, 0x5F),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad8, 0x60),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad9, 0x61),
|
||||
new HLEKeyboardMappingEntry(Key.Keypad0, 0x62),
|
||||
new HLEKeyboardMappingEntry(Key.KeypadDecimal, 0x63),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.F13, 0x68),
|
||||
new HLEKeyboardMappingEntry(Key.F14, 0x69),
|
||||
new HLEKeyboardMappingEntry(Key.F15, 0x6A),
|
||||
new HLEKeyboardMappingEntry(Key.F16, 0x6B),
|
||||
new HLEKeyboardMappingEntry(Key.F17, 0x6C),
|
||||
new HLEKeyboardMappingEntry(Key.F18, 0x6D),
|
||||
new HLEKeyboardMappingEntry(Key.F19, 0x6E),
|
||||
new HLEKeyboardMappingEntry(Key.F20, 0x6F),
|
||||
new HLEKeyboardMappingEntry(Key.F21, 0x70),
|
||||
new HLEKeyboardMappingEntry(Key.F22, 0x71),
|
||||
new HLEKeyboardMappingEntry(Key.F23, 0x72),
|
||||
new HLEKeyboardMappingEntry(Key.F24, 0x73),
|
||||
|
||||
new HLEKeyboardMappingEntry(Key.ControlLeft, 0xE0),
|
||||
new HLEKeyboardMappingEntry(Key.ShiftLeft, 0xE1),
|
||||
new HLEKeyboardMappingEntry(Key.AltLeft, 0xE2),
|
||||
new HLEKeyboardMappingEntry(Key.WinLeft, 0xE3),
|
||||
new HLEKeyboardMappingEntry(Key.ControlRight, 0xE4),
|
||||
new HLEKeyboardMappingEntry(Key.ShiftRight, 0xE5),
|
||||
new HLEKeyboardMappingEntry(Key.AltRight, 0xE6),
|
||||
new HLEKeyboardMappingEntry(Key.WinRight, 0xE7),
|
||||
};
|
||||
|
||||
private static readonly HLEKeyboardMappingEntry[] KeyModifierMapping = new HLEKeyboardMappingEntry[]
|
||||
{
|
||||
new HLEKeyboardMappingEntry(Key.ControlLeft, 0),
|
||||
new HLEKeyboardMappingEntry(Key.ShiftLeft, 1),
|
||||
new HLEKeyboardMappingEntry(Key.AltLeft, 2),
|
||||
new HLEKeyboardMappingEntry(Key.WinLeft, 3),
|
||||
new HLEKeyboardMappingEntry(Key.ControlRight, 4),
|
||||
new HLEKeyboardMappingEntry(Key.ShiftRight, 5),
|
||||
new HLEKeyboardMappingEntry(Key.AltRight, 6),
|
||||
new HLEKeyboardMappingEntry(Key.WinRight, 7),
|
||||
new HLEKeyboardMappingEntry(Key.CapsLock, 8),
|
||||
new HLEKeyboardMappingEntry(Key.ScrollLock, 9),
|
||||
new HLEKeyboardMappingEntry(Key.NumLock, 10),
|
||||
};
|
||||
|
||||
private bool _isValid;
|
||||
private string _id;
|
||||
|
||||
private MotionInput _leftMotionInput;
|
||||
private MotionInput _rightMotionInput;
|
||||
|
||||
private IGamepad _gamepad;
|
||||
private InputConfig _config;
|
||||
|
||||
public IGamepadDriver GamepadDriver { get; private set; }
|
||||
public GamepadStateSnapshot State { get; private set; }
|
||||
|
||||
public string Id => _id;
|
||||
|
||||
private CemuHookClient _cemuHookClient;
|
||||
|
||||
public NpadController(CemuHookClient cemuHookClient)
|
||||
{
|
||||
State = default;
|
||||
_id = null;
|
||||
_isValid = false;
|
||||
_cemuHookClient = cemuHookClient;
|
||||
}
|
||||
|
||||
public bool UpdateDriverConfiguration(IGamepadDriver gamepadDriver, InputConfig config)
|
||||
{
|
||||
GamepadDriver = gamepadDriver;
|
||||
|
||||
_gamepad?.Dispose();
|
||||
|
||||
_id = config.Id;
|
||||
_gamepad = GamepadDriver.GetGamepad(_id);
|
||||
_isValid = _gamepad != null;
|
||||
|
||||
UpdateUserConfiguration(config);
|
||||
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
public void UpdateUserConfiguration(InputConfig config)
|
||||
{
|
||||
if (config is StandardControllerInputConfig controllerConfig)
|
||||
{
|
||||
bool needsMotionInputUpdate = _config == null || (_config is StandardControllerInputConfig oldControllerConfig &&
|
||||
(oldControllerConfig.Motion.EnableMotion != controllerConfig.Motion.EnableMotion) &&
|
||||
(oldControllerConfig.Motion.MotionBackend != controllerConfig.Motion.MotionBackend));
|
||||
|
||||
if (needsMotionInputUpdate)
|
||||
{
|
||||
UpdateMotionInput(controllerConfig.Motion);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-controller doesn't have motions.
|
||||
_leftMotionInput = null;
|
||||
}
|
||||
|
||||
_config = config;
|
||||
|
||||
if (_isValid)
|
||||
{
|
||||
_gamepad.SetConfiguration(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMotionInput(MotionConfigController motionConfig)
|
||||
{
|
||||
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
|
||||
{
|
||||
_leftMotionInput = new MotionInput();
|
||||
}
|
||||
else
|
||||
{
|
||||
_leftMotionInput = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_isValid && GamepadDriver != null)
|
||||
{
|
||||
State = _gamepad.GetMappedStateSnapshot();
|
||||
|
||||
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion)
|
||||
{
|
||||
if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
|
||||
{
|
||||
if (_gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion))
|
||||
{
|
||||
Vector3 accelerometer = _gamepad.GetMotionData(MotionInputId.Accelerometer);
|
||||
Vector3 gyroscope = _gamepad.GetMotionData(MotionInputId.Gyroscope);
|
||||
|
||||
accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y);
|
||||
gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y);
|
||||
|
||||
_leftMotionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
|
||||
|
||||
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
|
||||
{
|
||||
_rightMotionInput = _leftMotionInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook && controllerConfig.Motion is CemuHookMotionConfigController cemuControllerConfig)
|
||||
{
|
||||
int clientId = (int)controllerConfig.PlayerIndex;
|
||||
|
||||
// First of all ensure we are registered
|
||||
_cemuHookClient.RegisterClient(clientId, cemuControllerConfig.DsuServerHost, cemuControllerConfig.DsuServerPort);
|
||||
|
||||
// Then request and retrieve the data
|
||||
_cemuHookClient.RequestData(clientId, cemuControllerConfig.Slot);
|
||||
_cemuHookClient.TryGetData(clientId, cemuControllerConfig.Slot, out _leftMotionInput);
|
||||
|
||||
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
|
||||
{
|
||||
if (!cemuControllerConfig.MirrorInput)
|
||||
{
|
||||
_cemuHookClient.RequestData(clientId, cemuControllerConfig.AltSlot);
|
||||
_cemuHookClient.TryGetData(clientId, cemuControllerConfig.AltSlot, out _rightMotionInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
_rightMotionInput = _leftMotionInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset states
|
||||
State = default;
|
||||
_leftMotionInput = null;
|
||||
}
|
||||
}
|
||||
|
||||
public GamepadInput GetHLEInputState()
|
||||
{
|
||||
GamepadInput state = new GamepadInput();
|
||||
|
||||
// First update all buttons
|
||||
foreach (HLEButtonMappingEntry entry in _hleButtonMapping)
|
||||
{
|
||||
if (State.IsPressed(entry.DriverInputId))
|
||||
{
|
||||
state.Buttons |= entry.HLEInput;
|
||||
}
|
||||
}
|
||||
|
||||
if (_gamepad is IKeyboard)
|
||||
{
|
||||
(float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
|
||||
(float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
|
||||
|
||||
state.LStick = new JoystickPosition
|
||||
{
|
||||
Dx = ClampAxis(leftAxisX),
|
||||
Dy = ClampAxis(leftAxisY)
|
||||
};
|
||||
|
||||
state.RStick = new JoystickPosition
|
||||
{
|
||||
Dx = ClampAxis(rightAxisX),
|
||||
Dy = ClampAxis(rightAxisY)
|
||||
};
|
||||
}
|
||||
else if (_config is StandardControllerInputConfig controllerConfig)
|
||||
{
|
||||
(float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
|
||||
(float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
|
||||
|
||||
state.LStick = ClampToCircle(ApplyDeadzone(leftAxisX, leftAxisY, controllerConfig.DeadzoneLeft), controllerConfig.RangeLeft);
|
||||
state.RStick = ClampToCircle(ApplyDeadzone(rightAxisX, rightAxisY, controllerConfig.DeadzoneRight), controllerConfig.RangeRight);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static JoystickPosition ApplyDeadzone(float x, float y, float deadzone)
|
||||
{
|
||||
float magnitudeClamped = Math.Min(MathF.Sqrt(x * x + y * y), 1f);
|
||||
|
||||
if (magnitudeClamped <= deadzone)
|
||||
{
|
||||
return new JoystickPosition() {Dx = 0, Dy = 0};
|
||||
}
|
||||
|
||||
return new JoystickPosition()
|
||||
{
|
||||
Dx = ClampAxis((x / magnitudeClamped) * ((magnitudeClamped - deadzone) / (1 - deadzone))),
|
||||
Dy = ClampAxis((y / magnitudeClamped) * ((magnitudeClamped - deadzone) / (1 - deadzone)))
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static short ClampAxis(float value)
|
||||
{
|
||||
if (Math.Sign(value) < 0)
|
||||
{
|
||||
return (short)Math.Max(value * -short.MinValue, short.MinValue);
|
||||
}
|
||||
|
||||
return (short)Math.Min(value * short.MaxValue, short.MaxValue);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static JoystickPosition ClampToCircle(JoystickPosition position, float range)
|
||||
{
|
||||
Vector2 point = new Vector2(position.Dx, position.Dy) * range;
|
||||
|
||||
if (point.Length() > short.MaxValue)
|
||||
{
|
||||
point = point / point.Length() * short.MaxValue;
|
||||
}
|
||||
|
||||
return new JoystickPosition
|
||||
{
|
||||
Dx = (int)point.X,
|
||||
Dy = (int)point.Y
|
||||
};
|
||||
}
|
||||
|
||||
public SixAxisInput GetHLEMotionState(bool isJoyconRightPair = false)
|
||||
{
|
||||
float[] orientationForHLE = new float[9];
|
||||
Vector3 gyroscope;
|
||||
Vector3 accelerometer;
|
||||
Vector3 rotation;
|
||||
|
||||
MotionInput motionInput = _leftMotionInput;
|
||||
|
||||
if (isJoyconRightPair)
|
||||
{
|
||||
if (_rightMotionInput == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
motionInput = _rightMotionInput;
|
||||
}
|
||||
|
||||
if (motionInput != null)
|
||||
{
|
||||
gyroscope = Truncate(motionInput.Gyroscrope * 0.0027f, 3);
|
||||
accelerometer = Truncate(motionInput.Accelerometer, 3);
|
||||
rotation = Truncate(motionInput.Rotation * 0.0027f, 3);
|
||||
|
||||
Matrix4x4 orientation = motionInput.GetOrientation();
|
||||
|
||||
orientationForHLE[0] = Math.Clamp(orientation.M11, -1f, 1f);
|
||||
orientationForHLE[1] = Math.Clamp(orientation.M12, -1f, 1f);
|
||||
orientationForHLE[2] = Math.Clamp(orientation.M13, -1f, 1f);
|
||||
orientationForHLE[3] = Math.Clamp(orientation.M21, -1f, 1f);
|
||||
orientationForHLE[4] = Math.Clamp(orientation.M22, -1f, 1f);
|
||||
orientationForHLE[5] = Math.Clamp(orientation.M23, -1f, 1f);
|
||||
orientationForHLE[6] = Math.Clamp(orientation.M31, -1f, 1f);
|
||||
orientationForHLE[7] = Math.Clamp(orientation.M32, -1f, 1f);
|
||||
orientationForHLE[8] = Math.Clamp(orientation.M33, -1f, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
gyroscope = new Vector3();
|
||||
accelerometer = new Vector3();
|
||||
rotation = new Vector3();
|
||||
}
|
||||
|
||||
return new SixAxisInput()
|
||||
{
|
||||
Accelerometer = accelerometer,
|
||||
Gyroscope = gyroscope,
|
||||
Rotation = rotation,
|
||||
Orientation = orientationForHLE
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector3 Truncate(Vector3 value, int decimals)
|
||||
{
|
||||
float power = MathF.Pow(10, decimals);
|
||||
|
||||
value.X = float.IsNegative(value.X) ? MathF.Ceiling(value.X * power) / power : MathF.Floor(value.X * power) / power;
|
||||
value.Y = float.IsNegative(value.Y) ? MathF.Ceiling(value.Y * power) / power : MathF.Floor(value.Y * power) / power;
|
||||
value.Z = float.IsNegative(value.Z) ? MathF.Ceiling(value.Z * power) / power : MathF.Floor(value.Z * power) / power;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public KeyboardInput? GetHLEKeyboardInput()
|
||||
{
|
||||
if (_gamepad is IKeyboard keyboard)
|
||||
{
|
||||
KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot();
|
||||
|
||||
KeyboardInput hidKeyboard = new KeyboardInput
|
||||
{
|
||||
Modifier = 0,
|
||||
Keys = new ulong[0x4]
|
||||
};
|
||||
|
||||
foreach (HLEKeyboardMappingEntry entry in KeyMapping)
|
||||
{
|
||||
ulong value = keyboardState.IsPressed(entry.TargetKey) ? 1UL : 0UL;
|
||||
|
||||
hidKeyboard.Keys[entry.Target / 0x40] |= (value << (entry.Target % 0x40));
|
||||
}
|
||||
|
||||
foreach (HLEKeyboardMappingEntry entry in KeyModifierMapping)
|
||||
{
|
||||
int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0;
|
||||
|
||||
hidKeyboard.Modifier |= value << entry.Target;
|
||||
}
|
||||
|
||||
return hidKeyboard;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_gamepad?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
public void UpdateRumble(ConcurrentQueue<(VibrationValue, VibrationValue)> queue)
|
||||
{
|
||||
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
|
||||
{
|
||||
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble)
|
||||
{
|
||||
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
|
||||
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
|
||||
|
||||
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
|
||||
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
|
||||
|
||||
_gamepad.Rumble(low, high, uint.MaxValue);
|
||||
|
||||
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
|
||||
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
|
||||
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
|
||||
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
|
||||
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
|
||||
$"--> ({low}, {high})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
320
src/Ryujinx.Input/HLE/NpadManager.cs
Normal file
320
src/Ryujinx.Input/HLE/NpadManager.cs
Normal file
@ -0,0 +1,320 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
|
||||
using Switch = Ryujinx.HLE.Switch;
|
||||
|
||||
namespace Ryujinx.Input.HLE
|
||||
{
|
||||
public class NpadManager : IDisposable
|
||||
{
|
||||
private CemuHookClient _cemuHookClient;
|
||||
|
||||
private object _lock = new object();
|
||||
|
||||
private bool _blockInputUpdates;
|
||||
|
||||
private const int MaxControllers = 9;
|
||||
|
||||
private NpadController[] _controllers;
|
||||
|
||||
private readonly IGamepadDriver _keyboardDriver;
|
||||
private readonly IGamepadDriver _gamepadDriver;
|
||||
private readonly IGamepadDriver _mouseDriver;
|
||||
private bool _isDisposed;
|
||||
|
||||
private List<InputConfig> _inputConfig;
|
||||
private bool _enableKeyboard;
|
||||
private bool _enableMouse;
|
||||
private Switch _device;
|
||||
|
||||
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
|
||||
{
|
||||
_controllers = new NpadController[MaxControllers];
|
||||
_cemuHookClient = new CemuHookClient(this);
|
||||
|
||||
_keyboardDriver = keyboardDriver;
|
||||
_gamepadDriver = gamepadDriver;
|
||||
_mouseDriver = mouseDriver;
|
||||
_inputConfig = new List<InputConfig>();
|
||||
|
||||
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||
}
|
||||
|
||||
private void RefreshInputConfigForHLE()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
List<InputConfig> validInputs = new List<InputConfig>();
|
||||
foreach (var inputConfigEntry in _inputConfig)
|
||||
{
|
||||
if (_controllers[(int)inputConfigEntry.PlayerIndex] != null)
|
||||
{
|
||||
validInputs.Add(inputConfigEntry);
|
||||
}
|
||||
}
|
||||
|
||||
_device.Hid.RefreshInputConfig(validInputs);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOnGamepadDisconnected(string obj)
|
||||
{
|
||||
// Force input reload
|
||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
||||
}
|
||||
|
||||
private void HandleOnGamepadConnected(string id)
|
||||
{
|
||||
// Force input reload
|
||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
|
||||
{
|
||||
IGamepadDriver targetDriver = _gamepadDriver;
|
||||
|
||||
if (config is StandardControllerInputConfig)
|
||||
{
|
||||
targetDriver = _gamepadDriver;
|
||||
}
|
||||
else if (config is StandardKeyboardInputConfig)
|
||||
{
|
||||
targetDriver = _keyboardDriver;
|
||||
}
|
||||
|
||||
Debug.Assert(targetDriver != null, "Unknown input configuration!");
|
||||
|
||||
if (controller.GamepadDriver != targetDriver || controller.Id != config.Id)
|
||||
{
|
||||
return controller.UpdateDriverConfiguration(targetDriver, config);
|
||||
}
|
||||
else
|
||||
{
|
||||
return controller.GamepadDriver != null;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadConfiguration(List<InputConfig> inputConfig, bool enableKeyboard, bool enableMouse)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
for (int i = 0; i < _controllers.Length; i++)
|
||||
{
|
||||
_controllers[i]?.Dispose();
|
||||
_controllers[i] = null;
|
||||
}
|
||||
|
||||
List<InputConfig> validInputs = new List<InputConfig>();
|
||||
|
||||
foreach (InputConfig inputConfigEntry in inputConfig)
|
||||
{
|
||||
NpadController controller = new NpadController(_cemuHookClient);
|
||||
|
||||
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
controller.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
_controllers[(int)inputConfigEntry.PlayerIndex] = controller;
|
||||
validInputs.Add(inputConfigEntry);
|
||||
}
|
||||
}
|
||||
|
||||
_inputConfig = inputConfig;
|
||||
_enableKeyboard = enableKeyboard;
|
||||
_enableMouse = enableMouse;
|
||||
|
||||
_device.Hid.RefreshInputConfig(validInputs);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnblockInputUpdates()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_blockInputUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void BlockInputUpdates()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_blockInputUpdates = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(Switch device, List<InputConfig> inputConfig, bool enableKeyboard, bool enableMouse)
|
||||
{
|
||||
_device = device;
|
||||
_device.Configuration.RefreshInputConfig = RefreshInputConfigForHLE;
|
||||
|
||||
ReloadConfiguration(inputConfig, enableKeyboard, enableMouse);
|
||||
}
|
||||
|
||||
public void Update(float aspectRatio = 1)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
List<GamepadInput> hleInputStates = new List<GamepadInput>();
|
||||
List<SixAxisInput> hleMotionStates = new List<SixAxisInput>(NpadDevices.MaxControllers);
|
||||
|
||||
KeyboardInput? hleKeyboardInput = null;
|
||||
|
||||
foreach (InputConfig inputConfig in _inputConfig)
|
||||
{
|
||||
GamepadInput inputState = default;
|
||||
(SixAxisInput, SixAxisInput) motionState = default;
|
||||
|
||||
NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
|
||||
Ryujinx.HLE.HOS.Services.Hid.PlayerIndex playerIndex = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
|
||||
|
||||
bool isJoyconPair = false;
|
||||
|
||||
// Do we allow input updates and is a controller connected?
|
||||
if (!_blockInputUpdates && controller != null)
|
||||
{
|
||||
DriverConfigurationUpdate(ref controller, inputConfig);
|
||||
|
||||
controller.UpdateUserConfiguration(inputConfig);
|
||||
controller.Update();
|
||||
controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
|
||||
|
||||
inputState = controller.GetHLEInputState();
|
||||
|
||||
inputState.Buttons |= _device.Hid.UpdateStickButtons(inputState.LStick, inputState.RStick);
|
||||
|
||||
isJoyconPair = inputConfig.ControllerType == Common.Configuration.Hid.ControllerType.JoyconPair;
|
||||
|
||||
var altMotionState = isJoyconPair ? controller.GetHLEMotionState(true) : default;
|
||||
|
||||
motionState = (controller.GetHLEMotionState(), altMotionState);
|
||||
|
||||
if (_enableKeyboard)
|
||||
{
|
||||
hleKeyboardInput = controller.GetHLEKeyboardInput();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure that orientation isn't null
|
||||
motionState.Item1.Orientation = new float[9];
|
||||
}
|
||||
|
||||
inputState.PlayerId = playerIndex;
|
||||
motionState.Item1.PlayerId = playerIndex;
|
||||
|
||||
hleInputStates.Add(inputState);
|
||||
hleMotionStates.Add(motionState.Item1);
|
||||
|
||||
if (isJoyconPair && !motionState.Item2.Equals(default))
|
||||
{
|
||||
motionState.Item2.PlayerId = playerIndex;
|
||||
|
||||
hleMotionStates.Add(motionState.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
_device.Hid.Npads.Update(hleInputStates);
|
||||
_device.Hid.Npads.UpdateSixAxis(hleMotionStates);
|
||||
|
||||
if (hleKeyboardInput.HasValue)
|
||||
{
|
||||
_device.Hid.Keyboard.Update(hleKeyboardInput.Value);
|
||||
}
|
||||
|
||||
if (_enableMouse)
|
||||
{
|
||||
var mouse = _mouseDriver.GetGamepad("0") as IMouse;
|
||||
|
||||
var mouseInput = IMouse.GetMouseStateSnapshot(mouse);
|
||||
|
||||
uint buttons = 0;
|
||||
|
||||
if (mouseInput.IsPressed(MouseButton.Button1))
|
||||
{
|
||||
buttons |= 1 << 0;
|
||||
}
|
||||
|
||||
if (mouseInput.IsPressed(MouseButton.Button2))
|
||||
{
|
||||
buttons |= 1 << 1;
|
||||
}
|
||||
|
||||
if (mouseInput.IsPressed(MouseButton.Button3))
|
||||
{
|
||||
buttons |= 1 << 2;
|
||||
}
|
||||
|
||||
if (mouseInput.IsPressed(MouseButton.Button4))
|
||||
{
|
||||
buttons |= 1 << 3;
|
||||
}
|
||||
|
||||
if (mouseInput.IsPressed(MouseButton.Button5))
|
||||
{
|
||||
buttons |= 1 << 4;
|
||||
}
|
||||
|
||||
var position = IMouse.GetScreenPosition(mouseInput.Position, mouse.ClientSize, aspectRatio);
|
||||
|
||||
_device.Hid.Mouse.Update((int)position.X, (int)position.Y, buttons, (int)mouseInput.Scroll.X, (int)mouseInput.Scroll.Y, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_device.Hid.Mouse.Update(0, 0);
|
||||
}
|
||||
|
||||
_device.TamperMachine.UpdateInput(hleInputStates);
|
||||
}
|
||||
}
|
||||
|
||||
internal InputConfig GetPlayerInputConfigByIndex(int index)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _inputConfig.Find(x => x.PlayerIndex == (Ryujinx.Common.Configuration.Hid.PlayerIndex)index);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_cemuHookClient.Dispose();
|
||||
|
||||
_gamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
|
||||
_gamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
|
||||
|
||||
for (int i = 0; i < _controllers.Length; i++)
|
||||
{
|
||||
_controllers[i]?.Dispose();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
}
|
||||
}
|
99
src/Ryujinx.Input/HLE/TouchScreenManager.cs
Normal file
99
src/Ryujinx.Input/HLE/TouchScreenManager.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.TouchScreen;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Input.HLE
|
||||
{
|
||||
public class TouchScreenManager : IDisposable
|
||||
{
|
||||
private readonly IMouse _mouse;
|
||||
private Switch _device;
|
||||
private bool _wasClicking;
|
||||
|
||||
public TouchScreenManager(IMouse mouse)
|
||||
{
|
||||
_mouse = mouse;
|
||||
}
|
||||
|
||||
public void Initialize(Switch device)
|
||||
{
|
||||
_device = device;
|
||||
}
|
||||
|
||||
public bool Update(bool isFocused, bool isClicking = false, float aspectRatio = 0)
|
||||
{
|
||||
if (!isFocused || (!_wasClicking && !isClicking))
|
||||
{
|
||||
// In case we lost focus, send the end touch.
|
||||
if (_wasClicking && !isClicking)
|
||||
{
|
||||
MouseStateSnapshot snapshot = IMouse.GetMouseStateSnapshot(_mouse);
|
||||
var touchPosition = IMouse.GetScreenPosition(snapshot.Position, _mouse.ClientSize, aspectRatio);
|
||||
|
||||
TouchPoint currentPoint = new TouchPoint
|
||||
{
|
||||
Attribute = TouchAttribute.End,
|
||||
|
||||
X = (uint)touchPosition.X,
|
||||
Y = (uint)touchPosition.Y,
|
||||
|
||||
// Placeholder values till more data is acquired
|
||||
DiameterX = 10,
|
||||
DiameterY = 10,
|
||||
Angle = 90
|
||||
};
|
||||
|
||||
_device.Hid.Touchscreen.Update(currentPoint);
|
||||
|
||||
}
|
||||
|
||||
_wasClicking = false;
|
||||
|
||||
_device.Hid.Touchscreen.Update();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aspectRatio > 0)
|
||||
{
|
||||
MouseStateSnapshot snapshot = IMouse.GetMouseStateSnapshot(_mouse);
|
||||
var touchPosition = IMouse.GetScreenPosition(snapshot.Position, _mouse.ClientSize, aspectRatio);
|
||||
|
||||
TouchAttribute attribute = TouchAttribute.None;
|
||||
|
||||
if (!_wasClicking && isClicking)
|
||||
{
|
||||
attribute = TouchAttribute.Start;
|
||||
}
|
||||
else if (_wasClicking && !isClicking)
|
||||
{
|
||||
attribute = TouchAttribute.End;
|
||||
}
|
||||
|
||||
TouchPoint currentPoint = new TouchPoint
|
||||
{
|
||||
Attribute = attribute,
|
||||
|
||||
X = (uint)touchPosition.X,
|
||||
Y = (uint)touchPosition.Y,
|
||||
|
||||
// Placeholder values till more data is acquired
|
||||
DiameterX = 10,
|
||||
DiameterY = 10,
|
||||
Angle = 90
|
||||
};
|
||||
|
||||
_device.Hid.Touchscreen.Update(currentPoint);
|
||||
|
||||
_wasClicking = isClicking;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
122
src/Ryujinx.Input/IGamepad.cs
Normal file
122
src/Ryujinx.Input/IGamepad.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent an emulated gamepad.
|
||||
/// </summary>
|
||||
public interface IGamepad : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Features supported by the gamepad.
|
||||
/// </summary>
|
||||
GamepadFeaturesFlag Features { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique Id of the gamepad.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the gamepad.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the gamepad is connected.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given input button is pressed on the gamepad.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The button id</param>
|
||||
/// <returns>True if the given button is pressed on the gamepad</returns>
|
||||
bool IsPressed(GamepadButtonInputId inputId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the values of a given input joystick on the gamepad.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The stick id</param>
|
||||
/// <returns>The values of the given input joystick on the gamepad</returns>
|
||||
(float, float) GetStick(StickInputId inputId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the values of a given motion sensors on the gamepad.
|
||||
/// </summary>
|
||||
/// <param name="inputId">The motion id</param>
|
||||
/// <returns> The values of the given motion sensors on the gamepad.</returns>
|
||||
Vector3 GetMotionData(MotionInputId inputId);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the threshold of the triggers on the gamepad.
|
||||
/// </summary>
|
||||
/// <param name="triggerThreshold">The threshold value for the triggers on the gamepad</param>
|
||||
void SetTriggerThreshold(float triggerThreshold);
|
||||
|
||||
/// <summary>
|
||||
/// Set the configuration of the gamepad.
|
||||
/// </summary>
|
||||
/// <remarks>This expect config to be in the format expected by the driver</remarks>
|
||||
/// <param name="configuration">The configuration of the gamepad</param>
|
||||
void SetConfiguration(InputConfig configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Starts a rumble effect on the gamepad.
|
||||
/// </summary>
|
||||
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
|
||||
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
|
||||
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
|
||||
void Rumble(float lowFrequency, float highFrequency, uint durationMs);
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
|
||||
/// </summary>
|
||||
/// <returns>A remapped snaphost of the state of the gamepad.</returns>
|
||||
GamepadStateSnapshot GetMappedStateSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of the gamepad.
|
||||
/// </summary>
|
||||
/// <returns>A snaphost of the state of the gamepad.</returns>
|
||||
GamepadStateSnapshot GetStateSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of a gamepad.
|
||||
/// </summary>
|
||||
/// <param name="gamepad">The gamepad to do a snapshot of</param>
|
||||
/// <returns>A snaphost of the state of the gamepad.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static GamepadStateSnapshot GetStateSnapshot(IGamepad gamepad)
|
||||
{
|
||||
// NOTE: Update Array size if JoystickInputId is changed.
|
||||
Array3<Array2<float>> joysticksState = default;
|
||||
|
||||
for (StickInputId inputId = StickInputId.Left; inputId < StickInputId.Count; inputId++)
|
||||
{
|
||||
(float state0, float state1) = gamepad.GetStick(inputId);
|
||||
|
||||
Array2<float> state = default;
|
||||
|
||||
state[0] = state0;
|
||||
state[1] = state1;
|
||||
|
||||
joysticksState[(int)inputId] = state;
|
||||
}
|
||||
|
||||
// NOTE: Update Array size if GamepadInputId is changed.
|
||||
Array28<bool> buttonsState = default;
|
||||
|
||||
for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
|
||||
{
|
||||
buttonsState[(int)inputId] = gamepad.IsPressed(inputId);
|
||||
}
|
||||
|
||||
return new GamepadStateSnapshot(joysticksState, buttonsState);
|
||||
}
|
||||
}
|
||||
}
|
37
src/Ryujinx.Input/IGamepadDriver.cs
Normal file
37
src/Ryujinx.Input/IGamepadDriver.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent an emulated gamepad driver used to provide input in the emulator.
|
||||
/// </summary>
|
||||
public interface IGamepadDriver : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the driver
|
||||
/// </summary>
|
||||
string DriverName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique ids of the gamepads connected.
|
||||
/// </summary>
|
||||
ReadOnlySpan<string> GamepadsIds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a gamepad is connected.
|
||||
/// </summary>
|
||||
event Action<string> OnGamepadConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a gamepad is disconnected.
|
||||
/// </summary>
|
||||
event Action<string> OnGamepadDisconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Open a gampad by its unique id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique id of the gamepad</param>
|
||||
/// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
|
||||
IGamepad GetGamepad(string id);
|
||||
}
|
||||
}
|
41
src/Ryujinx.Input/IKeyboard.cs
Normal file
41
src/Ryujinx.Input/IKeyboard.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent an emulated keyboard.
|
||||
/// </summary>
|
||||
public interface IKeyboard : IGamepad
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if a given key is pressed on the keyboard.
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <returns>True if the given key is pressed on the keyboard</returns>
|
||||
bool IsPressed(Key key);
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of the keyboard.
|
||||
/// </summary>
|
||||
/// <returns>A snaphost of the state of the keyboard.</returns>
|
||||
KeyboardStateSnapshot GetKeyboardStateSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of a keyboard.
|
||||
/// </summary>
|
||||
/// <param name="keyboard">The keyboard to do a snapshot of</param>
|
||||
/// <returns>A snaphost of the state of the keyboard.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard)
|
||||
{
|
||||
bool[] keysState = new bool[(int)Key.Count];
|
||||
|
||||
for (Key key = 0; key < Key.Count; key++)
|
||||
{
|
||||
keysState[(int)key] = keyboard.IsPressed(key);
|
||||
}
|
||||
|
||||
return new KeyboardStateSnapshot(keysState);
|
||||
}
|
||||
}
|
||||
}
|
104
src/Ryujinx.Input/IMouse.cs
Normal file
104
src/Ryujinx.Input/IMouse.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent an emulated mouse.
|
||||
/// </summary>
|
||||
public interface IMouse : IGamepad
|
||||
{
|
||||
private const int SwitchPanelWidth = 1280;
|
||||
private const int SwitchPanelHeight = 720;
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given button is pressed on the mouse.
|
||||
/// </summary>
|
||||
/// <param name="button">The button</param>
|
||||
/// <returns>True if the given button is pressed on the mouse</returns>
|
||||
bool IsButtonPressed(MouseButton button);
|
||||
|
||||
/// <summary>
|
||||
/// Get the position of the mouse in the client.
|
||||
/// </summary>
|
||||
Vector2 GetPosition();
|
||||
|
||||
/// <summary>
|
||||
/// Get the mouse scroll delta.
|
||||
/// </summary>
|
||||
Vector2 GetScroll();
|
||||
|
||||
/// <summary>
|
||||
/// Get the client size.
|
||||
/// </summary>
|
||||
Size ClientSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the button states of the mouse.
|
||||
/// </summary>
|
||||
bool[] Buttons { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of a mouse.
|
||||
/// </summary>
|
||||
/// <param name="mouse">The mouse to do a snapshot of</param>
|
||||
/// <returns>A snaphost of the state of the mouse.</returns>
|
||||
public static MouseStateSnapshot GetMouseStateSnapshot(IMouse mouse)
|
||||
{
|
||||
bool[] buttons = new bool[(int)MouseButton.Count];
|
||||
|
||||
mouse.Buttons.CopyTo(buttons, 0);
|
||||
|
||||
return new MouseStateSnapshot(buttons, mouse.GetPosition(), mouse.GetScroll());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the position of a mouse on screen relative to the app's view
|
||||
/// </summary>
|
||||
/// <param name="mousePosition">The position of the mouse in the client</param>
|
||||
/// <param name="clientSize">The size of the client</param>
|
||||
/// <param name="aspectRatio">The aspect ratio of the view</param>
|
||||
/// <returns>A snaphost of the state of the mouse.</returns>
|
||||
public static Vector2 GetScreenPosition(Vector2 mousePosition, Size clientSize, float aspectRatio)
|
||||
{
|
||||
float mouseX = mousePosition.X;
|
||||
float mouseY = mousePosition.Y;
|
||||
|
||||
float aspectWidth = SwitchPanelHeight * aspectRatio;
|
||||
|
||||
int screenWidth = clientSize.Width;
|
||||
int screenHeight = clientSize.Height;
|
||||
|
||||
if (clientSize.Width > clientSize.Height * aspectWidth / SwitchPanelHeight)
|
||||
{
|
||||
screenWidth = (int)(clientSize.Height * aspectWidth) / SwitchPanelHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
screenHeight = (clientSize.Width * SwitchPanelHeight) / (int)aspectWidth;
|
||||
}
|
||||
|
||||
int startX = (clientSize.Width - screenWidth) >> 1;
|
||||
int startY = (clientSize.Height - screenHeight) >> 1;
|
||||
|
||||
int endX = startX + screenWidth;
|
||||
int endY = startY + screenHeight;
|
||||
|
||||
if (mouseX >= startX &&
|
||||
mouseY >= startY &&
|
||||
mouseX < endX &&
|
||||
mouseY < endY)
|
||||
{
|
||||
int screenMouseX = (int)mouseX - startX;
|
||||
int screenMouseY = (int)mouseY - startY;
|
||||
|
||||
mouseX = (screenMouseX * (int)aspectWidth) / screenWidth;
|
||||
mouseY = (screenMouseY * SwitchPanelHeight) / screenHeight;
|
||||
|
||||
return new Vector2(mouseX, mouseY);
|
||||
}
|
||||
|
||||
return new Vector2();
|
||||
}
|
||||
}
|
||||
}
|
142
src/Ryujinx.Input/Key.cs
Normal file
142
src/Ryujinx.Input/Key.cs
Normal file
@ -0,0 +1,142 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a key from a keyboard.
|
||||
/// </summary>
|
||||
public enum Key
|
||||
{
|
||||
Unknown,
|
||||
ShiftLeft,
|
||||
ShiftRight,
|
||||
ControlLeft,
|
||||
ControlRight,
|
||||
AltLeft,
|
||||
AltRight,
|
||||
WinLeft,
|
||||
WinRight,
|
||||
Menu,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
F21,
|
||||
F22,
|
||||
F23,
|
||||
F24,
|
||||
F25,
|
||||
F26,
|
||||
F27,
|
||||
F28,
|
||||
F29,
|
||||
F30,
|
||||
F31,
|
||||
F32,
|
||||
F33,
|
||||
F34,
|
||||
F35,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Enter,
|
||||
Escape,
|
||||
Space,
|
||||
Tab,
|
||||
BackSpace,
|
||||
Insert,
|
||||
Delete,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Home,
|
||||
End,
|
||||
CapsLock,
|
||||
ScrollLock,
|
||||
PrintScreen,
|
||||
Pause,
|
||||
NumLock,
|
||||
Clear,
|
||||
Keypad0,
|
||||
Keypad1,
|
||||
Keypad2,
|
||||
Keypad3,
|
||||
Keypad4,
|
||||
Keypad5,
|
||||
Keypad6,
|
||||
Keypad7,
|
||||
Keypad8,
|
||||
Keypad9,
|
||||
KeypadDivide,
|
||||
KeypadMultiply,
|
||||
KeypadSubtract,
|
||||
KeypadAdd,
|
||||
KeypadDecimal,
|
||||
KeypadEnter,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G,
|
||||
H,
|
||||
I,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
M,
|
||||
N,
|
||||
O,
|
||||
P,
|
||||
Q,
|
||||
R,
|
||||
S,
|
||||
T,
|
||||
U,
|
||||
V,
|
||||
W,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
Number0,
|
||||
Number1,
|
||||
Number2,
|
||||
Number3,
|
||||
Number4,
|
||||
Number5,
|
||||
Number6,
|
||||
Number7,
|
||||
Number8,
|
||||
Number9,
|
||||
Tilde,
|
||||
Grave,
|
||||
Minus,
|
||||
Plus,
|
||||
BracketLeft,
|
||||
BracketRight,
|
||||
Semicolon,
|
||||
Quote,
|
||||
Comma,
|
||||
Period,
|
||||
Slash,
|
||||
BackSlash,
|
||||
Unbound,
|
||||
|
||||
Count
|
||||
}
|
||||
}
|
29
src/Ryujinx.Input/KeyboardStateSnapshot.cs
Normal file
29
src/Ryujinx.Input/KeyboardStateSnapshot.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A snapshot of a <see cref="IKeyboard"/>.
|
||||
/// </summary>
|
||||
public class KeyboardStateSnapshot
|
||||
{
|
||||
private bool[] _keysState;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="KeyboardStateSnapshot"/>.
|
||||
/// </summary>
|
||||
/// <param name="keysState">The keys state</param>
|
||||
public KeyboardStateSnapshot(bool[] keysState)
|
||||
{
|
||||
_keysState = keysState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given key is pressed.
|
||||
/// </summary>
|
||||
/// <param name="key">The key</param>
|
||||
/// <returns>True if the given key is pressed</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsPressed(Key key) => _keysState[(int)key];
|
||||
}
|
||||
}
|
475
src/Ryujinx.Input/Motion/CemuHook/Client.cs
Normal file
475
src/Ryujinx.Input/Motion/CemuHook/Client.cs
Normal file
@ -0,0 +1,475 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.Input.Motion.CemuHook.Protocol;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Hashing;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Input.Motion.CemuHook
|
||||
{
|
||||
public class Client : IDisposable
|
||||
{
|
||||
public const uint Magic = 0x43555344; // DSUC
|
||||
public const ushort Version = 1001;
|
||||
|
||||
private bool _active;
|
||||
|
||||
private readonly Dictionary<int, IPEndPoint> _hosts;
|
||||
private readonly Dictionary<int, Dictionary<int, MotionInput>> _motionData;
|
||||
private readonly Dictionary<int, UdpClient> _clients;
|
||||
|
||||
private readonly bool[] _clientErrorStatus = new bool[Enum.GetValues<PlayerIndex>().Length];
|
||||
private readonly long[] _clientRetryTimer = new long[Enum.GetValues<PlayerIndex>().Length];
|
||||
private NpadManager _npadManager;
|
||||
|
||||
public Client(NpadManager npadManager)
|
||||
{
|
||||
_npadManager = npadManager;
|
||||
_hosts = new Dictionary<int, IPEndPoint>();
|
||||
_motionData = new Dictionary<int, Dictionary<int, MotionInput>>();
|
||||
_clients = new Dictionary<int, UdpClient>();
|
||||
|
||||
CloseClients();
|
||||
}
|
||||
|
||||
public void CloseClients()
|
||||
{
|
||||
_active = false;
|
||||
|
||||
lock (_clients)
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Value?.Dispose();
|
||||
}
|
||||
catch (SocketException socketException)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error: {socketException.ErrorCode}");
|
||||
}
|
||||
}
|
||||
|
||||
_hosts.Clear();
|
||||
_clients.Clear();
|
||||
_motionData.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterClient(int player, string host, int port)
|
||||
{
|
||||
if (_clients.ContainsKey(player) || !CanConnect(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_clients)
|
||||
{
|
||||
if (_clients.ContainsKey(player) || !CanConnect(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UdpClient client = null;
|
||||
|
||||
try
|
||||
{
|
||||
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port);
|
||||
|
||||
client = new UdpClient(host, port);
|
||||
|
||||
_clients.Add(player, client);
|
||||
_hosts.Add(player, endPoint);
|
||||
|
||||
_active = true;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
ReceiveLoop(player);
|
||||
});
|
||||
}
|
||||
catch (FormatException formatException)
|
||||
{
|
||||
if (!_clientErrorStatus[player])
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {formatException.Message}");
|
||||
|
||||
_clientErrorStatus[player] = true;
|
||||
}
|
||||
}
|
||||
catch (SocketException socketException)
|
||||
{
|
||||
if (!_clientErrorStatus[player])
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {socketException.ErrorCode}");
|
||||
|
||||
_clientErrorStatus[player] = true;
|
||||
}
|
||||
|
||||
RemoveClient(player);
|
||||
|
||||
client?.Dispose();
|
||||
|
||||
SetRetryTimer(player);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to register motion client. Error: {exception.Message}");
|
||||
|
||||
_clientErrorStatus[player] = true;
|
||||
|
||||
RemoveClient(player);
|
||||
|
||||
client?.Dispose();
|
||||
|
||||
SetRetryTimer(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetData(int player, int slot, out MotionInput input)
|
||||
{
|
||||
lock (_motionData)
|
||||
{
|
||||
if (_motionData.ContainsKey(player))
|
||||
{
|
||||
if (_motionData[player].TryGetValue(slot, out input))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RemoveClient(int clientId)
|
||||
{
|
||||
_clients?.Remove(clientId);
|
||||
|
||||
_hosts?.Remove(clientId);
|
||||
}
|
||||
|
||||
private void Send(byte[] data, int clientId)
|
||||
{
|
||||
if (_clients.TryGetValue(clientId, out UdpClient _client))
|
||||
{
|
||||
if (_client != null && _client.Client != null && _client.Client.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
_client?.Send(data, data.Length);
|
||||
}
|
||||
catch (SocketException socketException)
|
||||
{
|
||||
if (!_clientErrorStatus[clientId])
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error: {socketException.ErrorCode}");
|
||||
}
|
||||
|
||||
_clientErrorStatus[clientId] = true;
|
||||
|
||||
RemoveClient(clientId);
|
||||
|
||||
_client?.Dispose();
|
||||
|
||||
SetRetryTimer(clientId);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
_clientErrorStatus[clientId] = true;
|
||||
|
||||
RemoveClient(clientId);
|
||||
|
||||
_client?.Dispose();
|
||||
|
||||
SetRetryTimer(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Receive(int clientId, int timeout = 0)
|
||||
{
|
||||
if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client))
|
||||
{
|
||||
if (_client != null && _client.Client != null && _client.Client.Connected)
|
||||
{
|
||||
_client.Client.ReceiveTimeout = timeout;
|
||||
|
||||
var result = _client?.Receive(ref endPoint);
|
||||
|
||||
if (result.Length > 0)
|
||||
{
|
||||
_clientErrorStatus[clientId] = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Client {clientId} is not registered.");
|
||||
}
|
||||
|
||||
private void SetRetryTimer(int clientId)
|
||||
{
|
||||
var elapsedMs = PerformanceCounter.ElapsedMilliseconds;
|
||||
|
||||
_clientRetryTimer[clientId] = elapsedMs;
|
||||
}
|
||||
|
||||
private void ResetRetryTimer(int clientId)
|
||||
{
|
||||
_clientRetryTimer[clientId] = 0;
|
||||
}
|
||||
|
||||
private bool CanConnect(int clientId)
|
||||
{
|
||||
return _clientRetryTimer[clientId] == 0 || PerformanceCounter.ElapsedMilliseconds - 5000 > _clientRetryTimer[clientId];
|
||||
}
|
||||
|
||||
public void ReceiveLoop(int clientId)
|
||||
{
|
||||
if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client))
|
||||
{
|
||||
if (_client != null && _client.Client != null && _client.Client.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_active)
|
||||
{
|
||||
byte[] data = Receive(clientId);
|
||||
|
||||
if (data.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Task.Run(() => HandleResponse(data, clientId));
|
||||
}
|
||||
}
|
||||
catch (SocketException socketException)
|
||||
{
|
||||
if (!_clientErrorStatus[clientId])
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error: {socketException.ErrorCode}");
|
||||
}
|
||||
|
||||
_clientErrorStatus[clientId] = true;
|
||||
|
||||
RemoveClient(clientId);
|
||||
|
||||
_client?.Dispose();
|
||||
|
||||
SetRetryTimer(clientId);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
_clientErrorStatus[clientId] = true;
|
||||
|
||||
RemoveClient(clientId);
|
||||
|
||||
_client?.Dispose();
|
||||
|
||||
SetRetryTimer(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleResponse(byte[] data, int clientId)
|
||||
{
|
||||
ResetRetryTimer(clientId);
|
||||
|
||||
MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4));
|
||||
|
||||
data = data.AsSpan()[16..].ToArray();
|
||||
|
||||
using MemoryStream stream = new MemoryStream(data);
|
||||
using BinaryReader reader = new BinaryReader(stream);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MessageType.Protocol:
|
||||
break;
|
||||
case MessageType.Info:
|
||||
ControllerInfoResponse contollerInfo = reader.ReadStruct<ControllerInfoResponse>();
|
||||
break;
|
||||
case MessageType.Data:
|
||||
ControllerDataResponse inputData = reader.ReadStruct<ControllerDataResponse>();
|
||||
|
||||
Vector3 accelerometer = new Vector3()
|
||||
{
|
||||
X = -inputData.AccelerometerX,
|
||||
Y = inputData.AccelerometerZ,
|
||||
Z = -inputData.AccelerometerY
|
||||
};
|
||||
|
||||
Vector3 gyroscrope = new Vector3()
|
||||
{
|
||||
X = inputData.GyroscopePitch,
|
||||
Y = inputData.GyroscopeRoll,
|
||||
Z = -inputData.GyroscopeYaw
|
||||
};
|
||||
|
||||
ulong timestamp = inputData.MotionTimestamp;
|
||||
|
||||
InputConfig config = _npadManager.GetPlayerInputConfigByIndex(clientId);
|
||||
|
||||
lock (_motionData)
|
||||
{
|
||||
// Sanity check the configuration state and remove client if needed if needed.
|
||||
if (config is StandardControllerInputConfig controllerConfig &&
|
||||
controllerConfig.Motion.EnableMotion &&
|
||||
controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook &&
|
||||
controllerConfig.Motion is CemuHookMotionConfigController cemuHookConfig)
|
||||
{
|
||||
int slot = inputData.Shared.Slot;
|
||||
|
||||
if (_motionData.ContainsKey(clientId))
|
||||
{
|
||||
if (_motionData[clientId].ContainsKey(slot))
|
||||
{
|
||||
MotionInput previousData = _motionData[clientId][slot];
|
||||
|
||||
previousData.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
|
||||
}
|
||||
else
|
||||
{
|
||||
MotionInput input = new MotionInput();
|
||||
|
||||
input.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
|
||||
|
||||
_motionData[clientId].Add(slot, input);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MotionInput input = new MotionInput();
|
||||
|
||||
input.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
|
||||
|
||||
_motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveClient(clientId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestInfo(int clientId, int slot)
|
||||
{
|
||||
if (!_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Header header = GenerateHeader(clientId);
|
||||
|
||||
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.WriteStruct(header);
|
||||
|
||||
ControllerInfoRequest request = new ControllerInfoRequest()
|
||||
{
|
||||
Type = MessageType.Info,
|
||||
PortsCount = 4
|
||||
};
|
||||
|
||||
request.PortIndices[0] = (byte)slot;
|
||||
|
||||
writer.WriteStruct(request);
|
||||
|
||||
header.Length = (ushort)(stream.Length - 16);
|
||||
|
||||
writer.Seek(6, SeekOrigin.Begin);
|
||||
writer.Write(header.Length);
|
||||
|
||||
Crc32.Hash(stream.ToArray(), header.Crc32.AsSpan());
|
||||
|
||||
writer.Seek(8, SeekOrigin.Begin);
|
||||
writer.Write(header.Crc32.AsSpan());
|
||||
|
||||
byte[] data = stream.ToArray();
|
||||
|
||||
Send(data, clientId);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void RequestData(int clientId, int slot)
|
||||
{
|
||||
if (!_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Header header = GenerateHeader(clientId);
|
||||
|
||||
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.WriteStruct(header);
|
||||
|
||||
ControllerDataRequest request = new ControllerDataRequest()
|
||||
{
|
||||
Type = MessageType.Data,
|
||||
Slot = (byte)slot,
|
||||
SubscriberType = SubscriberType.Slot
|
||||
};
|
||||
|
||||
writer.WriteStruct(request);
|
||||
|
||||
header.Length = (ushort)(stream.Length - 16);
|
||||
|
||||
writer.Seek(6, SeekOrigin.Begin);
|
||||
writer.Write(header.Length);
|
||||
|
||||
Crc32.Hash(stream.ToArray(), header.Crc32.AsSpan());
|
||||
|
||||
writer.Seek(8, SeekOrigin.Begin);
|
||||
writer.Write(header.Crc32.AsSpan());
|
||||
|
||||
byte[] data = stream.ToArray();
|
||||
|
||||
Send(data, clientId);
|
||||
}
|
||||
}
|
||||
|
||||
private Header GenerateHeader(int clientId)
|
||||
{
|
||||
Header header = new Header()
|
||||
{
|
||||
Id = (uint)clientId,
|
||||
MagicString = Magic,
|
||||
Version = Version,
|
||||
Length = 0
|
||||
};
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_active = false;
|
||||
|
||||
CloseClients();
|
||||
}
|
||||
}
|
||||
}
|
47
src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs
Normal file
47
src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Input.Motion.CemuHook.Protocol
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
struct ControllerDataRequest
|
||||
{
|
||||
public MessageType Type;
|
||||
public SubscriberType SubscriberType;
|
||||
public byte Slot;
|
||||
public Array6<byte> MacAddress;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ControllerDataResponse
|
||||
{
|
||||
public SharedResponse Shared;
|
||||
public byte Connected;
|
||||
public uint PacketId;
|
||||
public byte ExtraButtons;
|
||||
public byte MainButtons;
|
||||
public ushort PSExtraInput;
|
||||
public ushort LeftStickXY;
|
||||
public ushort RightStickXY;
|
||||
public uint DPadAnalog;
|
||||
public ulong MainButtonsAnalog;
|
||||
|
||||
public Array6<byte> Touch1;
|
||||
public Array6<byte> Touch2;
|
||||
|
||||
public ulong MotionTimestamp;
|
||||
public float AccelerometerX;
|
||||
public float AccelerometerY;
|
||||
public float AccelerometerZ;
|
||||
public float GyroscopePitch;
|
||||
public float GyroscopeYaw;
|
||||
public float GyroscopeRoll;
|
||||
}
|
||||
|
||||
enum SubscriberType : byte
|
||||
{
|
||||
All,
|
||||
Slot,
|
||||
Mac
|
||||
}
|
||||
}
|
20
src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs
Normal file
20
src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Input.Motion.CemuHook.Protocol
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ControllerInfoResponse
|
||||
{
|
||||
public SharedResponse Shared;
|
||||
private byte _zero;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct ControllerInfoRequest
|
||||
{
|
||||
public MessageType Type;
|
||||
public int PortsCount;
|
||||
public Array4<byte> PortIndices;
|
||||
}
|
||||
}
|
15
src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs
Normal file
15
src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Input.Motion.CemuHook.Protocol
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct Header
|
||||
{
|
||||
public uint MagicString;
|
||||
public ushort Version;
|
||||
public ushort Length;
|
||||
public Array4<byte> Crc32;
|
||||
public uint Id;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace Ryujinx.Input.Motion.CemuHook.Protocol
|
||||
{
|
||||
public enum MessageType : uint
|
||||
{
|
||||
Protocol = 0x100000,
|
||||
Info,
|
||||
Data
|
||||
}
|
||||
}
|
51
src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs
Normal file
51
src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Input.Motion.CemuHook.Protocol
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct SharedResponse
|
||||
{
|
||||
public MessageType Type;
|
||||
public byte Slot;
|
||||
public SlotState State;
|
||||
public DeviceModelType ModelType;
|
||||
public ConnectionType ConnectionType;
|
||||
|
||||
public Array6<byte> MacAddress;
|
||||
public BatteryStatus BatteryStatus;
|
||||
}
|
||||
|
||||
public enum SlotState : byte
|
||||
{
|
||||
Disconnected,
|
||||
Reserved,
|
||||
Connected
|
||||
}
|
||||
|
||||
public enum DeviceModelType : byte
|
||||
{
|
||||
None,
|
||||
PartialGyro,
|
||||
FullGyro
|
||||
}
|
||||
|
||||
public enum ConnectionType : byte
|
||||
{
|
||||
None,
|
||||
USB,
|
||||
Bluetooth
|
||||
}
|
||||
|
||||
public enum BatteryStatus : byte
|
||||
{
|
||||
NA,
|
||||
Dying,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Full,
|
||||
Charging,
|
||||
Charged
|
||||
}
|
||||
}
|
65
src/Ryujinx.Input/Motion/MotionInput.cs
Normal file
65
src/Ryujinx.Input/Motion/MotionInput.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using Ryujinx.Input.Motion;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
public class MotionInput
|
||||
{
|
||||
public ulong TimeStamp { get; set; }
|
||||
public Vector3 Accelerometer { get; set; }
|
||||
public Vector3 Gyroscrope { get; set; }
|
||||
public Vector3 Rotation { get; set; }
|
||||
|
||||
private readonly MotionSensorFilter _filter;
|
||||
|
||||
public MotionInput()
|
||||
{
|
||||
TimeStamp = 0;
|
||||
Accelerometer = new Vector3();
|
||||
Gyroscrope = new Vector3();
|
||||
Rotation = new Vector3();
|
||||
|
||||
// TODO: RE the correct filter.
|
||||
_filter = new MotionSensorFilter(0f);
|
||||
}
|
||||
|
||||
public void Update(Vector3 accel, Vector3 gyro, ulong timestamp, int sensitivity, float deadzone)
|
||||
{
|
||||
if (TimeStamp != 0)
|
||||
{
|
||||
Accelerometer = -accel;
|
||||
|
||||
if (gyro.Length() < deadzone)
|
||||
{
|
||||
gyro = Vector3.Zero;
|
||||
}
|
||||
|
||||
gyro *= (sensitivity / 100f);
|
||||
|
||||
Gyroscrope = gyro;
|
||||
|
||||
float deltaTime = MathF.Abs((long)(timestamp - TimeStamp) / 1000000f);
|
||||
|
||||
Vector3 deltaGyro = gyro * deltaTime;
|
||||
|
||||
Rotation += deltaGyro;
|
||||
|
||||
_filter.SamplePeriod = deltaTime;
|
||||
_filter.Update(accel, DegreeToRad(gyro));
|
||||
}
|
||||
|
||||
TimeStamp = timestamp;
|
||||
}
|
||||
|
||||
public Matrix4x4 GetOrientation()
|
||||
{
|
||||
return Matrix4x4.CreateFromQuaternion(_filter.Quaternion);
|
||||
}
|
||||
|
||||
private static Vector3 DegreeToRad(Vector3 degree)
|
||||
{
|
||||
return degree * (MathF.PI / 180);
|
||||
}
|
||||
}
|
||||
}
|
162
src/Ryujinx.Input/Motion/MotionSensorFilter.cs
Normal file
162
src/Ryujinx.Input/Motion/MotionSensorFilter.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Input.Motion
|
||||
{
|
||||
// MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm.
|
||||
// See: https://x-io.co.uk/open-source-imu-and-ahrs-algorithms/
|
||||
// Based on: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs
|
||||
class MotionSensorFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Sample rate coefficient.
|
||||
/// </summary>
|
||||
public const float SampleRateCoefficient = 0.45f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample period.
|
||||
/// </summary>
|
||||
public float SamplePeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the algorithm proportional gain.
|
||||
/// </summary>
|
||||
public float Kp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the algorithm integral gain.
|
||||
/// </summary>
|
||||
public float Ki { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Quaternion output.
|
||||
/// </summary>
|
||||
public Quaternion Quaternion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Integral error.
|
||||
/// </summary>
|
||||
private Vector3 _intergralError;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="samplePeriod">
|
||||
/// Sample period.
|
||||
/// </param>
|
||||
public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="samplePeriod">
|
||||
/// Sample period.
|
||||
/// </param>
|
||||
/// <param name="kp">
|
||||
/// Algorithm proportional gain.
|
||||
/// </param>
|
||||
public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="samplePeriod">
|
||||
/// Sample period.
|
||||
/// </param>
|
||||
/// <param name="kp">
|
||||
/// Algorithm proportional gain.
|
||||
/// </param>
|
||||
/// <param name="ki">
|
||||
/// Algorithm integral gain.
|
||||
/// </param>
|
||||
public MotionSensorFilter(float samplePeriod, float kp, float ki)
|
||||
{
|
||||
SamplePeriod = samplePeriod;
|
||||
Kp = kp;
|
||||
Ki = ki;
|
||||
|
||||
Reset();
|
||||
|
||||
_intergralError = new Vector3();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm IMU update method. Requires only gyroscope and accelerometer data.
|
||||
/// </summary>
|
||||
/// <param name="accel">
|
||||
/// Accelerometer measurement in any calibrated units.
|
||||
/// </param>
|
||||
/// <param name="gyro">
|
||||
/// Gyroscope measurement in radians.
|
||||
/// </param>
|
||||
public void Update(Vector3 accel, Vector3 gyro)
|
||||
{
|
||||
// Normalise accelerometer measurement.
|
||||
float norm = 1f / accel.Length();
|
||||
|
||||
if (!float.IsFinite(norm))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
accel *= norm;
|
||||
|
||||
float q2 = Quaternion.X;
|
||||
float q3 = Quaternion.Y;
|
||||
float q4 = Quaternion.Z;
|
||||
float q1 = Quaternion.W;
|
||||
|
||||
// Estimated direction of gravity.
|
||||
Vector3 gravity = new Vector3()
|
||||
{
|
||||
X = 2f * (q2 * q4 - q1 * q3),
|
||||
Y = 2f * (q1 * q2 + q3 * q4),
|
||||
Z = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4
|
||||
};
|
||||
|
||||
// Error is cross product between estimated direction and measured direction of gravity.
|
||||
Vector3 error = new Vector3()
|
||||
{
|
||||
X = accel.Y * gravity.Z - accel.Z * gravity.Y,
|
||||
Y = accel.Z * gravity.X - accel.X * gravity.Z,
|
||||
Z = accel.X * gravity.Y - accel.Y * gravity.X
|
||||
};
|
||||
|
||||
if (Ki > 0f)
|
||||
{
|
||||
_intergralError += error; // Accumulate integral error.
|
||||
}
|
||||
else
|
||||
{
|
||||
_intergralError = Vector3.Zero; // Prevent integral wind up.
|
||||
}
|
||||
|
||||
// Apply feedback terms.
|
||||
gyro += (Kp * error) + (Ki * _intergralError);
|
||||
|
||||
// Integrate rate of change of quaternion.
|
||||
Vector3 delta = new Vector3(q2, q3, q4);
|
||||
|
||||
q1 += (-q2 * gyro.X - q3 * gyro.Y - q4 * gyro.Z) * (SampleRateCoefficient * SamplePeriod);
|
||||
q2 += (q1 * gyro.X + delta.Y * gyro.Z - delta.Z * gyro.Y) * (SampleRateCoefficient * SamplePeriod);
|
||||
q3 += (q1 * gyro.Y - delta.X * gyro.Z + delta.Z * gyro.X) * (SampleRateCoefficient * SamplePeriod);
|
||||
q4 += (q1 * gyro.Z + delta.X * gyro.Y - delta.Y * gyro.X) * (SampleRateCoefficient * SamplePeriod);
|
||||
|
||||
// Normalise quaternion.
|
||||
Quaternion quaternion = new Quaternion(q2, q3, q4, q1);
|
||||
|
||||
norm = 1f / quaternion.Length();
|
||||
|
||||
if (!float.IsFinite(norm))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion = quaternion * norm;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Quaternion = Quaternion.Identity;
|
||||
}
|
||||
}
|
||||
}
|
25
src/Ryujinx.Input/MotionInputId.cs
Normal file
25
src/Ryujinx.Input/MotionInputId.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a motion sensor on a gamepad.
|
||||
/// </summary>
|
||||
public enum MotionInputId : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid.
|
||||
/// </summary>
|
||||
Invalid,
|
||||
|
||||
/// <summary>
|
||||
/// Accelerometer.
|
||||
/// </summary>
|
||||
/// <remarks>Values are in m/s^2</remarks>
|
||||
Accelerometer,
|
||||
|
||||
/// <summary>
|
||||
/// Gyroscope.
|
||||
/// </summary>
|
||||
/// <remarks>Values are in degrees</remarks>
|
||||
Gyroscope
|
||||
}
|
||||
}
|
16
src/Ryujinx.Input/MouseButton.cs
Normal file
16
src/Ryujinx.Input/MouseButton.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
public enum MouseButton : byte
|
||||
{
|
||||
Button1,
|
||||
Button2,
|
||||
Button3,
|
||||
Button4,
|
||||
Button5,
|
||||
Button6,
|
||||
Button7,
|
||||
Button8,
|
||||
Button9,
|
||||
Count
|
||||
}
|
||||
}
|
45
src/Ryujinx.Input/MouseStateSnapshot.cs
Normal file
45
src/Ryujinx.Input/MouseStateSnapshot.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A snapshot of a <see cref="IMouse"/>.
|
||||
/// </summary>
|
||||
public class MouseStateSnapshot
|
||||
{
|
||||
private bool[] _buttonState;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the mouse cursor
|
||||
/// </summary>
|
||||
public Vector2 Position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The scroll delta of the mouse
|
||||
/// </summary>
|
||||
public Vector2 Scroll { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="MouseStateSnapshot"/>.
|
||||
/// </summary>
|
||||
/// <param name="buttonState">The button state</param>
|
||||
/// <param name="position">The position of the cursor</param>
|
||||
/// <param name="scroll">The scroll delta</param>
|
||||
public MouseStateSnapshot(bool[] buttonState, Vector2 position, Vector2 scroll)
|
||||
{
|
||||
_buttonState = buttonState;
|
||||
|
||||
Position = position;
|
||||
Scroll = scroll;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given button is pressed.
|
||||
/// </summary>
|
||||
/// <param name="button">The button</param>
|
||||
/// <returns>True if the given button is pressed</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsPressed(MouseButton button) => _buttonState[(int)button];
|
||||
}
|
||||
}
|
17
src/Ryujinx.Input/Ryujinx.Input.csproj
Normal file
17
src/Ryujinx.Input/Ryujinx.Input.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Hashing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
14
src/Ryujinx.Input/StickInputId.cs
Normal file
14
src/Ryujinx.Input/StickInputId.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a joystick from a gamepad.
|
||||
/// </summary>
|
||||
public enum StickInputId : byte
|
||||
{
|
||||
Unbound,
|
||||
Left,
|
||||
Right,
|
||||
|
||||
Count
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user