Compare commits

...

24 Commits

Author SHA1 Message Date
Evan Husted ca91ce8b7e Merge 8a6f806dda into bc07bc482d 2025-02-16 14:18:12 +01:00
Vudjun bc07bc482d Gracefully handle errors in socket creation (#662)
In all other calls, exceptions are handled within the ManagedSocket
class, this can't easily be done here as the exception is thrown from
the constructor, so the exception is handled in ISocket and the correct
error is passed to the application.

This should fix #660
2025-02-16 02:26:37 -06:00
Vudjun 61975ca44d Add logging in socket implementation (#661)
Adds logging to most Socket calls to help with debugging network
issues.

Shouldn't affect any functionality. There's a small chance it could spam
the log in some games.
2025-02-16 01:03:44 -06:00
Evan Husted 8a6f806dda Merge branch 'master' into xeyes 2025-02-12 16:34:47 -06:00
Evan Husted 15d589c455 Merge branch 'master' into xeyes 2025-02-11 22:55:43 -06:00
Evan Husted 3262020e18 Merge branch 'master' into xeyes 2025-02-07 21:35:51 -06:00
Evan Husted a3afebd3a2 Merge branch 'master' into xeyes 2025-02-05 14:59:03 -06:00
Evan Husted 24e88e2485 Merge branch 'master' into xeyes 2025-01-29 02:48:51 -06:00
Evan Husted 3b447b764e Merge branch 'master' into xeyes 2025-01-28 01:45:49 -06:00
Evan Husted 849fd0199e Merge branch 'master' into xeyes 2025-01-26 17:33:58 -06:00
Evan Husted 57e26114e8 Merge branch 'master' into xeyes 2025-01-25 21:56:40 -06:00
Evan Husted 6a283190b3 Add the controller image back 2025-01-24 22:24:02 -06:00
MutantAura 85547874c8 Consolidate most logic into StickVisualizer. 2025-01-24 20:08:58 -06:00
MutantAura 16ca8e5005 Move most logic into new StickVisualizer class and add keyboard support. 2025-01-24 20:01:36 -06:00
MutantAura cfa5ad0757 Simplfy clamping and fix bug on window close. 2025-01-24 19:58:17 -06:00
MutantAura 43ece083b2 Move some backing fields to match others.
formatting pt.2
2025-01-24 19:57:11 -06:00
MutantAura e1f5c501b0 Fix some issues with stick magnitude. 2025-01-24 19:56:21 -06:00
MutantAura ffe366d953 Cleanup AXAML and hide sticks when only one is present on guest controller. 2025-01-24 19:52:57 -06:00
MutantAura 75c7a29278 style and analyzers 2025-01-24 19:52:52 -06:00
MutantAura 3a0d9c1435 whitespace 2025-01-24 19:52:47 -06:00
MutantAura 93d1476a2a Remove unused grid definitions. 2025-01-24 19:52:39 -06:00
MutantAura ce13830063 Clean up some magic numbers between code and axaml. 2025-01-24 19:52:24 -06:00
MutantAura aa3f2824e0 Polish the aesthetic and include deadzone visualization. 2025-01-24 19:51:44 -06:00
MutantAura d2bb580aea Initial implementation of analog stick visualization. 2025-01-24 19:50:09 -06:00
10 changed files with 619 additions and 36 deletions
@@ -34,6 +34,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
{ {
if (errorCode != LinuxError.SUCCESS) if (errorCode != LinuxError.SUCCESS)
{ {
if (errorCode != LinuxError.EWOULDBLOCK)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Operation failed with error {errorCode}.");
}
result = -1; result = -1;
} }
@@ -66,6 +70,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
BsdSocketType type = (BsdSocketType)context.RequestData.ReadInt32(); BsdSocketType type = (BsdSocketType)context.RequestData.ReadInt32();
ProtocolType protocol = (ProtocolType)context.RequestData.ReadInt32(); ProtocolType protocol = (ProtocolType)context.RequestData.ReadInt32();
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Creating socket with domain={domain}, type={type}, protocol={protocol}");
BsdSocketCreationFlags creationFlags = (BsdSocketCreationFlags)((int)type >> (int)BsdSocketCreationFlags.FlagsShift); BsdSocketCreationFlags creationFlags = (BsdSocketCreationFlags)((int)type >> (int)BsdSocketCreationFlags.FlagsShift);
type &= BsdSocketType.TypeMask; type &= BsdSocketType.TypeMask;
@@ -95,12 +101,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
} }
} }
ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId)
{
Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking),
};
LinuxError errno = LinuxError.SUCCESS; LinuxError errno = LinuxError.SUCCESS;
ISocket newBsdSocket;
try
{
newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId)
{
Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking),
};
}
catch (SocketException exception)
{
LinuxError errNo = WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
return WriteBsdResult(context, 0, errNo);
}
int newSockFd = _context.RegisterFileDescriptor(newBsdSocket); int newSockFd = _context.RegisterFileDescriptor(newBsdSocket);
@@ -111,6 +126,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
if (exempt) if (exempt)
{ {
Logger.Info?.Print(LogClass.ServiceBsd, "Disconnecting exempt socket.");
newBsdSocket.Disconnect(); newBsdSocket.Disconnect();
} }
@@ -797,7 +813,11 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
{ {
errno = socket.Listen(backlog); errno = socket.Listen(backlog);
} }
else
{
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, $"Invalid socket fd '{socketFd}'.");
}
return WriteBsdResult(context, 0, errno); return WriteBsdResult(context, 0, errno);
} }
@@ -92,18 +92,30 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{ {
newSocket = new ManagedSocket(Socket.Accept()); newSocket = new ManagedSocket(Socket.Accept());
IPEndPoint remoteEndPoint = newSocket.RemoteEndPoint;
bool isPrivateIp = remoteEndPoint.Address.ToString().StartsWith("192.168.");
Logger.Info?.PrintMsg(LogClass.ServiceBsd,
isPrivateIp
? $"Accepted connection from {ProtocolType}/{remoteEndPoint.Address}:{remoteEndPoint.Port}"
: $"Accepted connection from {ProtocolType}/***:{remoteEndPoint.Port}");
return LinuxError.SUCCESS; return LinuxError.SUCCESS;
} }
catch (SocketException exception) catch (SocketException exception)
{ {
newSocket = null; newSocket = null;
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
public LinuxError Bind(IPEndPoint localEndPoint) public LinuxError Bind(IPEndPoint localEndPoint)
{ {
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Socket binding to: {ProtocolType}/{localEndPoint.Port}");
try try
{ {
Socket.Bind(localEndPoint); Socket.Bind(localEndPoint);
@@ -112,6 +124,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -123,6 +139,15 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public LinuxError Connect(IPEndPoint remoteEndPoint) public LinuxError Connect(IPEndPoint remoteEndPoint)
{ {
bool isLDNPrivateIP = remoteEndPoint.Address.ToString().StartsWith("192.168.");
if (isLDNPrivateIP)
{
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Connecting to: {ProtocolType}/{remoteEndPoint.Address}:{remoteEndPoint.Port}");
}
else
{
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Connecting to: {ProtocolType}/***:{remoteEndPoint.Port}");
}
try try
{ {
Socket.Connect(remoteEndPoint); Socket.Connect(remoteEndPoint);
@@ -137,6 +162,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
else else
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -144,11 +173,13 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public void Disconnect() public void Disconnect()
{ {
Logger.Info?.Print(LogClass.ServiceBsd, "Socket disconnecting");
Socket.Disconnect(true); Socket.Disconnect(true);
} }
public void Dispose() public void Dispose()
{ {
Logger.Info?.Print(LogClass.ServiceBsd, "Socket closed");
Socket.Close(); Socket.Close();
Socket.Dispose(); Socket.Dispose();
} }
@@ -159,10 +190,16 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{ {
Socket.Listen(backlog); Socket.Listen(backlog);
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Socket listening: {ProtocolType}/{(Socket.LocalEndPoint as IPEndPoint).Port}");
return LinuxError.SUCCESS; return LinuxError.SUCCESS;
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -182,11 +219,15 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
bool hasEmittedBlockingWarning = false; private bool _hasEmittedBlockingWarning;
public LinuxError Receive(out int receiveSize, Span<byte> buffer, BsdSocketFlags flags) public LinuxError Receive(out int receiveSize, Span<byte> buffer, BsdSocketFlags flags)
{ {
@@ -202,10 +243,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
shouldBlockAfterOperation = true; shouldBlockAfterOperation = true;
} }
if (Blocking && !hasEmittedBlockingWarning) if (Blocking && !_hasEmittedBlockingWarning)
{ {
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
hasEmittedBlockingWarning = true; _hasEmittedBlockingWarning = true;
} }
receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags)); receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags));
@@ -214,6 +255,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
receiveSize = -1; receiveSize = -1;
result = WinSockHelper.ConvertError((WsaError)exception.ErrorCode); result = WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
@@ -245,10 +290,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
shouldBlockAfterOperation = true; shouldBlockAfterOperation = true;
} }
if (Blocking && !hasEmittedBlockingWarning) if (Blocking && !_hasEmittedBlockingWarning)
{ {
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
hasEmittedBlockingWarning = true; _hasEmittedBlockingWarning = true;
} }
if (!Socket.IsBound) if (!Socket.IsBound)
@@ -265,6 +310,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
receiveSize = -1; receiveSize = -1;
result = WinSockHelper.ConvertError((WsaError)exception.ErrorCode); result = WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
@@ -288,6 +337,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
sendSize = -1; sendSize = -1;
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
@@ -304,6 +357,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
sendSize = -1; sendSize = -1;
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
@@ -341,6 +398,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -387,6 +448,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -519,6 +584,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -557,6 +626,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
} }
catch (SocketException exception) catch (SocketException exception)
{ {
if (exception.SocketErrorCode != SocketError.WouldBlock)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
}
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode); return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
} }
} }
@@ -1,4 +1,6 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -64,10 +66,18 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
{ {
if (_proxy.Supported(domain, type, protocol)) if (_proxy.Supported(domain, type, protocol))
{ {
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Socket is using LDN proxy");
return new LdnProxySocket(domain, type, protocol, _proxy); return new LdnProxySocket(domain, type, protocol, _proxy);
} }
else
{
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, $"LDN proxy does not support socket {domain}, {type}, {protocol}");
}
}
else
{
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Opening socket using host networking stack");
} }
return new DefaultSocket(domain, type, protocol, lanInterfaceId); return new DefaultSocket(domain, type, protocol, lanInterfaceId);
} }
} }
@@ -0,0 +1,260 @@
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Input;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Models.Input
{
public class StickVisualizer : BaseModel, IDisposable
{
public const int DrawStickPollRate = 50; // Milliseconds per poll.
public const int DrawStickCircumference = 5;
public const float DrawStickScaleFactor = DrawStickCanvasCenter;
public const int DrawStickCanvasSize = 100;
public const int DrawStickBorderSize = DrawStickCanvasSize + 5;
public const float DrawStickCanvasCenter = (DrawStickCanvasSize - DrawStickCircumference) / 2;
public const float MaxVectorLength = DrawStickCanvasSize / 2;
public CancellationTokenSource PollTokenSource;
public CancellationToken PollToken;
private static float _vectorLength;
private static float _vectorMultiplier;
private bool disposedValue;
private DeviceType _type;
public DeviceType Type
{
get => _type;
set
{
_type = value;
OnPropertyChanged();
}
}
private GamepadInputConfig _gamepadConfig;
public GamepadInputConfig GamepadConfig
{
get => _gamepadConfig;
set
{
_gamepadConfig = value;
OnPropertyChanged();
}
}
private KeyboardInputConfig _keyboardConfig;
public KeyboardInputConfig KeyboardConfig
{
get => _keyboardConfig;
set
{
_keyboardConfig = value;
OnPropertyChanged();
}
}
private (float, float) _uiStickLeft;
public (float, float) UiStickLeft
{
get => (_uiStickLeft.Item1 * DrawStickScaleFactor, _uiStickLeft.Item2 * DrawStickScaleFactor);
set
{
_uiStickLeft = value;
OnPropertyChanged();
OnPropertyChanged(nameof(UiStickRightX));
OnPropertyChanged(nameof(UiStickRightY));
OnPropertyChanged(nameof(UiDeadzoneRight));
}
}
private (float, float) _uiStickRight;
public (float, float) UiStickRight
{
get => (_uiStickRight.Item1 * DrawStickScaleFactor, _uiStickRight.Item2 * DrawStickScaleFactor);
set
{
_uiStickRight = value;
OnPropertyChanged();
OnPropertyChanged(nameof(UiStickLeftX));
OnPropertyChanged(nameof(UiStickLeftY));
OnPropertyChanged(nameof(UiDeadzoneLeft));
}
}
public float UiStickLeftX => ClampVector(UiStickLeft).Item1;
public float UiStickLeftY => ClampVector(UiStickLeft).Item2;
public float UiStickRightX => ClampVector(UiStickRight).Item1;
public float UiStickRightY => ClampVector(UiStickRight).Item2;
public int UiStickCircumference => DrawStickCircumference;
public int UiCanvasSize => DrawStickCanvasSize;
public int UiStickBorderSize => DrawStickBorderSize;
public float? UiDeadzoneLeft => _gamepadConfig?.DeadzoneLeft * DrawStickCanvasSize - DrawStickCircumference;
public float? UiDeadzoneRight => _gamepadConfig?.DeadzoneRight * DrawStickCanvasSize - DrawStickCircumference;
private InputViewModel Parent;
public StickVisualizer(InputViewModel parent)
{
Parent = parent;
PollTokenSource = new CancellationTokenSource();
PollToken = PollTokenSource.Token;
Task.Run(Initialize, PollToken);
}
public void UpdateConfig(object config)
{
if (config is ControllerInputViewModel padConfig)
{
GamepadConfig = padConfig.Config;
Type = DeviceType.Controller;
return;
}
else if (config is KeyboardInputViewModel keyConfig)
{
KeyboardConfig = keyConfig.Config;
Type = DeviceType.Keyboard;
return;
}
Type = DeviceType.None;
}
public async Task Initialize()
{
(float, float) leftBuffer;
(float, float) rightBuffer;
while (!PollToken.IsCancellationRequested)
{
leftBuffer = (0f, 0f);
rightBuffer = (0f, 0f);
switch (Type)
{
case DeviceType.Keyboard:
IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0");
if (keyboard != null)
{
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
{
leftBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
{
leftBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
{
leftBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
{
leftBuffer.Item2 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
{
rightBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
{
rightBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
{
rightBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
{
rightBuffer.Item2 -= 1;
}
UiStickLeft = leftBuffer;
UiStickRight = rightBuffer;
}
break;
case DeviceType.Controller:
IGamepad controller = Parent.SelectedGamepad;
if (controller != null)
{
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
}
break;
case DeviceType.None:
break;
default:
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
}
UiStickLeft = leftBuffer;
UiStickRight = rightBuffer;
await Task.Delay(DrawStickPollRate, PollToken);
}
PollTokenSource.Dispose();
}
public static (float, float) ClampVector((float, float) vect)
{
_vectorMultiplier = 1;
_vectorLength = MathF.Sqrt((vect.Item1 * vect.Item1) + (vect.Item2 * vect.Item2));
if (_vectorLength > MaxVectorLength)
{
_vectorMultiplier = MaxVectorLength / _vectorLength;
}
vect.Item1 = vect.Item1 * _vectorMultiplier + DrawStickCanvasCenter;
vect.Item2 = vect.Item2 * _vectorMultiplier + DrawStickCanvasCenter;
return vect;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
PollTokenSource.Cancel();
}
KeyboardConfig = null;
GamepadConfig = null;
Parent = null;
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
@@ -1,5 +1,9 @@
using Avalonia.Svg.Skia; using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input; using Ryujinx.Ava.UI.Views.Input;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
@@ -10,8 +14,30 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public partial class ControllerInputViewModel : BaseModel public partial class ControllerInputViewModel : BaseModel
{ {
[ObservableProperty] private GamepadInputConfig _config; private GamepadInputConfig _config;
public GamepadInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
private StickVisualizer _visualizer;
public StickVisualizer Visualizer
{
get => _visualizer;
set
{
_visualizer = value;
OnPropertyChanged();
}
}
private bool _isLeft; private bool _isLeft;
public bool IsLeft public bool IsLeft
{ {
@@ -37,14 +63,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
} }
public bool HasSides => IsLeft ^ IsRight; public bool HasSides => IsLeft ^ IsRight;
[ObservableProperty] private SvgImage _image; [ObservableProperty] private SvgImage _image;
public InputViewModel ParentModel { get; } public InputViewModel ParentModel { get; }
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config) public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
{ {
ParentModel = model; ParentModel = model;
Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged; model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged(); OnParentModelChanged();
config.PropertyChanged += (_, args) => config.PropertyChanged += (_, args) =>
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private int _controller; private int _controller;
private string _controllerImage; private string _controllerImage;
private int _device; private int _device;
[ObservableProperty] private object _configViewModel; private object _configViewModel;
[ObservableProperty] private string _profileName; [ObservableProperty] private string _profileName;
private bool _isLoaded; private bool _isLoaded;
@@ -74,6 +74,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed)); OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
} }
} }
public StickVisualizer VisualStick { get; private set; }
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; } public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; } public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
@@ -94,6 +95,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsModified { get; set; } public bool IsModified { get; set; }
public event Action NotifyChangesEvent; public event Action NotifyChangesEvent;
public object ConfigViewModel
{
get => _configViewModel;
set
{
_configViewModel = value;
VisualStick.UpdateConfig(value);
OnPropertyChanged();
}
}
public PlayerIndex PlayerIdChoose public PlayerIndex PlayerIdChoose
{ {
get => _playerIdChoose; get => _playerIdChoose;
@@ -269,6 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Devices = []; Devices = [];
ProfilesList = []; ProfilesList = [];
DeviceList = []; DeviceList = [];
VisualStick = new StickVisualizer(this);
ControllerImage = ProControllerResource; ControllerImage = ProControllerResource;
@@ -289,12 +304,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Config is StandardKeyboardInputConfig keyboardInputConfig) if (Config is StandardKeyboardInputConfig keyboardInputConfig)
{ {
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
} }
if (Config is StandardControllerInputConfig controllerInputConfig) if (Config is StandardControllerInputConfig controllerInputConfig)
{ {
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig)); ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
} }
} }
@@ -893,6 +908,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates(); _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
VisualStick.Dispose();
SelectedGamepad?.Dispose(); SelectedGamepad?.Dispose();
AvaloniaKeyboardDriver.Dispose(); AvaloniaKeyboardDriver.Dispose();
@@ -6,7 +6,29 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public partial class KeyboardInputViewModel : BaseModel public partial class KeyboardInputViewModel : BaseModel
{ {
[ObservableProperty] private KeyboardInputConfig _config; private KeyboardInputConfig _config;
public KeyboardInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
private StickVisualizer _visualizer;
public StickVisualizer Visualizer
{
get => _visualizer;
set
{
_visualizer = value;
OnPropertyChanged();
}
}
private bool _isLeft; private bool _isLeft;
public bool IsLeft public bool IsLeft
@@ -38,9 +60,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public readonly InputViewModel ParentModel; public readonly InputViewModel ParentModel;
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config, StickVisualizer visualizer)
{ {
ParentModel = model; ParentModel = model;
Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged; model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged(); OnParentModelChanged();
Config = config; Config = config;
@@ -316,17 +316,103 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<!-- Controller Picture --> <!-- Controller Picture -->
<Image
Margin="0,10"
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Source="{Binding Image}" />
<Border <Border
BorderBrush="{DynamicResource ThemeControlBorderColor}" BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="5" CornerRadius="5"
Margin="0,10"
MinHeight="90"> MinHeight="90">
<StackPanel Orientation="Vertical">
<Image
Margin="5,10"
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Source="{Binding Image}" />
<StackPanel
Margin="10"
Orientation="Horizontal"
Spacing="20"
HorizontalAlignment="Center">
<Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsLeft}">
<Canvas
Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}">
<Grid
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse
HorizontalAlignment="Center"
Stroke="Black"
StrokeThickness="1"
Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding Visualizer.UiCanvasSize}" />
<Ellipse
HorizontalAlignment="Center"
Fill="Gray"
Opacity="100"
Height="{Binding Visualizer.UiDeadzoneLeft}"
Width="{Binding Visualizer.UiDeadzoneLeft}" />
</Grid>
<Ellipse
Fill="Red"
Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
</Canvas>
</Border>
<Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsRight}">
<Canvas
Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}">
<Grid
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse
HorizontalAlignment="Center"
Stroke="Black"
StrokeThickness="1"
Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding Visualizer.UiCanvasSize}" />
<Ellipse
HorizontalAlignment="Center"
Fill="Gray"
Opacity="100"
Height="{Binding Visualizer.UiDeadzoneRight}"
Width="{Binding Visualizer.UiDeadzoneRight}" />
</Grid>
<Ellipse
Fill="Red"
Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
Canvas.Left="{Binding Visualizer.UiStickRightX}" />
</Canvas>
</Border>
</StackPanel>
</StackPanel>
</Border>
<Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5">
<StackPanel <StackPanel
Margin="8" Margin="8"
Orientation="Vertical"> Orientation="Vertical">
@@ -345,8 +431,8 @@
Minimum="0" Minimum="0"
Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" /> Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" />
<TextBlock <TextBlock
Width="25" Width="25"
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" /> Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
</StackPanel> </StackPanel>
<StackPanel <StackPanel
Orientation="Vertical" Orientation="Vertical"
@@ -309,12 +309,79 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<!-- Controller Picture --> <!-- Controller Picture -->
<Image <Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
Margin="0,10" Margin="0,10"
MaxHeight="300" MinHeight="90">
HorizontalAlignment="Stretch" <StackPanel
VerticalAlignment="Stretch" Margin="10"
Source="{Binding Image}" /> Orientation="Horizontal"
Spacing="20"
HorizontalAlignment="Center">
<Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsLeft}">
<Canvas
Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}">
<Grid
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse
HorizontalAlignment="Center"
Stroke="Black"
StrokeThickness="1"
Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding Visualizer.UiCanvasSize}"/>
</Grid>
<Ellipse
Fill="Red"
Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
</Canvas>
</Border>
<Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsRight}">
<Canvas
Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}">
<Grid
Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse
HorizontalAlignment="Center"
Stroke="Black"
StrokeThickness="1"
Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding Visualizer.UiCanvasSize}"/>
</Grid>
<Ellipse
Fill="Red"
Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
Canvas.Left="{Binding Visualizer.UiStickRightX}" />
</Canvas>
</Border>
</StackPanel>
</Border>
<Border <Border
BorderBrush="{DynamicResource ThemeControlBorderColor}" BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1" BorderThickness="1"
+1 -1
View File
@@ -12,7 +12,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
Width="1100" Width="1100"
Height="768" Height="850"
MinWidth="800" MinWidth="800"
MinHeight="480" MinHeight="480"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"