5131b71437
* add RecyclableMemoryStream dependency and MemoryStreamManager * organize BinaryReader/BinaryWriter extensions * add StreamExtensions to reduce need for BinaryWriter * simple replacments of MemoryStream with RecyclableMemoryStream * add write ReadOnlySequence<byte> support to IVirtualMemoryManager * avoid 0-length array creation * rework IpcMessage and related types to greatly reduce memory allocation by using RecylableMemoryStream, keeping streams around longer, avoiding their creation when possible, and avoiding creation of BinaryReader and BinaryWriter when possible * reduce LINQ-induced memory allocations with custom methods to query KPriorityQueue * use RecyclableMemoryStream in StreamUtils, and use StreamUtils in EmbeddedResources * add constants for nanosecond/millisecond conversions * code formatting * XML doc adjustments * fix: StreamExtension.WriteByte not writing non-zero values for lengths <= 16 * XML Doc improvements. Implement StreamExtensions.WriteByte() block writes for large-enough count values. * add copyless path for StreamExtension.Write(ReadOnlySpan<int>) * add default implementation of IVirtualMemoryManager.Write(ulong, ReadOnlySequence<byte>); remove previous explicit implementations * code style fixes * remove LINQ completely from KScheduler/KPriorityQueue by implementing a custom struct-based enumerator
475 lines
No EOL
15 KiB
C#
475 lines
No EOL
15 KiB
C#
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();
|
|
}
|
|
}
|
|
} |