diff --git a/docs/compatibility.csv b/docs/compatibility.csv index 570c93618..53ad389b6 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2483,7 +2483,7 @@ 0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23 0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56 010003A00D0B4000,"SaGa SCARLET GRACE: AMBITIONS™",,playable,2022-10-06 13:20:31 -01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN,playable,2023-12-04 18:33:37 +01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN;deadlock,ingame,2025-02-02 16:57:53 0100DE600BEEE000,"SAINTS ROW®: THE THIRD™ - THE FULL PACKAGE",slow;LAN,playable,2023-08-24 02:40:58 01007F000EB36000,"Sakai and...",nvdec,playable,2022-12-15 13:53:19 0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13 diff --git a/src/Ryujinx.Common/Helpers/Patterns.cs b/src/Ryujinx.Common/Helpers/Patterns.cs new file mode 100644 index 000000000..84cc1353a --- /dev/null +++ b/src/Ryujinx.Common/Helpers/Patterns.cs @@ -0,0 +1,118 @@ +using System.Text.RegularExpressions; + +namespace Ryujinx.Common.Helper +{ + public static partial class Patterns + { + #region Accessors + + public static readonly Regex Numeric = NumericRegex(); + + public static readonly Regex AmdGcn = AmdGcnRegex(); + public static readonly Regex NvidiaConsumerClass = NvidiaConsumerClassRegex(); + + public static readonly Regex DomainLp1Ns = DomainLp1NsRegex(); + public static readonly Regex DomainLp1Lp1Npln = DomainLp1Lp1NplnRegex(); + public static readonly Regex DomainLp1Znc = DomainLp1ZncRegex(); + public static readonly Regex DomainSbApi = DomainSbApiRegex(); + public static readonly Regex DomainSbAccounts = DomainSbAccountsRegex(); + public static readonly Regex DomainAccounts = DomainAccountsRegex(); + + public static readonly Regex Module = ModuleRegex(); + public static readonly Regex FsSdk = FsSdkRegex(); + public static readonly Regex SdkMw = SdkMwRegex(); + + // ReSharper disable once InconsistentNaming + public static readonly Regex CJK = CJKRegex(); + + public static readonly Regex LdnPassphrase = LdnPassphraseRegex(); + + public static readonly Regex CleanText = CleanTextRegex(); + + #endregion + + #region Generated pattern stubs + + #region Numeric validation + + [GeneratedRegex("[0-9]|.")] + internal static partial Regex NumericRegex(); + + #endregion + + #region GPU names + + [GeneratedRegex( + "Radeon (((HD|R(5|7|9|X)) )?((M?[2-6]\\d{2}(\\D|$))|([7-8]\\d{3}(\\D|$))|Fury|Nano))|(Pro Duo)")] + internal static partial Regex AmdGcnRegex(); + + [GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")] + internal static partial Regex NvidiaConsumerClassRegex(); + + #endregion + + #region DNS blocking + + public static readonly Regex[] BlockedHosts = + [ + DomainLp1Ns, + DomainLp1Lp1Npln, + DomainLp1Znc, + DomainSbApi, + DomainSbAccounts, + DomainAccounts + ]; + + const RegexOptions DnsRegexOpts = + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + + [GeneratedRegex(@"^(.*)\-lp1\.(n|s)\.n\.srv\.nintendo\.net$", DnsRegexOpts)] + internal static partial Regex DomainLp1NsRegex(); + + [GeneratedRegex(@"^(.*)\-lp1\.lp1\.t\.npln\.srv\.nintendo\.net$", DnsRegexOpts)] + internal static partial Regex DomainLp1Lp1NplnRegex(); + + [GeneratedRegex(@"^(.*)\-lp1\.(znc|p)\.srv\.nintendo\.net$", DnsRegexOpts)] + internal static partial Regex DomainLp1ZncRegex(); + + [GeneratedRegex(@"^(.*)\-sb\-api\.accounts\.nintendo\.com$", DnsRegexOpts)] + internal static partial Regex DomainSbApiRegex(); + + [GeneratedRegex(@"^(.*)\-sb\.accounts\.nintendo\.com$", DnsRegexOpts)] + internal static partial Regex DomainSbAccountsRegex(); + + [GeneratedRegex(@"^accounts\.nintendo\.com$", DnsRegexOpts)] + internal static partial Regex DomainAccountsRegex(); + + #endregion + + #region Executable information + + [GeneratedRegex(@"[a-z]:[\\/][ -~]{5,}\.nss", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + internal static partial Regex ModuleRegex(); + + [GeneratedRegex(@"sdk_version: ([0-9.]*)")] + internal static partial Regex FsSdkRegex(); + + [GeneratedRegex(@"SDK MW[ -~]*")] + internal static partial Regex SdkMwRegex(); + + #endregion + + #region CJK + + [GeneratedRegex( + "\\p{IsHangulJamo}|\\p{IsCJKRadicalsSupplement}|\\p{IsCJKSymbolsandPunctuation}|\\p{IsEnclosedCJKLettersandMonths}|\\p{IsCJKCompatibility}|\\p{IsCJKUnifiedIdeographsExtensionA}|\\p{IsCJKUnifiedIdeographs}|\\p{IsHangulSyllables}|\\p{IsCJKCompatibilityForms}")] + private static partial Regex CJKRegex(); + + #endregion + + [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] + private static partial Regex LdnPassphraseRegex(); + + [GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")] + private static partial Regex CleanTextRegex(); + + #endregion + } +} diff --git a/src/Ryujinx.Common/Helpers/RunningPlatform.cs b/src/Ryujinx.Common/Helpers/RunningPlatform.cs index 61f5bd614..8d85c4a3c 100644 --- a/src/Ryujinx.Common/Helpers/RunningPlatform.cs +++ b/src/Ryujinx.Common/Helpers/RunningPlatform.cs @@ -10,14 +10,18 @@ namespace Ryujinx.Common.Helper public static bool IsMacOS => OperatingSystem.IsMacOS(); public static bool IsWindows => OperatingSystem.IsWindows(); public static bool IsLinux => OperatingSystem.IsLinux(); + + public static bool IsArm => RuntimeInformation.OSArchitecture is Architecture.Arm64; + + public static bool IsX64 => RuntimeInformation.OSArchitecture is Architecture.X64; - public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64; - public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64; + public static bool IsIntelMac => IsMacOS && IsX64; + public static bool IsArmMac => IsMacOS && IsArm; - public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64); - public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64); + public static bool IsX64Windows => IsWindows && IsX64; + public static bool IsArmWindows => IsWindows && IsArm; - public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64); - public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64); + public static bool IsX64Linux => IsLinux && IsX64; + public static bool IsArmLinux => IsLinux && IsArmMac; } } diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index 609dc572a..42322c8a2 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -47,7 +47,8 @@ namespace Ryujinx.Common "01006f8002326000", // Animal Crossings: New Horizons "01009bf0072d4000", // Captain Toad: Treasure Tracker "01009510001ca000", // Fast RMX - "01005CA01580E000", // Persona 5 Royale + "01005CA01580E000", // Persona 5 Royal + "0100b880154fc000", // Persona 5 The Royal (Japan) "010015100b514000", // Super Mario Bros. Wonder "0100000000010000", // Super Mario Odyssey diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs index b68a64a47..d51b0ef60 100644 --- a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs @@ -87,7 +87,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization } using ManualResetEvent waitEvent = new(false); - SyncpointWaiterHandle info = _syncpoints[id].RegisterCallback(threshold, (x) => waitEvent.Set()); + SyncpointWaiterHandle info = _syncpoints[id].RegisterCallback(threshold, _ => waitEvent.Set()); if (info == null) { @@ -96,7 +96,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization bool signaled = waitEvent.WaitOne(timeout); - if (!signaled && info != null) + if (!signaled) { Logger.Error?.Print(LogClass.Gpu, $"Wait on syncpoint {id} for threshold {threshold} took more than {timeout.TotalMilliseconds}ms, resuming execution..."); diff --git a/src/Ryujinx.Graphics.Vulkan/Vendor.cs b/src/Ryujinx.Graphics.Vulkan/Vendor.cs index 6a2a76a88..87c6407cd 100644 --- a/src/Ryujinx.Graphics.Vulkan/Vendor.cs +++ b/src/Ryujinx.Graphics.Vulkan/Vendor.cs @@ -16,14 +16,8 @@ namespace Ryujinx.Graphics.Vulkan Unknown, } - static partial class VendorUtils + static class VendorUtils { - [GeneratedRegex("Radeon (((HD|R(5|7|9|X)) )?((M?[2-6]\\d{2}(\\D|$))|([7-8]\\d{3}(\\D|$))|Fury|Nano))|(Pro Duo)")] - public static partial Regex AmdGcnRegex(); - - [GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")] - public static partial Regex NvidiaConsumerClassRegex(); - public static Vendor FromId(uint id) { return id switch diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 986baf91b..e90606dcf 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -1,5 +1,6 @@ using Gommon; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Shader; @@ -375,11 +376,11 @@ namespace Ryujinx.Graphics.Vulkan GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}"; - IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer); + IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && Patterns.AmdGcn.IsMatch(GpuRenderer); if (Vendor == Vendor.Nvidia) { - Match match = VendorUtils.NvidiaConsumerClassRegex().Match(GpuRenderer); + Match match = Patterns.NvidiaConsumerClass.Match(GpuRenderer); if (match != null && int.TryParse(match.Groups[2].Value, out int gpuNumber)) { diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs index 54b5721c1..b41bc60b1 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -5,6 +5,7 @@ using LibHac.FsSystem; using LibHac.Ncm; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using Ryujinx.HLE.HOS.SystemState; @@ -30,9 +31,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error public event EventHandler AppletStateChanged; - [GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")] - private static partial Regex CleanTextRegex(); - public ErrorApplet(Horizon horizon) { _horizon = horizon; @@ -107,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Applets.Error private static string CleanText(string value) { - return CleanTextRegex().Replace(value, string.Empty).Replace("\0", string.Empty); + return Patterns.CleanText.Replace(value, string.Empty).Replace("\0", string.Empty); } private string GetMessageText(uint module, uint description, string key) diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs deleted file mode 100644 index 6134a3cdd..000000000 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CJKCharacterValidation.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard -{ - public static partial class CJKCharacterValidation - { - public static bool IsCJK(char value) - { - Regex regex = CJKRegex(); - - return regex.IsMatch(value.ToString()); - } - - [GeneratedRegex("\\p{IsHangulJamo}|\\p{IsCJKRadicalsSupplement}|\\p{IsCJKSymbolsandPunctuation}|\\p{IsEnclosedCJKLettersandMonths}|\\p{IsCJKCompatibility}|\\p{IsCJKUnifiedIdeographsExtensionA}|\\p{IsCJKUnifiedIdeographs}|\\p{IsHangulSyllables}|\\p{IsCJKCompatibilityForms}")] - private static partial Regex CJKRegex(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CharacterValidation.cs new file mode 100644 index 000000000..5ce8204cb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/CharacterValidation.cs @@ -0,0 +1,10 @@ +using Ryujinx.Common.Helper; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + public static class CharacterValidation + { + public static bool IsNumeric(char value) => Patterns.Numeric.IsMatch(value.ToString()); + public static bool IsCJK(char value) => Patterns.CJK.IsMatch(value.ToString()); + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs deleted file mode 100644 index d72b68eae..000000000 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/NumericCharacterValidation.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard -{ - public static partial class NumericCharacterValidation - { - public static bool IsNumeric(char value) - { - Regex regex = NumericRegex(); - - return regex.IsMatch(value.ToString()); - } - - [GeneratedRegex("[0-9]|.")] - private static partial Regex NumericRegex(); - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs index 78c6be164..507e60573 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Proxy/DnsBlacklist.cs @@ -1,37 +1,13 @@ +using Ryujinx.Common.Helper; using System.Text.RegularExpressions; namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Proxy { - static partial class DnsBlacklist + static class DnsBlacklist { - const RegexOptions RegexOpts = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; - - [GeneratedRegex(@"^(.*)\-lp1\.(n|s)\.n\.srv\.nintendo\.net$", RegexOpts)] - private static partial Regex BlockedHost1(); - [GeneratedRegex(@"^(.*)\-lp1\.lp1\.t\.npln\.srv\.nintendo\.net$", RegexOpts)] - private static partial Regex BlockedHost2(); - [GeneratedRegex(@"^(.*)\-lp1\.(znc|p)\.srv\.nintendo\.net$", RegexOpts)] - private static partial Regex BlockedHost3(); - [GeneratedRegex(@"^(.*)\-sb\-api\.accounts\.nintendo\.com$", RegexOpts)] - private static partial Regex BlockedHost4(); - [GeneratedRegex(@"^(.*)\-sb\.accounts\.nintendo\.com$", RegexOpts)] - private static partial Regex BlockedHost5(); - [GeneratedRegex(@"^accounts\.nintendo\.com$", RegexOpts)] - private static partial Regex BlockedHost6(); - - private static readonly Regex[] _blockedHosts = - [ - BlockedHost1(), - BlockedHost2(), - BlockedHost3(), - BlockedHost4(), - BlockedHost5(), - BlockedHost6() - ]; - public static bool IsHostBlocked(string host) { - foreach (Regex regex in _blockedHosts) + foreach (Regex regex in Patterns.BlockedHosts) { if (regex.IsMatch(host)) { diff --git a/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs b/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs index 5217612b9..84f229d8e 100644 --- a/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs +++ b/src/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs @@ -2,6 +2,7 @@ using LibHac.Common.FixedArrays; using LibHac.Fs; using LibHac.Loader; using LibHac.Tools.FsSystem; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using System; using System.Text; @@ -29,13 +30,6 @@ namespace Ryujinx.HLE.Loaders.Executables public string Name; public Array32 BuildId; - [GeneratedRegex(@"[a-z]:[\\/][ -~]{5,}\.nss", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex ModuleRegex(); - [GeneratedRegex(@"sdk_version: ([0-9.]*)")] - private static partial Regex FsSdkRegex(); - [GeneratedRegex(@"SDK MW[ -~]*")] - private static partial Regex SdkMwRegex(); - public NsoExecutable(IStorage inStorage, string name = null) { NsoReader reader = new(); @@ -90,7 +84,7 @@ namespace Ryujinx.HLE.Loaders.Executables if (string.IsNullOrEmpty(modulePath)) { - Match moduleMatch = ModuleRegex().Match(rawTextBuffer); + Match moduleMatch = Patterns.Module.Match(rawTextBuffer); if (moduleMatch.Success) { modulePath = moduleMatch.Value; @@ -99,13 +93,13 @@ namespace Ryujinx.HLE.Loaders.Executables stringBuilder.AppendLine($" Module: {modulePath}"); - Match fsSdkMatch = FsSdkRegex().Match(rawTextBuffer); + Match fsSdkMatch = Patterns.FsSdk.Match(rawTextBuffer); if (fsSdkMatch.Success) { stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", string.Empty)}"); } - MatchCollection sdkMwMatches = SdkMwRegex().Matches(rawTextBuffer); + MatchCollection sdkMwMatches = Patterns.SdkMw.Matches(rawTextBuffer); if (sdkMwMatches.Count != 0) { string libHeader = " SDK Libraries: "; diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs index 305d54bd1..15689f0c8 100644 --- a/src/Ryujinx.Horizon/HorizonStatic.cs +++ b/src/Ryujinx.Horizon/HorizonStatic.cs @@ -1,31 +1,37 @@ +using MsgPack; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; +using System.Threading; namespace Ryujinx.Horizon { public static class HorizonStatic { - [ThreadStatic] - private static HorizonOptions _options; + internal static void HandlePlayReport(MessagePackObject report) => + new Thread(() => PlayReport?.Invoke(report)) + { + Name = "HLE.PlayReportEvent", + IsBackground = true, + Priority = ThreadPriority.AboveNormal + }.Start(); + + public static event Action PlayReport; - [ThreadStatic] - private static ISyscallApi _syscall; + [field: ThreadStatic] + public static HorizonOptions Options { get; private set; } - [ThreadStatic] - private static IVirtualMemoryManager _addressSpace; + [field: ThreadStatic] + public static ISyscallApi Syscall { get; private set; } - [ThreadStatic] - private static IThreadContext _threadContext; + [field: ThreadStatic] + public static IVirtualMemoryManager AddressSpace { get; private set; } - [ThreadStatic] - private static int _threadHandle; + [field: ThreadStatic] + public static IThreadContext ThreadContext { get; private set; } - public static HorizonOptions Options => _options; - public static ISyscallApi Syscall => _syscall; - public static IVirtualMemoryManager AddressSpace => _addressSpace; - public static IThreadContext ThreadContext => _threadContext; - public static int CurrentThreadHandle => _threadHandle; + [field: ThreadStatic] + public static int CurrentThreadHandle { get; private set; } public static void Register( HorizonOptions options, @@ -34,11 +40,11 @@ namespace Ryujinx.Horizon IThreadContext threadContext, int threadHandle) { - _options = options; - _syscall = syscallApi; - _addressSpace = addressSpace; - _threadContext = threadContext; - _threadHandle = threadHandle; + Options = options; + Syscall = syscallApi; + AddressSpace = addressSpace; + ThreadContext = threadContext; + CurrentThreadHandle = threadHandle; } } } diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index 4ed7dd48e..2f8657e0b 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -1,3 +1,4 @@ +using Gommon; using MsgPack; using MsgPack.Serialization; using Ryujinx.Common.Logging; @@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf; using Ryujinx.Horizon.Sdk.Sf.Hipc; using System; using System.Text; +using System.Threading; using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId; namespace Ryujinx.Horizon.Prepo.Ipc @@ -230,6 +232,8 @@ namespace Ryujinx.Horizon.Prepo.Ipc builder.AppendLine($" Room: {gameRoom}"); builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}"); + + HorizonStatic.HandlePlayReport(deserializedReport); Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString()); diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 00fee0714..a8b1f3bfc 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -925,7 +925,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.EnableInternetAccess, ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, ConfigurationState.Instance.System.FsGlobalAccessLogMode, - ConfigurationState.Instance.System.SystemTimeOffset, + ConfigurationState.Instance.System.MatchSystemTime + ? 0 + : ConfigurationState.Instance.System.SystemTimeOffset, ConfigurationState.Instance.System.TimeZone, ConfigurationState.Instance.System.MemoryManagerMode, ConfigurationState.Instance.System.IgnoreMissingServices, diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 36616d50c..3e2db8724 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1524,6 +1524,156 @@ }, { "ID": "GameListHeaderDeveloper", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Developed by {0}", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderVersion", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "Έκδοση: {0}", + "en_US": "Version: {0}", + "es_ES": "Versión: {0}", + "fr_FR": "", + "he_IL": "", + "it_IT": "Versione: {0}", + "ja_JP": "バージョン: {0}", + "ko_KR": "버전: {0}", + "no_NO": "Versjon: {0}", + "pl_PL": "Wersja: {0}", + "pt_BR": "Versão: {0}", + "ru_RU": "Версия: {0}", + "sv_SE": "", + "th_TH": "เวอร์ชั่น: {0}", + "tr_TR": "Sürüm: {0}", + "uk_UA": "Версія: {0}", + "zh_CN": "版本: {0}", + "zh_TW": "版本: {0}" + } + }, + { + "ID": "GameListHeaderTimePlayed", + "Translations": { + "ar_SA": "", + "de_DE": "Spielzeit: {0}", + "el_GR": "Χρόνος: {0}", + "en_US": "Play Time: {0}", + "es_ES": "Tiempo jugado: {0}", + "fr_FR": "Temps de jeu: {0}", + "he_IL": "", + "it_IT": "Tempo di gioco: {0}", + "ja_JP": "プレイ時間: {0}", + "ko_KR": "플레이 타임: {0}", + "no_NO": "Spilletid: {0}", + "pl_PL": "Czas w grze: {0}", + "pt_BR": "Tempo de jogo: {0}", + "ru_RU": "Время в игре: {0}", + "sv_SE": "Speltid: {0}", + "th_TH": "เล่นไปแล้ว: {0}", + "tr_TR": "Oynama Süresi: {0}", + "uk_UA": "Зіграно часу: {0}", + "zh_CN": "游玩时长: {0}", + "zh_TW": "遊玩時數: {0}" + } + }, + { + "ID": "GameListHeaderLastPlayed", + "Translations": { + "ar_SA": "", + "de_DE": "Zuletzt gespielt: {0}", + "el_GR": "Παίχτηκε: {0}", + "en_US": "Last Played: {0}", + "es_ES": "Jugado por última vez: {0}", + "fr_FR": "Dernière partie jouée: {0}", + "he_IL": "", + "it_IT": "Ultima partita: {0}", + "ja_JP": "最終プレイ日時: {0}", + "ko_KR": "마지막 플레이: {0}", + "no_NO": "Sist Spilt: {0}", + "pl_PL": "Ostatnio grane: {0}", + "pt_BR": "Último jogo: {0}", + "ru_RU": "Последний запуск: {0}", + "sv_SE": "Senast spelad: {0}", + "th_TH": "เล่นล่าสุด: {0}", + "tr_TR": "Son Oynama Tarihi: {0}", + "uk_UA": "Востаннє зіграно: {0}", + "zh_CN": "最近游玩: {0}", + "zh_TW": "最近遊玩: {0}" + } + }, + { + "ID": "GameListHeaderFileExtension", + "Translations": { + "ar_SA": "", + "de_DE": "Dateiformat: {0}", + "el_GR": "Κατάληξη: {0}", + "en_US": "Extension: {0}", + "es_ES": "Extensión: {0}", + "fr_FR": "Extension du Fichier: {0}", + "he_IL": "", + "it_IT": "Estensione: {0}", + "ja_JP": "ファイル拡張子: {0}", + "ko_KR": "파일 확장자: {0}", + "no_NO": "Fil Eks.: {0}", + "pl_PL": "Rozszerzenie pliku: {0}", + "pt_BR": "Extensão: {0}", + "ru_RU": "Расширение файла: {0}", + "sv_SE": "Filänd: {0}", + "th_TH": "นามสกุลไฟล์: {0}", + "tr_TR": "Dosya Uzantısı: {0}", + "uk_UA": "Розширення файлу: {0}", + "zh_CN": "扩展名: {0}", + "zh_TW": "副檔名: {0}" + } + }, + { + "ID": "GameListHeaderFileSize", + "Translations": { + "ar_SA": "", + "de_DE": "Dateigröße: {0}", + "el_GR": "Μέγεθος Αρχείου: {0}", + "en_US": "File Size: {0}", + "es_ES": "Tamaño del archivo: {0}", + "fr_FR": "Taille du Fichier: {0}", + "he_IL": "", + "it_IT": "Dimensione file: {0}", + "ja_JP": "ファイルサイズ: {0}", + "ko_KR": "파일 크기: {0}", + "no_NO": "Fil Størrelse: {0}", + "pl_PL": "Rozmiar pliku: {0}", + "pt_BR": "Tamanho: {0}", + "ru_RU": "Размер файла: {0}", + "sv_SE": "Filstorlek: {0}", + "th_TH": "ขนาดไฟล์: {0}", + "tr_TR": "Dosya Boyutu: {0}", + "uk_UA": "Розмір файлу: {0}", + "zh_CN": "大小: {0}", + "zh_TW": "檔案大小: {0}" + } + }, + { + "ID": "GameListSortDeveloper", "Translations": { "ar_SA": "المطور", "de_DE": "Entwickler", @@ -1548,32 +1698,7 @@ } }, { - "ID": "GameListHeaderVersion", - "Translations": { - "ar_SA": "الإصدار", - "de_DE": "", - "el_GR": "Έκδοση", - "en_US": "Version", - "es_ES": "Versión", - "fr_FR": "", - "he_IL": "גרסה", - "it_IT": "Versione", - "ja_JP": "バージョン", - "ko_KR": "버전", - "no_NO": "Versjon", - "pl_PL": "Wersja", - "pt_BR": "Versão", - "ru_RU": "Версия", - "sv_SE": "", - "th_TH": "เวอร์ชั่น", - "tr_TR": "Sürüm", - "uk_UA": "Версія", - "zh_CN": "版本", - "zh_TW": "版本" - } - }, - { - "ID": "GameListHeaderTimePlayed", + "ID": "GameListSortTimePlayed", "Translations": { "ar_SA": "وقت اللعب", "de_DE": "Spielzeit", @@ -1598,7 +1723,7 @@ } }, { - "ID": "GameListHeaderLastPlayed", + "ID": "GameListSortLastPlayed", "Translations": { "ar_SA": "آخر مرة لُعبت", "de_DE": "Zuletzt gespielt", @@ -1623,7 +1748,7 @@ } }, { - "ID": "GameListHeaderFileExtension", + "ID": "GameListSortFileExtension", "Translations": { "ar_SA": "صيغة الملف", "de_DE": "Dateiformat", @@ -1648,7 +1773,7 @@ } }, { - "ID": "GameListHeaderFileSize", + "ID": "GameListSortFileSize", "Translations": { "ar_SA": "حجم الملف", "de_DE": "Dateigröße", @@ -1673,7 +1798,7 @@ } }, { - "ID": "GameListHeaderPath", + "ID": "GameListSortPath", "Translations": { "ar_SA": "المسار", "de_DE": "Pfad", @@ -1697,6 +1822,106 @@ "zh_TW": "路徑" } }, + { + "ID": "GameListHeaderCompatibilityStatus", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Compatibility:", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderTitleId", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Title ID:", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderHostedGames", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Hosted Games: {0}", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListHeaderPlayerCount", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Online Players: {0}", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuOpenUserSaveDirectory", "Translations": { @@ -2522,6 +2747,106 @@ "zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式" } }, + { + "ID": "GameListContextMenuShowCompatEntry", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show Compatibility Entry", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListContextMenuShowCompatEntryToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show the selected game in the Compatibility List you can normally access via the Help menu.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListContextMenuShowGameData", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show Game Info", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListContextMenuShowGameDataToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show stats & details about the currently selected game.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuOpenModsDirectory", "Translations": { @@ -4153,23 +4478,23 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Resync to PC Date & Time", + "en_US": "Match System Time", "es_ES": "", - "fr_FR": "Resynchronier la Date à celle du PC", + "fr_FR": "", "he_IL": "", - "it_IT": "Sincronizza data e ora con il PC", + "it_IT": "", "ja_JP": "", - "ko_KR": "PC 날짜와 시간에 동기화", - "no_NO": "Resynkroniser til PC-dato og -klokkeslett", + "ko_KR": "", + "no_NO": "", "pl_PL": "", "pt_BR": "", - "ru_RU": "Повторная синхронизация с датой и временем на компьютере", - "sv_SE": "Återsynka till datorns datum och tid", + "ru_RU": "", + "sv_SE": "", "th_TH": "", "tr_TR": "", - "uk_UA": "Синхронізувати з датою та часом ПК", - "zh_CN": "与 PC 日期和时间重新同步", - "zh_TW": "重新同步至 PC 的日期和時間" + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { @@ -15528,23 +15853,23 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Resync System Time to match your PC's current date & time.\n\nThis is not an active setting, it can still fall out of sync; in which case just click this button again.", + "en_US": "Sync System Time to match your PC's current date & time.", "es_ES": "", - "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.\n\nCeci n'est pas un paramètrage automatique, la date peut se désynchroniser; dans ce cas là, rappuyer sur le boutton.", + "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.", "he_IL": "", - "it_IT": "Sincronizza data e ora del sistema con quelle del PC.\n\nQuesta non è un'opzione attiva, perciò data e ora potrebbero tornare a non essere sincronizzate: in tal caso basterà cliccare nuovamente questo pulsante.", + "it_IT": "Sincronizza data e ora del sistema con quelle del PC.", "ja_JP": "", - "ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.\n\n이 설정은 활성 설정이 아니므로 여전히 동기화되지 않을 수 있으며, 이 경우 이 버튼을 다시 클릭하면 됩니다.", - "no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett. \\Dette er ikke en aktiv innstilling, men den kan likevel komme ut av synkronisering; i så fall er det bare å klikke på denne knappen igjen.", + "ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.", + "no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.", "pl_PL": "", "pt_BR": "", - "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.\n\nЭто не активная настройка, она все еще может рассинхронизироваться; в этом случае просто нажмите эту кнопку еще раз.", - "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.\n\nDetta är inte en aktiv inställning och den kan tappa synken och om det händer så kan du klicka på denna knapp igen.", + "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.", + "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.", "th_TH": "", "tr_TR": "", - "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.", - "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间,时间仍然可能不同步;在这种情况下,只需再次单击此按钮即可。", - "zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。\n\n這不是一個主動設定,它仍然可能會失去同步;在這種情況下,只需再次點擊此按鈕。" + "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.", + "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。", + "zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。" } }, { @@ -23044,7 +23369,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "可游玩", - "zh_TW": "可暢順遊玩 (Playable)" + "zh_TW": "可暢順遊玩" } }, { @@ -23069,7 +23394,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "进入游戏", - "zh_TW": "大致可遊玩 (Ingame)" + "zh_TW": "大致可遊玩" } }, { @@ -23094,7 +23419,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "菜单", - "zh_TW": "只開啟至遊戲開始功能表 (Menus)" + "zh_TW": "只開啟至遊戲開始功能表" } }, { @@ -23119,7 +23444,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "启动", - "zh_TW": "只能啟動 (Boots)" + "zh_TW": "只能啟動" } }, { @@ -23144,7 +23469,132 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "什么都没有", - "zh_TW": "無法啟動 (Nothing)" + "zh_TW": "無法啟動" + } + }, + { + "ID": "CompatibilityListPlayableTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListIngameTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListMenusTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots and goes past the title screen but does not make it into main gameplay.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListBootsTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Boots but does not make it past the title screen.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "CompatibilityListNothingTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Does not boot or shows no signs of activity.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 70ebedfa4..20b296511 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -1,11 +1,19 @@ using DiscordRPC; using Gommon; +using MsgPack; using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Common; +using Ryujinx.Common.Helper; +using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; +using Ryujinx.Horizon; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Text; namespace Ryujinx.Ava @@ -16,12 +24,12 @@ namespace Ryujinx.Ava public static Timestamps GuestAppStartedAt { get; set; } private static string VersionString - => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; + => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; - private static readonly string _description = - ReleaseInformation.IsValid - ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" - : "dev build"; + private static readonly string _description = + ReleaseInformation.IsValid + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" + : "dev build"; private const string ApplicationId = "1293250299716173864"; @@ -30,6 +38,8 @@ namespace Ryujinx.Ava private static DiscordRpcClient _discordClient; private static RichPresence _discordPresenceMain; + private static RichPresence _discordPresencePlaying; + private static ApplicationMetadata _currentApp; public static void Initialize() { @@ -37,8 +47,7 @@ namespace Ryujinx.Ava { Assets = new Assets { - LargeImageKey = "ryujinx", - LargeImageText = TruncateToByteLength(_description) + LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description) }, Details = "Main Menu", State = "Idling", @@ -47,6 +56,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue); + HorizonStatic.PlayReport += HandlePlayReport; } private static void Update(object sender, ReactiveEventArgs evnt) @@ -77,16 +87,15 @@ namespace Ryujinx.Ava { if (titleId.TryGet(out string tid)) SwitchToPlayingState( - ApplicationLibrary.LoadAndSaveMetaData(tid), + ApplicationLibrary.LoadAndSaveMetaData(tid), Switch.Shared.Processes.ActiveApplication ); - else + else SwitchToMainState(); } - private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) - { - _discordClient?.SetPresence(new RichPresence + private static RichPresence CreatePlayingState(ApplicationMetadata appMeta, ProcessResult procRes) => + new() { Assets = new Assets { @@ -100,10 +109,42 @@ namespace Ryujinx.Ava ? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}" : "Never played", Timestamps = GuestAppStartedAt ??= Timestamps.Now - }); + }; + + private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) + { + _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes)); + _currentApp = appMeta; } - private static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain); + private static void SwitchToMainState() + { + _discordClient?.SetPresence(_discordPresenceMain); + _discordPresencePlaying = null; + _currentApp = null; + } + + private static void HandlePlayReport(MessagePackObject playReport) + { + if (_discordClient is null) return; + if (!TitleIDs.CurrentApplication.Value.HasValue) return; + if (_discordPresencePlaying is null) return; + + PlayReportAnalyzer.FormattedValue formattedValue = + PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport); + + if (!formattedValue.Handled) return; + + _discordPresencePlaying.Details = formattedValue.Reset + ? $"Playing {_currentApp.Title}" + : formattedValue.FormattedString; + + if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details)) + return; //don't trigger an update if the set presence Details are identical to current + + _discordClient.SetPresence(_discordPresencePlaying); + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); + } private static string TruncateToByteLength(string input) { diff --git a/src/Ryujinx/RyujinxApp.axaml.cs b/src/Ryujinx/RyujinxApp.axaml.cs index be24315f6..32318776a 100644 --- a/src/Ryujinx/RyujinxApp.axaml.cs +++ b/src/Ryujinx/RyujinxApp.axaml.cs @@ -32,6 +32,9 @@ namespace Ryujinx.Ava public static MainWindow MainWindow => Current! .ApplicationLifetime.Cast() .MainWindow.Cast(); + + public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current! + .ApplicationLifetime.Cast(); public static bool IsClipboardAvailable(out IClipboard clipboard) { diff --git a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs index 75a9b3d41..dd5b7d9f1 100644 --- a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs +++ b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs @@ -144,12 +144,12 @@ namespace Ryujinx.Ava.UI.Controls case KeyboardMode.Numeric: localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric); validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); - _checkInput = text => text.All(NumericCharacterValidation.IsNumeric); + _checkInput = text => text.All(CharacterValidation.IsNumeric); break; case KeyboardMode.Alphabet: localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet); validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText); - _checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value)); + _checkInput = text => text.All(value => !CharacterValidation.IsCJK(value)); break; case KeyboardMode.ASCII: localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII); diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 475b26787..3e47a1910 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -19,6 +19,17 @@ Header="{ext:Locale GameListContextMenuCreateShortcut}" Icon="{ext:Icon fa-solid fa-bookmark}" ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" /> + + - diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index 55a530e1c..e55e74455 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -12,6 +12,7 @@ using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities.AppLibrary; +using Ryujinx.Ava.Utilities.Compat; using Ryujinx.Common.Configuration; using Ryujinx.Common.Helper; using Ryujinx.HLE.HOS; @@ -333,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) return; - DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary); + DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary); if (selectedDlc is not null) { @@ -385,6 +386,18 @@ namespace Ryujinx.Ava.UI.Controls viewModel.SelectedApplication.Icon ); } + + public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await CompatibilityList.Show(viewModel.SelectedApplication.IdString); + } + + public async void OpenApplicationData_Click(object sender, RoutedEventArgs args) + { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) + await ApplicationDataView.Show(viewModel.SelectedApplication); + } public async void RunApplication_Click(object sender, RoutedEventArgs args) { @@ -394,12 +407,8 @@ namespace Ryujinx.Ava.UI.Controls public async void TrimXCI_Click(object sender, RoutedEventArgs args) { - MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; - - if (viewModel?.SelectedApplication != null) - { + if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path); - } } } } diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml new file mode 100644 index 000000000..45ae75639 --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs new file mode 100644 index 000000000..e85e1188e --- /dev/null +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs @@ -0,0 +1,86 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Ava.Utilities.AppLibrary; +using Ryujinx.Ava.Utilities.Compat; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class ApplicationDataView : UserControl + { + public static async Task Show(ApplicationData appData) + { + ContentDialog contentDialog = new() + { + Title = appData.Name, + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], + MinWidth = 256, + Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) } + }; + + Style closeButton = new(x => x.Name("CloseButton")); + closeButton.Setters.Add(new Setter(WidthProperty, 160d)); + + Style closeButtonParent = new(x => x.Name("CommandSpace")); + closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, + Avalonia.Layout.HorizontalAlignment.Center)); + + contentDialog.Styles.Add(closeButton); + contentDialog.Styles.Add(closeButtonParent); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + public ApplicationDataView() + { + InitializeComponent(); + } + + private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e) + { + if (sender is not Button { Content: TextBlock playabilityLabel }) + return; + + if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window)) + window.Close(ContentDialogResult.None); + + await CompatibilityList.Show((string)playabilityLabel.Tag); + } + + private async void IdString_OnClick(object sender, RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel mwvm) + return; + + if (sender is not Button { Content: TextBlock idText }) + return; + + if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard)) + return; + + ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text); + if (appData is null) + return; + + await clipboard.SetTextAsync(appData.IdString); + + NotificationHelper.ShowInformation( + "Copied Title ID", + $"{appData.Name} ({appData.IdString})"); + } + } +} + diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index 9b87805e4..733500338 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -86,6 +86,30 @@ Text="{Binding Version}" TextAlignment="Start" TextWrapping="Wrap" /> + diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs index fb09adf23..7c6b0cf15 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -5,7 +5,9 @@ using Avalonia.Interactivity; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.Utilities.AppLibrary; +using Ryujinx.Ava.Utilities.Compat; using System; +using System.Globalization; using System.Linq; namespace Ryujinx.Ava.UI.Controls @@ -28,6 +30,17 @@ namespace Ryujinx.Ava.UI.Controls if (sender is ListBox { SelectedItem: ApplicationData selected }) RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); } + + private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e) + { + if (DataContext is not MainWindowViewModel mwvm) + return; + + if (sender is not Button { Content: TextBlock playabilityLabel }) + return; + + await CompatibilityList.Show((string)playabilityLabel.Tag); + } private async void IdString_OnClick(object sender, RoutedEventArgs e) { diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs index b5d085ba1..ae83f9e98 100644 --- a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -159,6 +159,7 @@ namespace Ryujinx.Ava.UI.Helpers Symbol = (Symbol)symbol, Margin = new Thickness(10), FontSize = 40, + FlowDirection = FlowDirection.LeftToRight, VerticalAlignment = VerticalAlignment.Center, }; diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs index 47d0b94d0..7694e8883 100644 --- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs @@ -1,27 +1,31 @@ using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; +using Gommon; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Globalization; +using System.Text; namespace Ryujinx.Ava.UI.Helpers { internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter { - private static readonly MultiplayerInfoConverter _instance = new(); + public static readonly MultiplayerInfoConverter Instance = new(); public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is ApplicationData applicationData) - { - if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0) - { - return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; - } - } - - return ""; + if (value is not ApplicationData { HasLdnGames: true } applicationData) + return ""; + return new StringBuilder() + .AppendLine( + LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames] + .Format(applicationData.GameCount)) + .Append( + LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount] + .Format(applicationData.PlayerCount)) + .ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) @@ -31,7 +35,7 @@ namespace Ryujinx.Ava.UI.Helpers public override object ProvideValue(IServiceProvider serviceProvider) { - return _instance; + return Instance; } } } diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs index eaf6f7bdf..9e92d9289 100644 --- a/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Helper; using SharpMetal.QuartzCore; using System; @@ -7,14 +8,12 @@ namespace Ryujinx.Ava.UI.Renderer { public CAMetalLayer CreateSurface() { - if (OperatingSystem.IsMacOS()) + if (OperatingSystem.IsMacOS() && RunningPlatform.IsArm) { return new CAMetalLayer(MetalLayer); } - else - { - throw new NotSupportedException(); - } + + throw new NotSupportedException($"Cannot create a {nameof(CAMetalLayer)} without being on ARM Mac."); } } } diff --git a/src/Ryujinx/UI/Renderer/RendererHost.cs b/src/Ryujinx/UI/Renderer/RendererHost.cs index 7dfec8d62..f755b6d70 100644 --- a/src/Ryujinx/UI/Renderer/RendererHost.cs +++ b/src/Ryujinx/UI/Renderer/RendererHost.cs @@ -43,19 +43,19 @@ namespace Ryujinx.Ava.UI.Renderer public RendererHost(string titleId) { - switch (TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend)) - { - case GraphicsBackend.OpenGl: - EmbeddedWindow = new EmbeddedWindowOpenGL(); - break; - case GraphicsBackend.Metal: - EmbeddedWindow = new EmbeddedWindowMetal(); - break; - case GraphicsBackend.Vulkan: - EmbeddedWindow = new EmbeddedWindowVulkan(); - break; - } - + Focusable = true; + FlowDirection = FlowDirection.LeftToRight; + + EmbeddedWindow = +#pragma warning disable CS8509 + TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend) switch +#pragma warning restore CS8509 + { + GraphicsBackend.OpenGl => new EmbeddedWindowOpenGL(), + GraphicsBackend.Metal => new EmbeddedWindowMetal(), + GraphicsBackend.Vulkan => new EmbeddedWindowVulkan(), + }; + string backendText = EmbeddedWindow switch { EmbeddedWindowVulkan => "Vulkan", diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs new file mode 100644 index 000000000..9e0a3554a --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs @@ -0,0 +1,26 @@ +using Gommon; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Utilities.AppLibrary; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class ApplicationDataViewModel : BaseModel + { + public ApplicationData AppData { get; } + + public ApplicationDataViewModel(ApplicationData appData) => AppData = appData; + + 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); + public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString); + public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString); + public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString); + + public string FormattedLdnInfo => + $"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" + + $"\n" + + $"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}"; + } +} diff --git a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs index d50d8249a..b486aa766 100644 --- a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs @@ -14,9 +14,7 @@ namespace Ryujinx.Ava.UI.ViewModels public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary) { - _dlcs = appLibrary.DownloadableContents.Items - .Where(x => x.Dlc.TitleIdBase == titleId) - .Select(x => x.Dlc) + _dlcs = appLibrary.FindDlcsFor(titleId) .OrderBy(it => it.IsBundled ? 0 : 1) .ThenBy(it => it.TitleId) .ToArray(); diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 1533b7d5d..a16a06ff5 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -69,8 +69,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { - IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items - .Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase); + (DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id); bool hasBundledContent = false; foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 3a6fc8d2f..499eedc8d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -349,6 +349,10 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo; + + public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id); + public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; @@ -629,15 +633,15 @@ namespace Ryujinx.Ava.UI.ViewModels { return SortMode switch { - ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], - ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], - ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], - ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed], - ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], - ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], - ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel], + ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], + ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper], + ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed], + ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed], + ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListSortFileExtension], + ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListSortFileSize], + ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListSortPath], _ => string.Empty, }; } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index f2e3aee23..1f2980eee 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -2,7 +2,7 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; -using Gommon; +using CommunityToolkit.Mvvm.Input; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL3; @@ -16,6 +16,7 @@ using Ryujinx.Ava.Utilities.Configuration.System; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.GraphicsDriver; +using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Vulkan; @@ -27,8 +28,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using System.Threading.Tasks; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; @@ -115,10 +114,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); - public bool IsAppleSiliconMac => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; - - public bool IsMacOS => OperatingSystem.IsMacOS(); - public bool EnableDiscordIntegration { get; set; } public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } @@ -200,7 +195,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableTextureRecompression { get; set; } public bool EnableMacroHLE { get; set; } public bool EnableColorSpacePassthrough { get; set; } - public bool ColorSpacePassthroughAvailable => IsMacOS; + public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS; public bool EnableFileLog { get; set; } public bool EnableStub { get; set; } public bool EnableInfo { get; set; } @@ -296,6 +291,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } + [ObservableProperty] private bool _matchSystemTime; + public DateTimeOffset CurrentDate { get; set; } public TimeSpan CurrentTime { get; set; } @@ -330,9 +327,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] - private static partial Regex LdnPassphraseRegex(); - public bool IsInvalidLdnPassphraseVisible { get; set; } public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() @@ -414,17 +408,6 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex))); } - public void MatchSystemTime() - { - (DateTimeOffset dto, TimeSpan timeOfDay) = DateTimeOffset.Now.Extract(); - - CurrentDate = dto; - CurrentTime = timeOfDay; - - OnPropertyChanged(nameof(CurrentDate)); - OnPropertyChanged(nameof(CurrentTime)); - } - public async Task LoadTimeZones() { _timeZoneContentManager = new TimeZoneContentManager(); @@ -470,7 +453,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool ValidateLdnPassphrase(string passphrase) { - return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase)); + return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && Patterns.LdnPassphrase.IsMatch(passphrase)); } public void ValidateAndSetTimeZone(string location) @@ -526,7 +509,9 @@ namespace Ryujinx.Ava.UI.ViewModels CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay; - EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value; + MatchSystemTime = config.System.MatchSystemTime; + + EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval; CustomVSyncInterval = config.Graphics.CustomVSyncInterval; VSyncMode = config.Graphics.VSyncMode; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; @@ -631,6 +616,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.TimeZone.Value = TimeZone; } + config.System.MatchSystemTime.Value = MatchSystemTime; config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.DramSize.Value = DramSize; @@ -734,6 +720,25 @@ namespace Ryujinx.Ava.UI.ViewModels CloseWindow?.Invoke(); } + [ObservableProperty] private bool _wantsToReset; + + public AsyncRelayCommand ResetButton => Commands.Create(async () => + { + if (!WantsToReset) return; + + CloseWindow?.Invoke(); + ConfigurationState.Instance.LoadDefault(); + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + RyujinxApp.MainWindow.LoadApplications(); + + await ContentDialogHelper.CreateInfoDialog( + $"Your {RyujinxApp.FullAppName} configuration has been reset.", + "", + string.Empty, + LocaleManager.Instance[LocaleKeys.SettingsButtonClose], + "Configuration Reset"); + }); + public void CancelButton() { RevertIfNotSaved(); diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index aaafc3913..2b88aceed 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -41,8 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { - IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items - .Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase); + (TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id); bool hasBundledContent = false; SelectedUpdate = new TitleUpdateViewModelNoUpdate(); diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 92b81aeeb..17a27ef86 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes); XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show); AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show); - CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show); + CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show()); UpdateMenuItem.Command = Commands.Create(async () => { diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml index cdc66a138..db557b417 100644 --- a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml @@ -113,37 +113,37 @@ Tag="TitleId" /> diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml index 83f908a9c..62f087510 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel"> @@ -69,7 +70,7 @@ diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index a2559f393..42515a4e9 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -8,6 +8,7 @@ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" Design.Width="1000" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel"> @@ -48,7 +49,7 @@ - + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index d05f3b7bf..aa7144cf1 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -170,7 +170,8 @@ ToolTip.Tip="{ext:Locale TimeTooltip}" Width="250"/> @@ -181,17 +182,21 @@ - + Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" + ToolTip.Tip="{ext:Locale MatchTimeTooltip}" + Width="250"/> + ViewModel.MatchSystemTime(); } } diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index 59302b6fc..ec2b27265 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -10,6 +10,7 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" Width="1100" Height="768" MinWidth="800" @@ -107,24 +108,36 @@ - -