Compare commits
35 Commits
Canary-1.2
...
22f57e31d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22f57e31d9 | ||
|
|
52b0b45d34 | ||
|
|
12f0dbcc70 | ||
|
|
719be560ec | ||
|
|
18238736be | ||
|
|
5d9e4ad7a4 | ||
|
|
adba775f0c | ||
|
|
2ffaeb2803 | ||
|
|
b16b844760 | ||
|
|
bc07bc482d | ||
|
|
61975ca44d | ||
|
|
66054dd225 | ||
|
|
b1f61e5143 | ||
|
|
0d7d0e8092 | ||
|
|
8a6f806dda | ||
|
|
15d589c455 | ||
|
|
3262020e18 | ||
|
|
a3afebd3a2 | ||
|
|
24e88e2485 | ||
|
|
3b447b764e | ||
|
|
849fd0199e | ||
|
|
57e26114e8 | ||
|
|
6a283190b3 | ||
|
|
85547874c8 | ||
|
|
16ca8e5005 | ||
|
|
cfa5ad0757 | ||
|
|
43ece083b2 | ||
|
|
e1f5c501b0 | ||
|
|
ffe366d953 | ||
|
|
75c7a29278 | ||
|
|
3a0d9c1435 | ||
|
|
93d1476a2a | ||
|
|
ce13830063 | ||
|
|
aa3f2824e0 | ||
|
|
d2bb580aea |
@@ -969,6 +969,7 @@
|
||||
0100751007ADA000,"Don't Starve: Nintendo Switch Edition",nvdec,playable,2022-02-05 20:43:34
|
||||
010088B010DD2000,"Dongo Adventure",,playable,2022-10-04 16:22:26
|
||||
0100C1F0051B6000,"Donkey Kong Country™: Tropical Freeze",,playable,2024-08-05 16:46:10
|
||||
01009D901BC56000,"Donkey Kong Country™: Returns HD",gpu,ingame,2025-02-16 13:44:12
|
||||
0100F2C00F060000,"Doodle Derby",,boots,2020-12-04 22:51:48
|
||||
0100416004C00000,"DOOM",gpu;slow;nvdec;online-broken,ingame,2024-09-23 15:40:07
|
||||
010018900DD00000,"DOOM (1993)",nvdec;online-broken,menus,2022-09-06 13:32:19
|
||||
@@ -1158,7 +1159,7 @@
|
||||
010095600AA36000,"Fill-a-Pix: Phil's Epic Adventure",,playable,2020-12-22 13:48:22
|
||||
0100C3A00BB76000,"Fimbul",nvdec,playable,2022-07-26 13:31:47
|
||||
0100C8200E942000,"Fin and the Ancient Mystery",nvdec,playable,2020-12-17 16:40:39
|
||||
01000EA014150000,"FINAL FANTASY",crash,nothing,2024-09-05 20:55:30
|
||||
01000EA014150000,"FINAL FANTASY",,playable,2025-02-16 21:27:30
|
||||
01006B7014156000,"FINAL FANTASY II",crash,nothing,2024-04-13 19:18:04
|
||||
01006F000B056000,"FINAL FANTASY IX",audout;nvdec,playable,2021-06-05 11:35:00
|
||||
0100AA201415C000,"FINAL FANTASY V",,playable,2023-04-26 01:11:55
|
||||
@@ -2838,6 +2839,7 @@
|
||||
01009B90006DC000,"Super Mario Maker™ 2",online-broken;ldn-broken,playable,2024-08-25 11:05:19
|
||||
0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug,playable,2024-08-25 01:32:34
|
||||
010036B0034E4000,"Super Mario Party™",gpu;Needs Update;ldn-works,ingame,2024-06-21 05:10:16
|
||||
0100965017338000,"Super Mario Party Jamboree",mac-bug;gpu,ingame,2025-02-17 02:09:20
|
||||
0100BC0018138000,"Super Mario RPG™",gpu;audio;nvdec,ingame,2024-06-19 17:43:42
|
||||
,"Super Mario World",homebrew,boots,2024-06-13 01:40:31
|
||||
010049900F546000,"Super Mario™ 3D All-Stars",services-horizon;slow;vulkan;amd-vendor-bug,ingame,2024-05-07 02:38:16
|
||||
|
||||
|
@@ -53,10 +53,9 @@ namespace Ryujinx.Common
|
||||
"0100000000010000", // Super Mario Odyssey
|
||||
|
||||
// Further testing is appreciated, I did not test the entire game:
|
||||
"01007300020fa000", // Astral Chain
|
||||
"010076f0049a2000", // Bayonetta
|
||||
"0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon
|
||||
"0100f4300bf2c000", // New Pokemon Snap
|
||||
//"010076f0049a2000", // Bayonetta
|
||||
//"0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon
|
||||
//"0100f4300bf2c000", // New Pokemon Snap
|
||||
];
|
||||
|
||||
public static string GetDiscordGameAsset(string titleId)
|
||||
@@ -230,6 +229,7 @@ namespace Ryujinx.Common
|
||||
"01008c8012920000", // Dying Light Platinum Edition
|
||||
"01001cc01b2d4000", // Goat Simulator 3
|
||||
"01003620068ea000", // Hand of Fate 2
|
||||
"0100f7e00c70e000", // Hogwarts Legacy
|
||||
"010085500130a000", // Lego City: Undercover
|
||||
"010073c01af34000", // LEGO Horizon Adventures
|
||||
"0100d71004694000", // Minecraft
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
|
||||
public async Task<ushort> NatPunch()
|
||||
{
|
||||
NatDiscoverer discoverer = new();
|
||||
CancellationTokenSource cts = new(5000);
|
||||
CancellationTokenSource cts = new(2500);
|
||||
|
||||
NatDevice device;
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
|
||||
{
|
||||
if (errorCode != LinuxError.SUCCESS)
|
||||
{
|
||||
if (errorCode != LinuxError.EWOULDBLOCK)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Operation failed with error {errorCode}.");
|
||||
}
|
||||
result = -1;
|
||||
}
|
||||
|
||||
@@ -66,6 +70,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
|
||||
BsdSocketType type = (BsdSocketType)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);
|
||||
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;
|
||||
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);
|
||||
|
||||
@@ -111,6 +126,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
|
||||
|
||||
if (exempt)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceBsd, "Disconnecting exempt socket.");
|
||||
newBsdSocket.Disconnect();
|
||||
}
|
||||
|
||||
@@ -797,7 +813,11 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
|
||||
{
|
||||
errno = socket.Listen(backlog);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, $"Invalid socket fd '{socketFd}'.");
|
||||
}
|
||||
|
||||
return WriteBsdResult(context, 0, errno);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,18 +92,30 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
{
|
||||
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;
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
newSocket = null;
|
||||
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
|
||||
public LinuxError Bind(IPEndPoint localEndPoint)
|
||||
{
|
||||
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Socket binding to: {ProtocolType}/{localEndPoint.Port}");
|
||||
try
|
||||
{
|
||||
Socket.Bind(localEndPoint);
|
||||
@@ -112,6 +124,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
@@ -123,6 +139,15 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
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
|
||||
{
|
||||
Socket.Connect(remoteEndPoint);
|
||||
@@ -137,6 +162,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
@@ -144,11 +173,13 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceBsd, "Socket disconnecting");
|
||||
Socket.Disconnect(true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceBsd, "Socket closed");
|
||||
Socket.Close();
|
||||
Socket.Dispose();
|
||||
}
|
||||
@@ -159,10 +190,16 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
{
|
||||
Socket.Listen(backlog);
|
||||
|
||||
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Socket listening: {ProtocolType}/{(Socket.LocalEndPoint as IPEndPoint).Port}");
|
||||
|
||||
return LinuxError.SUCCESS;
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
@@ -182,11 +219,15 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasEmittedBlockingWarning = false;
|
||||
private bool _hasEmittedBlockingWarning;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (Blocking && !hasEmittedBlockingWarning)
|
||||
if (Blocking && !_hasEmittedBlockingWarning)
|
||||
{
|
||||
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));
|
||||
@@ -214,6 +255,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
receiveSize = -1;
|
||||
|
||||
result = WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
@@ -245,10 +290,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
shouldBlockAfterOperation = true;
|
||||
}
|
||||
|
||||
if (Blocking && !hasEmittedBlockingWarning)
|
||||
if (Blocking && !_hasEmittedBlockingWarning)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
|
||||
hasEmittedBlockingWarning = true;
|
||||
_hasEmittedBlockingWarning = true;
|
||||
}
|
||||
|
||||
if (!Socket.IsBound)
|
||||
@@ -265,6 +310,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
receiveSize = -1;
|
||||
|
||||
result = WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
@@ -288,6 +337,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
sendSize = -1;
|
||||
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
@@ -304,6 +357,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
sendSize = -1;
|
||||
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
@@ -341,6 +398,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
@@ -387,6 +448,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
@@ -519,6 +584,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
return WinSockHelper.ConvertError((WsaError)exception.ErrorCode);
|
||||
}
|
||||
}
|
||||
@@ -557,6 +626,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
if (exception.SocketErrorCode != SocketError.WouldBlock)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Socket Exception: {exception}");
|
||||
}
|
||||
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.Sockets.Bsd.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -64,10 +66,18 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
|
||||
{
|
||||
if (_proxy.Supported(domain, type, protocol))
|
||||
{
|
||||
Logger.Info?.PrintMsg(LogClass.ServiceBsd, $"Socket is using LDN 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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
private static readonly string _description =
|
||||
ReleaseInformation.IsValid
|
||||
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
|
||||
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}"
|
||||
: "dev build";
|
||||
|
||||
private const string ApplicationId = "1293250299716173864";
|
||||
|
||||
@@ -289,7 +289,8 @@ namespace Ryujinx.Headless
|
||||
|
||||
DriverUtilities.InitDriverConfig(option.BackendThreading == BackendThreading.Off);
|
||||
|
||||
if (_inputConfiguration.OfType<StandardControllerInputConfig>().Any(ic => ic.Led.UseRainbow))
|
||||
if (_inputConfiguration.OfType<StandardControllerInputConfig>()
|
||||
.Any(ic => ic?.Led?.UseRainbow ?? false))
|
||||
Rainbow.Enable();
|
||||
|
||||
while (true)
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace Ryujinx.Ava
|
||||
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
|
||||
{
|
||||
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PreviewerDetached = true;
|
||||
|
||||
@@ -119,17 +119,23 @@
|
||||
TextWrapping="Wrap" >
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="5" ToolTip.Tip="{Binding DynamicRichPresenceDescription}">
|
||||
<ui:SymbolIcon
|
||||
Foreground="ForestGreen"
|
||||
Symbol="Checkmark"
|
||||
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/>
|
||||
<TextBlock
|
||||
Foreground="ForestGreen"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"
|
||||
Text="{ext:Locale GameInfoRpcDynamic}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" >
|
||||
TextWrapping="Wrap">
|
||||
</TextBlock>
|
||||
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/>
|
||||
<ui:SymbolIcon
|
||||
Foreground="Red"
|
||||
Symbol="Cancel"
|
||||
IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/>
|
||||
<TextBlock
|
||||
Foreground="Red"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
||||
260
src/Ryujinx/UI/Models/Input/StickVisualizer.cs
Normal file
260
src/Ryujinx/UI/Models/Input/StickVisualizer.cs
Normal file
@@ -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,6 +1,7 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.PlayReport;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
@@ -10,6 +11,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
|
||||
|
||||
public string DynamicRichPresenceDescription =>
|
||||
AppData.HasDynamicRichPresenceSupport
|
||||
? AppData.RichPresenceSpec.Value.Description
|
||||
: GameSpec.DefaultDescription;
|
||||
|
||||
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
|
||||
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
|
||||
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using Avalonia.Svg.Skia;
|
||||
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.Views.Input;
|
||||
using Ryujinx.Common.Utilities;
|
||||
@@ -10,8 +14,30 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
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;
|
||||
public bool IsLeft
|
||||
{
|
||||
@@ -37,14 +63,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
}
|
||||
|
||||
public bool HasSides => IsLeft ^ IsRight;
|
||||
|
||||
|
||||
[ObservableProperty] private SvgImage _image;
|
||||
|
||||
|
||||
public InputViewModel ParentModel { get; }
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
|
||||
{
|
||||
ParentModel = model;
|
||||
Visualizer = visualizer;
|
||||
model.NotifyChangesEvent += OnParentModelChanged;
|
||||
OnParentModelChanged();
|
||||
config.PropertyChanged += (_, args) =>
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
private int _controller;
|
||||
private string _controllerImage;
|
||||
private int _device;
|
||||
[ObservableProperty] private object _configViewModel;
|
||||
private object _configViewModel;
|
||||
[ObservableProperty] private string _profileName;
|
||||
private bool _isLoaded;
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
|
||||
}
|
||||
}
|
||||
public StickVisualizer VisualStick { get; private set; }
|
||||
|
||||
public ObservableCollection<PlayerModel> PlayerIndexes { 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 event Action NotifyChangesEvent;
|
||||
|
||||
public object ConfigViewModel
|
||||
{
|
||||
get => _configViewModel;
|
||||
set
|
||||
{
|
||||
_configViewModel = value;
|
||||
|
||||
VisualStick.UpdateConfig(value);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerIndex PlayerIdChoose
|
||||
{
|
||||
get => _playerIdChoose;
|
||||
@@ -269,6 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
Devices = [];
|
||||
ProfilesList = [];
|
||||
DeviceList = [];
|
||||
VisualStick = new StickVisualizer(this);
|
||||
|
||||
ControllerImage = ProControllerResource;
|
||||
|
||||
@@ -289,12 +304,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
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)
|
||||
{
|
||||
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();
|
||||
|
||||
VisualStick.Dispose();
|
||||
|
||||
SelectedGamepad?.Dispose();
|
||||
|
||||
AvaloniaKeyboardDriver.Dispose();
|
||||
|
||||
@@ -6,7 +6,29 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
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;
|
||||
public bool IsLeft
|
||||
@@ -38,9 +60,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
public readonly InputViewModel ParentModel;
|
||||
|
||||
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config)
|
||||
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config, StickVisualizer visualizer)
|
||||
{
|
||||
ParentModel = model;
|
||||
Visualizer = visualizer;
|
||||
model.NotifyChangesEvent += OnParentModelChanged;
|
||||
OnParentModelChanged();
|
||||
Config = config;
|
||||
|
||||
@@ -316,17 +316,103 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<!-- Controller Picture -->
|
||||
<Image
|
||||
Margin="0,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Margin="0,10"
|
||||
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
|
||||
Margin="8"
|
||||
Orientation="Vertical">
|
||||
@@ -345,8 +431,8 @@
|
||||
Minimum="0"
|
||||
Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Width="25"
|
||||
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
|
||||
Width="25"
|
||||
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Orientation="Vertical"
|
||||
|
||||
@@ -309,12 +309,79 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<!-- Controller Picture -->
|
||||
<Image
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Margin="0,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
MinHeight="90">
|
||||
<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}"/>
|
||||
</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
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||
Width="1100"
|
||||
Height="768"
|
||||
Height="850"
|
||||
MinWidth="800"
|
||||
MinHeight="480"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
|
||||
@@ -10,6 +10,7 @@ using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using Ryujinx.Ava.Utilities.PlayReport;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
@@ -35,9 +36,14 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
{
|
||||
_id = value;
|
||||
|
||||
Compatibility = CompatibilityCsv.Find(Id);
|
||||
Compatibility = CompatibilityCsv.Find(value);
|
||||
RichPresenceSpec = PlayReports.Analyzer.TryGetSpec(IdString, out GameSpec gameSpec)
|
||||
? gameSpec
|
||||
: default(Optional<GameSpec>);
|
||||
}
|
||||
}
|
||||
public Optional<GameSpec> RichPresenceSpec { get; set; }
|
||||
|
||||
public string Developer { get; set; } = "Unknown";
|
||||
public string Version { get; set; } = "0";
|
||||
public int PlayerCount { get; set; }
|
||||
@@ -46,7 +52,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
||||
|
||||
public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
|
||||
public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString);
|
||||
public bool HasDynamicRichPresenceSupport => RichPresenceSpec.HasValue;
|
||||
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
|
||||
public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs);
|
||||
|
||||
public GameSpec GetSpec(string titleId) => _specs.First(x => x.TitleIds.ContainsIgnoreCase(titleId));
|
||||
|
||||
public bool TryGetSpec(string titleId, out GameSpec gameSpec)
|
||||
=> (gameSpec = _specs.FirstOrDefault(x => x.TitleIds.ContainsIgnoreCase(titleId))) != null;
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||
/// </summary>
|
||||
@@ -128,13 +133,13 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
{
|
||||
if (!playReport.ReportData.IsDictionary)
|
||||
return FormattedValue.Unhandled;
|
||||
|
||||
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
|
||||
|
||||
if (!TryGetSpec(runningGameId, out GameSpec spec))
|
||||
return FormattedValue.Unhandled;
|
||||
|
||||
foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority))
|
||||
{
|
||||
if (!formatSpec.Format(appMeta, playReport, out FormattedValue value))
|
||||
if (!formatSpec.TryFormat(appMeta, playReport, out FormattedValue value))
|
||||
continue;
|
||||
|
||||
return value;
|
||||
|
||||
@@ -9,72 +9,89 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
// init lazy value
|
||||
_ = Analyzer;
|
||||
}
|
||||
|
||||
|
||||
public static Analyzer Analyzer => _analyzerLazy.Value;
|
||||
|
||||
private static readonly Lazy<Analyzer> _analyzerLazy = new(() =>
|
||||
new Analyzer()
|
||||
.AddSpec(
|
||||
"01007ef00011e000",
|
||||
spec => spec
|
||||
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||
// reset to normal status when switching between normal & master mode in title screen
|
||||
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
|
||||
)
|
||||
.AddSpec(
|
||||
"0100f2c0115b6000",
|
||||
spec => spec
|
||||
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
|
||||
.AddSpec(
|
||||
"0100000000010000",
|
||||
spec =>
|
||||
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
||||
)
|
||||
.AddSpec(
|
||||
"010075000ecbe000",
|
||||
spec =>
|
||||
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
||||
)
|
||||
.AddSpec(
|
||||
"010028600ebda000",
|
||||
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
||||
)
|
||||
.AddSpec( // Global & China IDs
|
||||
["0100152000022000", "010075100e8ec000"],
|
||||
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||
)
|
||||
.AddSpec(
|
||||
["0100a3d008c5c000", "01008f6008c5e000"],
|
||||
spec => spec
|
||||
.AddValueFormatter("area_no", PokemonSVArea)
|
||||
.AddValueFormatter("team_circle", PokemonSVUnionCircle)
|
||||
)
|
||||
.AddSpec(
|
||||
"01006a800016e000",
|
||||
spec => spec
|
||||
.AddSparseMultiValueFormatter(
|
||||
[
|
||||
// Metadata to figure out what PlayReport we have.
|
||||
"match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count",
|
||||
"adv_slot",
|
||||
// List of Fighters
|
||||
"player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter",
|
||||
"player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter",
|
||||
// List of rankings/placements
|
||||
"player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank",
|
||||
"player_6_rank", "player_7_rank", "player_8_rank"
|
||||
],
|
||||
SuperSmashBrosUltimate_Mode
|
||||
)
|
||||
)
|
||||
.AddSpec(
|
||||
[
|
||||
"0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
|
||||
"010012f017576000", "0100c62011050000", "0100b3c014bda000"
|
||||
],
|
||||
spec => spec.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
|
||||
)
|
||||
.AddSpec("01002da013484000", spec => spec.AddValueFormatter("rupees", SkywardSwordHD_Rupees))
|
||||
private static readonly Lazy<Analyzer> _analyzerLazy = new(() => new Analyzer()
|
||||
.AddSpec(
|
||||
"01007ef00011e000",
|
||||
spec => spec
|
||||
.WithDescription("based on being in Master Mode.")
|
||||
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||
// reset to normal status when switching between normal & master mode in title screen
|
||||
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
|
||||
)
|
||||
.AddSpec(
|
||||
"0100f2c0115b6000",
|
||||
spec => spec
|
||||
.WithDescription("based on where you are in Hyrule (Depths, Surface, Sky).")
|
||||
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
|
||||
.AddSpec(
|
||||
"01002da013484000",
|
||||
spec => spec
|
||||
.WithDescription("based on how many Rupees you have.")
|
||||
.AddValueFormatter("rupees", SkywardSwordHD_Rupees))
|
||||
.AddSpec(
|
||||
"0100000000010000",
|
||||
spec => spec
|
||||
.WithDescription("based on if you're playing with Assist Mode.")
|
||||
.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
||||
)
|
||||
.AddSpec(
|
||||
"010075000ecbe000",
|
||||
spec => spec
|
||||
.WithDescription("based on if you're playing with Assist Mode.")
|
||||
.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
||||
)
|
||||
.AddSpec(
|
||||
"010028600ebda000",
|
||||
spec => spec
|
||||
.WithDescription("based on being in either Super Mario 3D World or Bowser's Fury.")
|
||||
.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
||||
)
|
||||
.AddSpec( // Global & China IDs
|
||||
["0100152000022000", "010075100e8ec000"],
|
||||
spec => spec
|
||||
.WithDescription(
|
||||
"based on what modes you're selecting in the menu & whether or not you're in a race.")
|
||||
.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||
)
|
||||
.AddSpec(
|
||||
["0100a3d008c5c000", "01008f6008c5e000"],
|
||||
spec => spec
|
||||
.WithDescription("based on what area of Paldea you're exploring.")
|
||||
.AddValueFormatter("area_no", PokemonSVArea)
|
||||
.AddValueFormatter("team_circle", PokemonSVUnionCircle)
|
||||
)
|
||||
.AddSpec(
|
||||
"01006a800016e000",
|
||||
spec => spec
|
||||
.WithDescription("based on what mode you're playing, who won, and what characters were present.")
|
||||
.AddSparseMultiValueFormatter(
|
||||
[
|
||||
// Metadata to figure out what PlayReport we have.
|
||||
"match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count",
|
||||
"adv_slot",
|
||||
// List of Fighters
|
||||
"player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter",
|
||||
"player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter",
|
||||
// List of rankings/placements
|
||||
"player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank",
|
||||
"player_6_rank", "player_7_rank", "player_8_rank"
|
||||
],
|
||||
SuperSmashBrosUltimate_Mode
|
||||
)
|
||||
)
|
||||
.AddSpec(
|
||||
[
|
||||
"0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
|
||||
"010012f017576000", "0100c62011050000", "0100b3c014bda000"
|
||||
],
|
||||
spec => spec
|
||||
.WithDescription(
|
||||
"based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.")
|
||||
.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
|
||||
)
|
||||
);
|
||||
|
||||
private static string Playing(string game) => $"Playing {game}";
|
||||
|
||||
@@ -23,6 +23,20 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
|
||||
public required string[] TitleIds { get; init; }
|
||||
|
||||
public const string DefaultDescription = "Formats the details on your Discord presence based on logged data from the game.";
|
||||
|
||||
private string _valueDescription;
|
||||
|
||||
public string Description => _valueDescription ?? DefaultDescription;
|
||||
|
||||
public GameSpec WithDescription(string description)
|
||||
{
|
||||
_valueDescription = description != null
|
||||
? $"Formats the details on your Discord presence {description}"
|
||||
: null;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<FormatterSpecBase> ValueFormatters { get; } = [];
|
||||
|
||||
|
||||
@@ -197,7 +211,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
public string[] ReportKeys { get; init; }
|
||||
public Delegate Formatter { get; init; }
|
||||
|
||||
public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
|
||||
public bool TryFormat(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
|
||||
out FormattedValue formattedValue)
|
||||
{
|
||||
formattedValue = default;
|
||||
|
||||
Reference in New Issue
Block a user