diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Input/Motion/CemuHook | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Input/Motion/CemuHook')
6 files changed, 617 insertions, 0 deletions
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Client.cs b/src/Ryujinx.Input/Motion/CemuHook/Client.cs new file mode 100644 index 00000000..4498b8ca --- /dev/null +++ b/src/Ryujinx.Input/Motion/CemuHook/Client.cs @@ -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(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs new file mode 100644 index 00000000..7fb72344 --- /dev/null +++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs @@ -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 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs new file mode 100644 index 00000000..63d4524a --- /dev/null +++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs @@ -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; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs new file mode 100644 index 00000000..57f58ff0 --- /dev/null +++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs @@ -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; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs new file mode 100644 index 00000000..de1e5e90 --- /dev/null +++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Input.Motion.CemuHook.Protocol +{ + public enum MessageType : uint + { + Protocol = 0x100000, + Info, + Data + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs new file mode 100644 index 00000000..e2e1ee9b --- /dev/null +++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs @@ -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 + } +}
\ No newline at end of file |
