Compare commits
33 Commits
Canary-1.2
...
Canary-1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11bc32d98e | ||
|
|
063430ea16 | ||
|
|
65f08caaa3 | ||
|
|
f225b18c05 | ||
|
|
d8549f687b | ||
|
|
5ab50680b4 | ||
|
|
a0edc5c2b0 | ||
|
|
158ea7b4d6 | ||
|
|
8bc3de8303 | ||
|
|
c812106611 | ||
|
|
11e4d8f970 | ||
|
|
774edb7b29 | ||
|
|
55536f5d78 | ||
|
|
b2eecd28ce | ||
|
|
fe43c32e60 | ||
|
|
8117e160c2 | ||
|
|
bf713a80d6 | ||
|
|
b38b5a1e70 | ||
|
|
2d7700949c | ||
|
|
ea2287af03 | ||
|
|
37af8c70aa | ||
|
|
50cee3fd19 | ||
|
|
a46aacf2e2 | ||
|
|
ad9d6588e8 | ||
|
|
38ef65aae0 | ||
|
|
9f94aa1c79 | ||
|
|
2c9a26c11c | ||
|
|
a4a15a4c80 | ||
|
|
cc3b95eee1 | ||
|
|
2ab806f759 | ||
|
|
6d75410bd2 | ||
|
|
196b2eaf66 | ||
|
|
82fe519766 |
@@ -2483,7 +2483,7 @@
|
|||||||
0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23
|
0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23
|
||||||
0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56
|
0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56
|
||||||
010003A00D0B4000,"SaGa SCARLET GRACE: AMBITIONS™",,playable,2022-10-06 13:20:31
|
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
|
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
|
01007F000EB36000,"Sakai and...",nvdec,playable,2022-12-15 13:53:19
|
||||||
0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13
|
0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13
|
||||||
|
|||||||
|
118
src/Ryujinx.Common/Helpers/Patterns.cs
Normal file
118
src/Ryujinx.Common/Helpers/Patterns.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,17 @@ namespace Ryujinx.Common.Helper
|
|||||||
public static bool IsWindows => OperatingSystem.IsWindows();
|
public static bool IsWindows => OperatingSystem.IsWindows();
|
||||||
public static bool IsLinux => OperatingSystem.IsLinux();
|
public static bool IsLinux => OperatingSystem.IsLinux();
|
||||||
|
|
||||||
public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64;
|
public static bool IsArm => RuntimeInformation.OSArchitecture is Architecture.Arm64;
|
||||||
public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64;
|
|
||||||
|
|
||||||
public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64);
|
public static bool IsX64 => RuntimeInformation.OSArchitecture is Architecture.X64;
|
||||||
public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
|
|
||||||
|
|
||||||
public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64);
|
public static bool IsIntelMac => IsMacOS && IsX64;
|
||||||
public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
|
public static bool IsArmMac => IsMacOS && IsArm;
|
||||||
|
|
||||||
|
public static bool IsX64Windows => IsWindows && IsX64;
|
||||||
|
public static bool IsArmWindows => IsWindows && IsArm;
|
||||||
|
|
||||||
|
public static bool IsX64Linux => IsLinux && IsX64;
|
||||||
|
public static bool IsArmLinux => IsLinux && IsArmMac;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ namespace Ryujinx.Common
|
|||||||
"01006f8002326000", // Animal Crossings: New Horizons
|
"01006f8002326000", // Animal Crossings: New Horizons
|
||||||
"01009bf0072d4000", // Captain Toad: Treasure Tracker
|
"01009bf0072d4000", // Captain Toad: Treasure Tracker
|
||||||
"01009510001ca000", // Fast RMX
|
"01009510001ca000", // Fast RMX
|
||||||
"01005CA01580E000", // Persona 5 Royale
|
"01005CA01580E000", // Persona 5 Royal
|
||||||
|
"0100b880154fc000", // Persona 5 The Royal (Japan)
|
||||||
"010015100b514000", // Super Mario Bros. Wonder
|
"010015100b514000", // Super Mario Bros. Wonder
|
||||||
"0100000000010000", // Super Mario Odyssey
|
"0100000000010000", // Super Mario Odyssey
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
|
|||||||
}
|
}
|
||||||
|
|
||||||
using ManualResetEvent waitEvent = new(false);
|
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)
|
if (info == null)
|
||||||
{
|
{
|
||||||
@@ -96,7 +96,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
|
|||||||
|
|
||||||
bool signaled = waitEvent.WaitOne(timeout);
|
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...");
|
Logger.Error?.Print(LogClass.Gpu, $"Wait on syncpoint {id} for threshold {threshold} took more than {timeout.TotalMilliseconds}ms, resuming execution...");
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
Unknown,
|
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)
|
public static Vendor FromId(uint id)
|
||||||
{
|
{
|
||||||
return id switch
|
return id switch
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Gommon;
|
using Gommon;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.Shader;
|
using Ryujinx.Graphics.Shader;
|
||||||
@@ -375,11 +376,11 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}";
|
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)
|
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))
|
if (match != null && int.TryParse(match.Groups[2].Value, out int gpuNumber))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using LibHac.FsSystem;
|
|||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
||||||
using Ryujinx.HLE.HOS.SystemState;
|
using Ryujinx.HLE.HOS.SystemState;
|
||||||
@@ -30,9 +31,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error
|
|||||||
|
|
||||||
public event EventHandler AppletStateChanged;
|
public event EventHandler AppletStateChanged;
|
||||||
|
|
||||||
[GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")]
|
|
||||||
private static partial Regex CleanTextRegex();
|
|
||||||
|
|
||||||
public ErrorApplet(Horizon horizon)
|
public ErrorApplet(Horizon horizon)
|
||||||
{
|
{
|
||||||
_horizon = horizon;
|
_horizon = horizon;
|
||||||
@@ -107,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Applets.Error
|
|||||||
|
|
||||||
private static string CleanText(string value)
|
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)
|
private string GetMessageText(uint module, uint description, string key)
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,13 @@
|
|||||||
|
using Ryujinx.Common.Helper;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Proxy
|
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)
|
public static bool IsHostBlocked(string host)
|
||||||
{
|
{
|
||||||
foreach (Regex regex in _blockedHosts)
|
foreach (Regex regex in Patterns.BlockedHosts)
|
||||||
{
|
{
|
||||||
if (regex.IsMatch(host))
|
if (regex.IsMatch(host))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using LibHac.Common.FixedArrays;
|
|||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Loader;
|
using LibHac.Loader;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -29,13 +30,6 @@ namespace Ryujinx.HLE.Loaders.Executables
|
|||||||
public string Name;
|
public string Name;
|
||||||
public Array32<byte> BuildId;
|
public Array32<byte> 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)
|
public NsoExecutable(IStorage inStorage, string name = null)
|
||||||
{
|
{
|
||||||
NsoReader reader = new();
|
NsoReader reader = new();
|
||||||
@@ -90,7 +84,7 @@ namespace Ryujinx.HLE.Loaders.Executables
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(modulePath))
|
if (string.IsNullOrEmpty(modulePath))
|
||||||
{
|
{
|
||||||
Match moduleMatch = ModuleRegex().Match(rawTextBuffer);
|
Match moduleMatch = Patterns.Module.Match(rawTextBuffer);
|
||||||
if (moduleMatch.Success)
|
if (moduleMatch.Success)
|
||||||
{
|
{
|
||||||
modulePath = moduleMatch.Value;
|
modulePath = moduleMatch.Value;
|
||||||
@@ -99,13 +93,13 @@ namespace Ryujinx.HLE.Loaders.Executables
|
|||||||
|
|
||||||
stringBuilder.AppendLine($" Module: {modulePath}");
|
stringBuilder.AppendLine($" Module: {modulePath}");
|
||||||
|
|
||||||
Match fsSdkMatch = FsSdkRegex().Match(rawTextBuffer);
|
Match fsSdkMatch = Patterns.FsSdk.Match(rawTextBuffer);
|
||||||
if (fsSdkMatch.Success)
|
if (fsSdkMatch.Success)
|
||||||
{
|
{
|
||||||
stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", string.Empty)}");
|
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)
|
if (sdkMwMatches.Count != 0)
|
||||||
{
|
{
|
||||||
string libHeader = " SDK Libraries: ";
|
string libHeader = " SDK Libraries: ";
|
||||||
|
|||||||
@@ -1,31 +1,37 @@
|
|||||||
|
using MsgPack;
|
||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Ryujinx.Horizon
|
namespace Ryujinx.Horizon
|
||||||
{
|
{
|
||||||
public static class HorizonStatic
|
public static class HorizonStatic
|
||||||
{
|
{
|
||||||
[ThreadStatic]
|
internal static void HandlePlayReport(MessagePackObject report) =>
|
||||||
private static HorizonOptions _options;
|
new Thread(() => PlayReport?.Invoke(report))
|
||||||
|
{
|
||||||
|
Name = "HLE.PlayReportEvent",
|
||||||
|
IsBackground = true,
|
||||||
|
Priority = ThreadPriority.AboveNormal
|
||||||
|
}.Start();
|
||||||
|
|
||||||
[ThreadStatic]
|
public static event Action<MessagePackObject> PlayReport;
|
||||||
private static ISyscallApi _syscall;
|
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static IVirtualMemoryManager _addressSpace;
|
public static HorizonOptions Options { get; private set; }
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static IThreadContext _threadContext;
|
public static ISyscallApi Syscall { get; private set; }
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static int _threadHandle;
|
public static IVirtualMemoryManager AddressSpace { get; private set; }
|
||||||
|
|
||||||
public static HorizonOptions Options => _options;
|
[field: ThreadStatic]
|
||||||
public static ISyscallApi Syscall => _syscall;
|
public static IThreadContext ThreadContext { get; private set; }
|
||||||
public static IVirtualMemoryManager AddressSpace => _addressSpace;
|
|
||||||
public static IThreadContext ThreadContext => _threadContext;
|
[field: ThreadStatic]
|
||||||
public static int CurrentThreadHandle => _threadHandle;
|
public static int CurrentThreadHandle { get; private set; }
|
||||||
|
|
||||||
public static void Register(
|
public static void Register(
|
||||||
HorizonOptions options,
|
HorizonOptions options,
|
||||||
@@ -34,11 +40,11 @@ namespace Ryujinx.Horizon
|
|||||||
IThreadContext threadContext,
|
IThreadContext threadContext,
|
||||||
int threadHandle)
|
int threadHandle)
|
||||||
{
|
{
|
||||||
_options = options;
|
Options = options;
|
||||||
_syscall = syscallApi;
|
Syscall = syscallApi;
|
||||||
_addressSpace = addressSpace;
|
AddressSpace = addressSpace;
|
||||||
_threadContext = threadContext;
|
ThreadContext = threadContext;
|
||||||
_threadHandle = threadHandle;
|
CurrentThreadHandle = threadHandle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Gommon;
|
||||||
using MsgPack;
|
using MsgPack;
|
||||||
using MsgPack.Serialization;
|
using MsgPack.Serialization;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
@@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf;
|
|||||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
|
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
|
||||||
|
|
||||||
namespace Ryujinx.Horizon.Prepo.Ipc
|
namespace Ryujinx.Horizon.Prepo.Ipc
|
||||||
@@ -231,6 +233,8 @@ namespace Ryujinx.Horizon.Prepo.Ipc
|
|||||||
builder.AppendLine($" Room: {gameRoom}");
|
builder.AppendLine($" Room: {gameRoom}");
|
||||||
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
|
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
|
||||||
|
|
||||||
|
HorizonStatic.HandlePlayReport(deserializedReport);
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
|
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|||||||
@@ -938,7 +938,9 @@ namespace Ryujinx.Ava
|
|||||||
ConfigurationState.Instance.System.EnableInternetAccess,
|
ConfigurationState.Instance.System.EnableInternetAccess,
|
||||||
ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
|
ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
|
||||||
ConfigurationState.Instance.System.FsGlobalAccessLogMode,
|
ConfigurationState.Instance.System.FsGlobalAccessLogMode,
|
||||||
ConfigurationState.Instance.System.SystemTimeOffset,
|
ConfigurationState.Instance.System.MatchSystemTime
|
||||||
|
? 0
|
||||||
|
: ConfigurationState.Instance.System.SystemTimeOffset,
|
||||||
ConfigurationState.Instance.System.TimeZone,
|
ConfigurationState.Instance.System.TimeZone,
|
||||||
ConfigurationState.Instance.System.MemoryManagerMode,
|
ConfigurationState.Instance.System.MemoryManagerMode,
|
||||||
ConfigurationState.Instance.System.IgnoreMissingServices,
|
ConfigurationState.Instance.System.IgnoreMissingServices,
|
||||||
|
|||||||
@@ -4153,23 +4153,23 @@
|
|||||||
"ar_SA": "",
|
"ar_SA": "",
|
||||||
"de_DE": "",
|
"de_DE": "",
|
||||||
"el_GR": "",
|
"el_GR": "",
|
||||||
"en_US": "Resync to PC Date & Time",
|
"en_US": "Match System Time",
|
||||||
"es_ES": "",
|
"es_ES": "",
|
||||||
"fr_FR": "Resynchronier la Date à celle du PC",
|
"fr_FR": "",
|
||||||
"he_IL": "",
|
"he_IL": "",
|
||||||
"it_IT": "Sincronizza data e ora con il PC",
|
"it_IT": "",
|
||||||
"ja_JP": "",
|
"ja_JP": "",
|
||||||
"ko_KR": "PC 날짜와 시간에 동기화",
|
"ko_KR": "",
|
||||||
"no_NO": "Resynkroniser til PC-dato og -klokkeslett",
|
"no_NO": "",
|
||||||
"pl_PL": "",
|
"pl_PL": "",
|
||||||
"pt_BR": "",
|
"pt_BR": "",
|
||||||
"ru_RU": "Повторная синхронизация с датой и временем на компьютере",
|
"ru_RU": "",
|
||||||
"sv_SE": "Återsynka till datorns datum och tid",
|
"sv_SE": "",
|
||||||
"th_TH": "",
|
"th_TH": "",
|
||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "Синхронізувати з датою та часом ПК",
|
"uk_UA": "",
|
||||||
"zh_CN": "与 PC 日期和时间重新同步",
|
"zh_CN": "",
|
||||||
"zh_TW": "重新同步至 PC 的日期和時間"
|
"zh_TW": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -11475,126 +11475,126 @@
|
|||||||
{
|
{
|
||||||
"ID": "DialogConfirmationTitle",
|
"ID": "DialogConfirmationTitle",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - تأكيد",
|
"ar_SA": "{0} - تأكيد",
|
||||||
"de_DE": "Ryujinx - Bestätigung",
|
"de_DE": "{0} - Bestätigung",
|
||||||
"el_GR": "Ryujinx - Επιβεβαίωση",
|
"el_GR": "{0} - Επιβεβαίωση",
|
||||||
"en_US": "Ryujinx - Confirmation",
|
"en_US": "{0} - Confirmation",
|
||||||
"es_ES": "Ryujinx - Confirmación",
|
"es_ES": "{0} - Confirmación",
|
||||||
"fr_FR": "",
|
"fr_FR": "",
|
||||||
"he_IL": "ריוג'ינקס - אישור",
|
"he_IL": "{0} - אישור",
|
||||||
"it_IT": "Ryujinx - Conferma",
|
"it_IT": "{0} - Conferma",
|
||||||
"ja_JP": "Ryujinx - 確認",
|
"ja_JP": "{0} - 確認",
|
||||||
"ko_KR": "Ryujinx - 확인",
|
"ko_KR": "{0} - 확인",
|
||||||
"no_NO": "Ryujinx - Bekreftelse",
|
"no_NO": "{0} - Bekreftelse",
|
||||||
"pl_PL": "Ryujinx - Potwierdzenie",
|
"pl_PL": "{0} - Potwierdzenie",
|
||||||
"pt_BR": "Ryujinx - Confirmação",
|
"pt_BR": "{0} - Confirmação",
|
||||||
"ru_RU": "Ryujinx - Подтверждение",
|
"ru_RU": "{0} - Подтверждение",
|
||||||
"sv_SE": "Ryujinx - Bekräftelse",
|
"sv_SE": "{0} - Bekräftelse",
|
||||||
"th_TH": "Ryujinx - ยืนยัน",
|
"th_TH": "{0} - ยืนยัน",
|
||||||
"tr_TR": "Ryujinx - Onay",
|
"tr_TR": "{0} - Onay",
|
||||||
"uk_UA": "Ryujinx - Підтвердження",
|
"uk_UA": "{0} - Підтвердження",
|
||||||
"zh_CN": "Ryujinx - 确认",
|
"zh_CN": "{0} - 确认",
|
||||||
"zh_TW": "Ryujinx - 確認"
|
"zh_TW": "{0} - 確認"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ID": "DialogUpdaterTitle",
|
"ID": "DialogUpdaterTitle",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - المحدث",
|
"ar_SA": "{0} - المحدث",
|
||||||
"de_DE": "",
|
"de_DE": "",
|
||||||
"el_GR": "Ryujinx - Ενημερωτής",
|
"el_GR": "{0} - Ενημερωτής",
|
||||||
"en_US": "Ryujinx - Updater",
|
"en_US": "{0} - Updater",
|
||||||
"es_ES": "Ryujinx - Actualizador",
|
"es_ES": "{0} - Actualizador",
|
||||||
"fr_FR": "Ryujinx - Mise à Jour",
|
"fr_FR": "{0} - Mise à Jour",
|
||||||
"he_IL": "ריוג'ינקס - מעדכן",
|
"he_IL": "{0} - מעדכן",
|
||||||
"it_IT": "Ryujinx - Aggiornamento",
|
"it_IT": "{0} - Aggiornamento",
|
||||||
"ja_JP": "Ryujinx - アップデータ",
|
"ja_JP": "{0} - アップデータ",
|
||||||
"ko_KR": "Ryujinx - 업데이터",
|
"ko_KR": "{0} - 업데이터",
|
||||||
"no_NO": "Ryujinx – Oppdaterer",
|
"no_NO": "{0} – Oppdaterer",
|
||||||
"pl_PL": "Ryujinx - Asystent aktualizacji",
|
"pl_PL": "{0} - Asystent aktualizacji",
|
||||||
"pt_BR": "Ryujinx - Atualizador",
|
"pt_BR": "{0} - Atualizador",
|
||||||
"ru_RU": "Ryujinx - Обновление",
|
"ru_RU": "{0} - Обновление",
|
||||||
"sv_SE": "Ryujinx - Uppdatering",
|
"sv_SE": "{0} - Uppdatering",
|
||||||
"th_TH": "Ryujinx - อัพเดต",
|
"th_TH": "{0} - อัพเดต",
|
||||||
"tr_TR": "Ryujinx - Güncelleyici",
|
"tr_TR": "{0} - Güncelleyici",
|
||||||
"uk_UA": "Ryujinx - Програма оновлення",
|
"uk_UA": "{0} - Програма оновлення",
|
||||||
"zh_CN": "Ryujinx - 更新",
|
"zh_CN": "{0} - 更新",
|
||||||
"zh_TW": "Ryujinx - 更新程式"
|
"zh_TW": "{0} - 更新程式"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ID": "DialogErrorTitle",
|
"ID": "DialogErrorTitle",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - خطأ",
|
"ar_SA": "{0} - خطأ",
|
||||||
"de_DE": "Ryujinx - Fehler",
|
"de_DE": "{0} - Fehler",
|
||||||
"el_GR": "Ryujinx - Σφάλμα",
|
"el_GR": "{0} - Σφάλμα",
|
||||||
"en_US": "Ryujinx - Error",
|
"en_US": "{0} - Error",
|
||||||
"es_ES": "",
|
"es_ES": "",
|
||||||
"fr_FR": "Ryujinx - Erreur",
|
"fr_FR": "{0} - Erreur",
|
||||||
"he_IL": "ריוג'ינקס - שגיאה",
|
"he_IL": "{0} - שגיאה",
|
||||||
"it_IT": "Ryujinx - Errore",
|
"it_IT": "{0} - Errore",
|
||||||
"ja_JP": "Ryujinx - エラー",
|
"ja_JP": "{0} - エラー",
|
||||||
"ko_KR": "Ryujinx - 오류",
|
"ko_KR": "{0} - 오류",
|
||||||
"no_NO": "Ryujinx - Feil",
|
"no_NO": "{0} - Feil",
|
||||||
"pl_PL": "Ryujinx - Błąd",
|
"pl_PL": "{0} - Błąd",
|
||||||
"pt_BR": "Ryujinx - Erro",
|
"pt_BR": "{0} - Erro",
|
||||||
"ru_RU": "Ryujinx - Ошибка",
|
"ru_RU": "{0} - Ошибка",
|
||||||
"sv_SE": "Ryujinx - Fel",
|
"sv_SE": "{0} - Fel",
|
||||||
"th_TH": "Ryujinx - ผิดพลาด",
|
"th_TH": "{0} - ผิดพลาด",
|
||||||
"tr_TR": "Ryujinx - Hata",
|
"tr_TR": "{0} - Hata",
|
||||||
"uk_UA": "Ryujinx - Помилка",
|
"uk_UA": "{0} - Помилка",
|
||||||
"zh_CN": "Ryujinx - 错误",
|
"zh_CN": "{0} - 错误",
|
||||||
"zh_TW": "Ryujinx - 錯誤"
|
"zh_TW": "{0} - 錯誤"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ID": "DialogWarningTitle",
|
"ID": "DialogWarningTitle",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - تحذير",
|
"ar_SA": "{0} - تحذير",
|
||||||
"de_DE": "Ryujinx - Warnung",
|
"de_DE": "{0} - Warnung",
|
||||||
"el_GR": "Ryujinx - Προειδοποίηση",
|
"el_GR": "{0} - Προειδοποίηση",
|
||||||
"en_US": "Ryujinx - Warning",
|
"en_US": "{0} - Warning",
|
||||||
"es_ES": "Ryujinx - Advertencia",
|
"es_ES": "{0} - Advertencia",
|
||||||
"fr_FR": "Ryujinx - Avertissement",
|
"fr_FR": "{0} - Avertissement",
|
||||||
"he_IL": "ריוג'ינקס - אזהרה",
|
"he_IL": "{0} - אזהרה",
|
||||||
"it_IT": "Ryujinx - Avviso",
|
"it_IT": "{0} - Avviso",
|
||||||
"ja_JP": "Ryujinx - 警告",
|
"ja_JP": "{0} - 警告",
|
||||||
"ko_KR": "Ryujinx - 경고",
|
"ko_KR": "{0} - 경고",
|
||||||
"no_NO": "Ryujinx - Advarsel",
|
"no_NO": "{0} - Advarsel",
|
||||||
"pl_PL": "Ryujinx - Ostrzeżenie",
|
"pl_PL": "{0} - Ostrzeżenie",
|
||||||
"pt_BR": "Ryujinx - Alerta",
|
"pt_BR": "{0} - Alerta",
|
||||||
"ru_RU": "Ryujinx - Предупреждение",
|
"ru_RU": "{0} - Предупреждение",
|
||||||
"sv_SE": "Ryujinx - Varning",
|
"sv_SE": "{0} - Varning",
|
||||||
"th_TH": "Ryujinx - คำเตือน",
|
"th_TH": "{0} - คำเตือน",
|
||||||
"tr_TR": "Ryujinx - Uyarı",
|
"tr_TR": "{0} - Uyarı",
|
||||||
"uk_UA": "Ryujinx - Попередження",
|
"uk_UA": "{0} - Попередження",
|
||||||
"zh_CN": "Ryujinx - 警告",
|
"zh_CN": "{0} - 警告",
|
||||||
"zh_TW": "Ryujinx - 警告"
|
"zh_TW": "{0} - 警告"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ID": "DialogExitTitle",
|
"ID": "DialogExitTitle",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - الخروج",
|
"ar_SA": "{0} - الخروج",
|
||||||
"de_DE": "Ryujinx - Beenden",
|
"de_DE": "{0} - Beenden",
|
||||||
"el_GR": "Ryujinx - Έξοδος",
|
"el_GR": "{0} - Έξοδος",
|
||||||
"en_US": "Ryujinx - Exit",
|
"en_US": "{0} - Exit",
|
||||||
"es_ES": "Ryujinx - Salir",
|
"es_ES": "{0} - Salir",
|
||||||
"fr_FR": "Ryujinx - Quitter",
|
"fr_FR": "{0} - Quitter",
|
||||||
"he_IL": "ריוג'ינקס - יציאה",
|
"he_IL": "{0} - יציאה",
|
||||||
"it_IT": "Ryujinx - Esci",
|
"it_IT": "{0} - Esci",
|
||||||
"ja_JP": "Ryujinx - 終了",
|
"ja_JP": "{0} - 終了",
|
||||||
"ko_KR": "Ryujinx - 종료",
|
"ko_KR": "{0} - 종료",
|
||||||
"no_NO": "Ryujinx - Avslutt",
|
"no_NO": "{0} - Avslutt",
|
||||||
"pl_PL": "Ryujinx - Wyjdź",
|
"pl_PL": "{0} - Wyjdź",
|
||||||
"pt_BR": "Ryujinx - Sair",
|
"pt_BR": "{0} - Sair",
|
||||||
"ru_RU": "Ryujinx - Выход",
|
"ru_RU": "{0} - Выход",
|
||||||
"sv_SE": "Ryujinx - Avslut",
|
"sv_SE": "{0} - Avslut",
|
||||||
"th_TH": "Ryujinx - ออก",
|
"th_TH": "{0} - ออก",
|
||||||
"tr_TR": "Ryujinx - Çıkış",
|
"tr_TR": "{0} - Çıkış",
|
||||||
"uk_UA": "Ryujinx - Вихід",
|
"uk_UA": "{0} - Вихід",
|
||||||
"zh_CN": "Ryujinx - 退出",
|
"zh_CN": "{0} - 退出",
|
||||||
"zh_TW": "Ryujinx - 結束"
|
"zh_TW": "{0} - 結束"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -15553,23 +15553,23 @@
|
|||||||
"ar_SA": "",
|
"ar_SA": "",
|
||||||
"de_DE": "",
|
"de_DE": "",
|
||||||
"el_GR": "",
|
"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": "",
|
"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": "",
|
"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": "",
|
"ja_JP": "",
|
||||||
"ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.\n\n이 설정은 활성 설정이 아니므로 여전히 동기화되지 않을 수 있으며, 이 경우 이 버튼을 다시 클릭하면 됩니다.",
|
"ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.",
|
||||||
"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.",
|
"no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.",
|
||||||
"pl_PL": "",
|
"pl_PL": "",
|
||||||
"pt_BR": "",
|
"pt_BR": "",
|
||||||
"ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.\n\nЭто не активная настройка, она все еще может рассинхронизироваться; в этом случае просто нажмите эту кнопку еще раз.",
|
"ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.",
|
||||||
"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.",
|
"sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.",
|
||||||
"th_TH": "",
|
"th_TH": "",
|
||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.",
|
"uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.",
|
||||||
"zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间,时间仍然可能不同步;在这种情况下,只需再次单击此按钮即可。",
|
"zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。",
|
||||||
"zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。\n\n這不是一個主動設定,它仍然可能會失去同步;在這種情況下,只需再次點擊此按鈕。"
|
"zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -17025,26 +17025,26 @@
|
|||||||
{
|
{
|
||||||
"ID": "DialogStopEmulationTitle",
|
"ID": "DialogStopEmulationTitle",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - إيقاف المحاكاة",
|
"ar_SA": "{0} - إيقاف المحاكاة",
|
||||||
"de_DE": "Ryujinx - Beende Emulation",
|
"de_DE": "{0} - Beende Emulation",
|
||||||
"el_GR": "Ryujinx - Διακοπή εξομοίωσης",
|
"el_GR": "{0} - Διακοπή εξομοίωσης",
|
||||||
"en_US": "Ryujinx - Stop Emulation",
|
"en_US": "{0} - Stop Emulation",
|
||||||
"es_ES": "Ryujinx - Detener emulación",
|
"es_ES": "{0} - Detener emulación",
|
||||||
"fr_FR": "Ryujinx - Arrêt de l'émulation",
|
"fr_FR": "{0} - Arrêt de l'émulation",
|
||||||
"he_IL": "ריוג'ינקס - עצור אמולציה",
|
"he_IL": "{0} - עצור אמולציה",
|
||||||
"it_IT": "Ryujinx - Ferma emulazione",
|
"it_IT": "{0} - Ferma emulazione",
|
||||||
"ja_JP": "Ryujinx - エミュレーションを中止",
|
"ja_JP": "{0} - エミュレーションを中止",
|
||||||
"ko_KR": "Ryujinx - 에뮬레이션 중지",
|
"ko_KR": "{0} - 에뮬레이션 중지",
|
||||||
"no_NO": "Ryujinx - Stopp emulasjon",
|
"no_NO": "{0} - Stopp emulasjon",
|
||||||
"pl_PL": "Ryujinx - Zatrzymaj Emulację",
|
"pl_PL": "{0} - Zatrzymaj Emulację",
|
||||||
"pt_BR": "Ryujinx - Parar emulação",
|
"pt_BR": "{0} - Parar emulação",
|
||||||
"ru_RU": "Ryujinx - Остановка эмуляции",
|
"ru_RU": "{0} - Остановка эмуляции",
|
||||||
"sv_SE": "Ryujinx - Stoppa emulering",
|
"sv_SE": "{0} - Stoppa emulering",
|
||||||
"th_TH": "Ryujinx - หยุดการจำลอง",
|
"th_TH": "{0} - หยุดการจำลอง",
|
||||||
"tr_TR": "Ryujinx - Emülasyonu Durdur",
|
"tr_TR": "{0} - Emülasyonu Durdur",
|
||||||
"uk_UA": "Ryujinx - Зупинити емуляцію",
|
"uk_UA": "{0} - Зупинити емуляцію",
|
||||||
"zh_CN": "Ryujinx - 停止模拟",
|
"zh_CN": "{0} - 停止模拟",
|
||||||
"zh_TW": "Ryujinx - 停止模擬"
|
"zh_TW": "{0} - 停止模擬"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -17950,51 +17950,51 @@
|
|||||||
{
|
{
|
||||||
"ID": "RyujinxInfo",
|
"ID": "RyujinxInfo",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - معلومات",
|
"ar_SA": "{0} - معلومات",
|
||||||
"de_DE": "",
|
"de_DE": "",
|
||||||
"el_GR": "Ryujinx - Πληροφορίες",
|
"el_GR": "{0} - Πληροφορίες",
|
||||||
"en_US": "Ryujinx - Info",
|
"en_US": "{0} - Info",
|
||||||
"es_ES": "",
|
"es_ES": "",
|
||||||
"fr_FR": "",
|
"fr_FR": "",
|
||||||
"he_IL": "ריוג'ינקס - מידע",
|
"he_IL": "{0} - מידע",
|
||||||
"it_IT": "Ryujinx - Informazioni",
|
"it_IT": "{0} - Informazioni",
|
||||||
"ja_JP": "Ryujinx - 情報",
|
"ja_JP": "{0} - 情報",
|
||||||
"ko_KR": "Ryujinx - 정보",
|
"ko_KR": "{0} - 정보",
|
||||||
"no_NO": "Ryujinx - Informasjon",
|
"no_NO": "{0} - Informasjon",
|
||||||
"pl_PL": "",
|
"pl_PL": "",
|
||||||
"pt_BR": "Ryujinx - Informação",
|
"pt_BR": "{0} - Informação",
|
||||||
"ru_RU": "Ryujinx - Информация",
|
"ru_RU": "{0} - Информация",
|
||||||
"sv_SE": "",
|
"sv_SE": "",
|
||||||
"th_TH": "Ryujinx – ข้อมูล",
|
"th_TH": "{0} – ข้อมูล",
|
||||||
"tr_TR": "Ryujinx - Bilgi",
|
"tr_TR": "{0} - Bilgi",
|
||||||
"uk_UA": "Ryujin x - Інформація",
|
"uk_UA": "{0} - Інформація",
|
||||||
"zh_CN": "Ryujinx - 信息",
|
"zh_CN": "{0} - 信息",
|
||||||
"zh_TW": "Ryujinx - 資訊"
|
"zh_TW": "{0} - 資訊"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ID": "RyujinxConfirm",
|
"ID": "RyujinxConfirm",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "ريوجينكس - تأكيد",
|
"ar_SA": "{0} - تأكيد",
|
||||||
"de_DE": "Ryujinx - Bestätigung",
|
"de_DE": "{0} - Bestätigung",
|
||||||
"el_GR": "Ryujinx - Επιβεβαίωση",
|
"el_GR": "{0} - Επιβεβαίωση",
|
||||||
"en_US": "Ryujinx - Confirmation",
|
"en_US": "{0} - Confirmation",
|
||||||
"es_ES": "Ryujinx - Confirmación",
|
"es_ES": "{0} - Confirmación",
|
||||||
"fr_FR": "",
|
"fr_FR": "",
|
||||||
"he_IL": "ריוג'ינקס - אישור",
|
"he_IL": "{0} - אישור",
|
||||||
"it_IT": "Ryujinx - Conferma",
|
"it_IT": "{0} - Conferma",
|
||||||
"ja_JP": "Ryujinx - 確認",
|
"ja_JP": "{0} - 確認",
|
||||||
"ko_KR": "Ryujinx - 확인",
|
"ko_KR": "{0} - 확인",
|
||||||
"no_NO": "Ryujinx - Bekreftelse",
|
"no_NO": "{0} - Bekreftelse",
|
||||||
"pl_PL": "Ryujinx - Potwierdzenie",
|
"pl_PL": "{0} - Potwierdzenie",
|
||||||
"pt_BR": "Ryujinx - Confirmação",
|
"pt_BR": "{0} - Confirmação",
|
||||||
"ru_RU": "Ryujinx - Подтверждение",
|
"ru_RU": "{0} - Подтверждение",
|
||||||
"sv_SE": "Ryujinx - Bekräfta",
|
"sv_SE": "{0} - Bekräfta",
|
||||||
"th_TH": "Ryujinx - ยืนยัน",
|
"th_TH": "{0} - ยืนยัน",
|
||||||
"tr_TR": "Ryujinx - Doğrulama",
|
"tr_TR": "{0} - Doğrulama",
|
||||||
"uk_UA": "Ryujinx - Підтвердження",
|
"uk_UA": "{0} - Підтвердження",
|
||||||
"zh_CN": "Ryujinx - 确认",
|
"zh_CN": "{0} - 确认",
|
||||||
"zh_TW": "Ryujinx - 確認"
|
"zh_TW": "{0} - 確認"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -18800,26 +18800,26 @@
|
|||||||
{
|
{
|
||||||
"ID": "RyujinxUpdater",
|
"ID": "RyujinxUpdater",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "محدث ريوجينكس",
|
"ar_SA": "محدث {0}",
|
||||||
"de_DE": "Ryujinx - Updater",
|
"de_DE": "",
|
||||||
"el_GR": "Ryujinx Ενημερωτής",
|
"el_GR": "{0} Ενημερωτής",
|
||||||
"en_US": "Ryujinx Updater",
|
"en_US": "{0} Updater",
|
||||||
"es_ES": "Actualizador de Ryujinx",
|
"es_ES": "Actualizador de {0}",
|
||||||
"fr_FR": "Mise à jour de Ryujinx",
|
"fr_FR": "Mise à jour de {0}",
|
||||||
"he_IL": "מעדכן ריוג'ינקס",
|
"he_IL": "מעדכן {0}",
|
||||||
"it_IT": "Aggiornamento di Ryujinx",
|
"it_IT": "Aggiornamento di {0}",
|
||||||
"ja_JP": "Ryujinx アップデータ",
|
"ja_JP": "{0} アップデータ",
|
||||||
"ko_KR": "Ryujinx 업데이터",
|
"ko_KR": "{0} 업데이터",
|
||||||
"no_NO": "Ryujinx Oppgradering",
|
"no_NO": "{0} Oppgradering",
|
||||||
"pl_PL": "Aktualizator Ryujinx",
|
"pl_PL": "Aktualizator {0}",
|
||||||
"pt_BR": "Atualizador do Ryujinx",
|
"pt_BR": "Atualizador do {0}",
|
||||||
"ru_RU": "Ryujinx - Обновление",
|
"ru_RU": "{0} Обновление",
|
||||||
"sv_SE": "Uppdaterare för Ryujinx",
|
"sv_SE": "Uppdaterare för {0}",
|
||||||
"th_TH": "ตัวอัปเดต Ryujinx",
|
"th_TH": "ตัวอัปเดต {0}",
|
||||||
"tr_TR": "Ryujinx Güncelleyicisi",
|
"tr_TR": "{0} Güncelleyicisi",
|
||||||
"uk_UA": "Програма оновлення Ryujinx",
|
"uk_UA": "Програма оновлення {0}",
|
||||||
"zh_CN": "Ryujinx 更新",
|
"zh_CN": "{0} 更新",
|
||||||
"zh_TW": "Ryujinx 更新程式"
|
"zh_TW": "{0} 更新程式"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23069,7 +23069,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "可游玩",
|
"zh_CN": "可游玩",
|
||||||
"zh_TW": "可暢順遊玩 (Playable)"
|
"zh_TW": "可暢順遊玩"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23094,7 +23094,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "进入游戏",
|
"zh_CN": "进入游戏",
|
||||||
"zh_TW": "大致可遊玩 (Ingame)"
|
"zh_TW": "大致可遊玩"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23119,7 +23119,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "菜单",
|
"zh_CN": "菜单",
|
||||||
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
|
"zh_TW": "只開啟至遊戲開始功能表"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23144,7 +23144,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "启动",
|
"zh_CN": "启动",
|
||||||
"zh_TW": "只能啟動 (Boots)"
|
"zh_TW": "只能啟動"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23169,7 +23169,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "什么都没有",
|
"zh_CN": "什么都没有",
|
||||||
"zh_TW": "無法啟動 (Nothing)"
|
"zh_TW": "無法啟動"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ namespace Ryujinx.Ava.Common
|
|||||||
|
|
||||||
if (publicDataNca is null)
|
if (publicDataNca is null)
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, "Extraction failure. The NCA was not present in the selected file");
|
Logger.Error?.Print(LogClass.Application, "Extraction failure. The PublicData NCA was not present in the selected file");
|
||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -349,10 +349,6 @@ namespace Ryujinx.Ava.Common
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
int index = Nca.GetSectionIndexFromType(NcaSectionType.Data, publicDataNca.Header.ContentType);
|
int index = Nca.GetSectionIndexFromType(NcaSectionType.Data, publicDataNca.Header.ContentType);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
|
|
||||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetDynamicValues(LocaleKeys.DialogConfirmationTitle, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.DialogUpdaterTitle, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.DialogErrorTitle, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.DialogWarningTitle, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.DialogExitTitle, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.DialogStopEmulationTitle, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.RyujinxInfo, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.RyujinxConfirm, RyujinxApp.FullAppName);
|
||||||
|
SetDynamicValues(LocaleKeys.RyujinxUpdater, RyujinxApp.FullAppName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string this[LocaleKeys key]
|
public string this[LocaleKeys key]
|
||||||
@@ -88,11 +98,16 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
|
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
|
||||||
=> Instance.UpdateAndGetDynamicValue(key, values);
|
=> Instance.UpdateAndGetDynamicValue(key, values);
|
||||||
|
|
||||||
public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values)
|
public void SetDynamicValues(LocaleKeys key, params object[] values)
|
||||||
{
|
{
|
||||||
_dynamicValues[key] = values;
|
_dynamicValues[key] = values;
|
||||||
|
|
||||||
OnPropertyChanged("Translation");
|
OnPropertyChanged("Translation");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values)
|
||||||
|
{
|
||||||
|
SetDynamicValues(key, values);
|
||||||
|
|
||||||
return this[key];
|
return this[key];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
using DiscordRPC;
|
using DiscordRPC;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
|
using MsgPack;
|
||||||
using Ryujinx.Ava.Utilities;
|
using Ryujinx.Ava.Utilities;
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using Ryujinx.Ava.Utilities.Configuration;
|
using Ryujinx.Ava.Utilities.Configuration;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.HLE.Loaders.Processes;
|
using Ryujinx.HLE.Loaders.Processes;
|
||||||
|
using Ryujinx.Horizon;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.Ava
|
namespace Ryujinx.Ava
|
||||||
@@ -20,8 +28,8 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
private static readonly string _description =
|
private static readonly string _description =
|
||||||
ReleaseInformation.IsValid
|
ReleaseInformation.IsValid
|
||||||
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
|
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
|
||||||
: "dev build";
|
: "dev build";
|
||||||
|
|
||||||
private const string ApplicationId = "1293250299716173864";
|
private const string ApplicationId = "1293250299716173864";
|
||||||
|
|
||||||
@@ -30,6 +38,8 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
private static DiscordRpcClient _discordClient;
|
private static DiscordRpcClient _discordClient;
|
||||||
private static RichPresence _discordPresenceMain;
|
private static RichPresence _discordPresenceMain;
|
||||||
|
private static RichPresence _discordPresencePlaying;
|
||||||
|
private static ApplicationMetadata _currentApp;
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
@@ -37,8 +47,7 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
Assets = new Assets
|
Assets = new Assets
|
||||||
{
|
{
|
||||||
LargeImageKey = "ryujinx",
|
LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description)
|
||||||
LargeImageText = TruncateToByteLength(_description)
|
|
||||||
},
|
},
|
||||||
Details = "Main Menu",
|
Details = "Main Menu",
|
||||||
State = "Idling",
|
State = "Idling",
|
||||||
@@ -47,6 +56,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
|
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
|
||||||
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
|
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
|
||||||
|
HorizonStatic.PlayReport += HandlePlayReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
|
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
|
||||||
@@ -84,9 +94,8 @@ namespace Ryujinx.Ava
|
|||||||
SwitchToMainState();
|
SwitchToMainState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
|
private static RichPresence CreatePlayingState(ApplicationMetadata appMeta, ProcessResult procRes) =>
|
||||||
{
|
new()
|
||||||
_discordClient?.SetPresence(new RichPresence
|
|
||||||
{
|
{
|
||||||
Assets = new Assets
|
Assets = new Assets
|
||||||
{
|
{
|
||||||
@@ -100,10 +109,48 @@ namespace Ryujinx.Ava
|
|||||||
? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}"
|
? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}"
|
||||||
: "Never played",
|
: "Never played",
|
||||||
Timestamps = GuestAppStartedAt ??= Timestamps.Now
|
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 UpdatePlayingState()
|
||||||
|
{
|
||||||
|
_discordClient?.SetPresence(_discordPresencePlaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
PlayReport.Analyzer.FormatPlayReportValue(TitleIDs.CurrentApplication.Value, _currentApp, playReport)
|
||||||
|
.Match(out bool handled,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
|
||||||
|
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
|
||||||
|
},
|
||||||
|
formattedString =>
|
||||||
|
{
|
||||||
|
_discordPresencePlaying.Details = formattedString;
|
||||||
|
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (handled)
|
||||||
|
UpdatePlayingState();
|
||||||
|
}
|
||||||
|
|
||||||
private static string TruncateToByteLength(string input)
|
private static string TruncateToByteLength(string input)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
public class RyujinxApp : Application
|
public class RyujinxApp : Application
|
||||||
{
|
{
|
||||||
internal static string FormatTitle(LocaleKeys? windowTitleKey = null)
|
internal static string FormatTitle(LocaleKeys? windowTitleKey = null, bool includeVersion = true)
|
||||||
=> windowTitleKey is null
|
=> windowTitleKey is null
|
||||||
? $"{FullAppName} {Program.Version}"
|
? $"{FullAppName}{(includeVersion ? $" {Program.Version}" : string.Empty)}"
|
||||||
: $"{FullAppName} {Program.Version} - {LocaleManager.Instance[windowTitleKey.Value]}";
|
: $"{FullAppName}{(includeVersion ? $" {Program.Version}" : string.Empty)} - {LocaleManager.Instance[windowTitleKey.Value]}";
|
||||||
|
|
||||||
public static readonly string FullAppName = ReleaseInformation.IsCanaryBuild ? "Ryujinx Canary" : "Ryujinx";
|
public static readonly string FullAppName = string.Intern(ReleaseInformation.IsCanaryBuild ? "Ryujinx Canary" : "Ryujinx");
|
||||||
|
|
||||||
public static MainWindow MainWindow => Current!
|
public static MainWindow MainWindow => Current!
|
||||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
|
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
case KeyboardMode.Numeric:
|
case KeyboardMode.Numeric:
|
||||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric);
|
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric);
|
||||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
||||||
_checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
|
_checkInput = text => text.All(CharacterValidation.IsNumeric);
|
||||||
break;
|
break;
|
||||||
case KeyboardMode.Alphabet:
|
case KeyboardMode.Alphabet:
|
||||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet);
|
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet);
|
||||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
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;
|
break;
|
||||||
case KeyboardMode.ASCII:
|
case KeyboardMode.ASCII:
|
||||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII);
|
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII);
|
||||||
|
|||||||
@@ -86,6 +86,13 @@
|
|||||||
Text="{Binding Version}"
|
Text="{Binding Version}"
|
||||||
TextAlignment="Start"
|
TextAlignment="Start"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock
|
||||||
|
IsVisible="{Binding HasPlayabilityInfo}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{Binding LocalizedStatus}"
|
||||||
|
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
Symbol = (Symbol)symbol,
|
Symbol = (Symbol)symbol,
|
||||||
Margin = new Thickness(10),
|
Margin = new Thickness(10),
|
||||||
FontSize = 40,
|
FontSize = 40,
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Ryujinx.Common.Helper;
|
||||||
using SharpMetal.QuartzCore;
|
using SharpMetal.QuartzCore;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -7,14 +8,12 @@ namespace Ryujinx.Ava.UI.Renderer
|
|||||||
{
|
{
|
||||||
public CAMetalLayer CreateSurface()
|
public CAMetalLayer CreateSurface()
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS() && RunningPlatform.IsArm)
|
||||||
{
|
{
|
||||||
return new CAMetalLayer(MetalLayer);
|
return new CAMetalLayer(MetalLayer);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
throw new NotSupportedException($"Cannot create a {nameof(CAMetalLayer)} without being on ARM Mac.");
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,18 +43,18 @@ namespace Ryujinx.Ava.UI.Renderer
|
|||||||
|
|
||||||
public RendererHost(string titleId)
|
public RendererHost(string titleId)
|
||||||
{
|
{
|
||||||
switch (TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend))
|
Focusable = true;
|
||||||
{
|
FlowDirection = FlowDirection.LeftToRight;
|
||||||
case GraphicsBackend.OpenGl:
|
|
||||||
EmbeddedWindow = new EmbeddedWindowOpenGL();
|
EmbeddedWindow =
|
||||||
break;
|
#pragma warning disable CS8509
|
||||||
case GraphicsBackend.Metal:
|
TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend) switch
|
||||||
EmbeddedWindow = new EmbeddedWindowMetal();
|
#pragma warning restore CS8509
|
||||||
break;
|
{
|
||||||
case GraphicsBackend.Vulkan:
|
GraphicsBackend.OpenGl => new EmbeddedWindowOpenGL(),
|
||||||
EmbeddedWindow = new EmbeddedWindowVulkan();
|
GraphicsBackend.Metal => new EmbeddedWindowMetal(),
|
||||||
break;
|
GraphicsBackend.Vulkan => new EmbeddedWindowVulkan(),
|
||||||
}
|
};
|
||||||
|
|
||||||
string backendText = EmbeddedWindow switch
|
string backendText = EmbeddedWindow switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using Ryujinx.Ava.Utilities.Configuration.System;
|
|||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Configuration.Multiplayer;
|
using Ryujinx.Common.Configuration.Multiplayer;
|
||||||
using Ryujinx.Common.GraphicsDriver;
|
using Ryujinx.Common.GraphicsDriver;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.Vulkan;
|
using Ryujinx.Graphics.Vulkan;
|
||||||
@@ -115,10 +116,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS();
|
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 EnableDiscordIntegration { get; set; }
|
||||||
public bool CheckUpdatesOnStart { get; set; }
|
public bool CheckUpdatesOnStart { get; set; }
|
||||||
public bool ShowConfirmExit { get; set; }
|
public bool ShowConfirmExit { get; set; }
|
||||||
@@ -200,7 +197,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public bool EnableTextureRecompression { get; set; }
|
public bool EnableTextureRecompression { get; set; }
|
||||||
public bool EnableMacroHLE { get; set; }
|
public bool EnableMacroHLE { get; set; }
|
||||||
public bool EnableColorSpacePassthrough { get; set; }
|
public bool EnableColorSpacePassthrough { get; set; }
|
||||||
public bool ColorSpacePassthroughAvailable => IsMacOS;
|
public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS;
|
||||||
public bool EnableFileLog { get; set; }
|
public bool EnableFileLog { get; set; }
|
||||||
public bool EnableStub { get; set; }
|
public bool EnableStub { get; set; }
|
||||||
public bool EnableInfo { get; set; }
|
public bool EnableInfo { get; set; }
|
||||||
@@ -296,6 +293,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _matchSystemTime;
|
||||||
|
|
||||||
public DateTimeOffset CurrentDate { get; set; }
|
public DateTimeOffset CurrentDate { get; set; }
|
||||||
|
|
||||||
public TimeSpan CurrentTime { get; set; }
|
public TimeSpan CurrentTime { get; set; }
|
||||||
@@ -330,9 +329,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex("Ryujinx-[0-9a-f]{8}")]
|
|
||||||
private static partial Regex LdnPassphraseRegex();
|
|
||||||
|
|
||||||
public bool IsInvalidLdnPassphraseVisible { get; set; }
|
public bool IsInvalidLdnPassphraseVisible { get; set; }
|
||||||
|
|
||||||
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
|
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
|
||||||
@@ -414,17 +410,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex)));
|
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()
|
public async Task LoadTimeZones()
|
||||||
{
|
{
|
||||||
_timeZoneContentManager = new TimeZoneContentManager();
|
_timeZoneContentManager = new TimeZoneContentManager();
|
||||||
@@ -470,7 +455,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
private bool ValidateLdnPassphrase(string passphrase)
|
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)
|
public void ValidateAndSetTimeZone(string location)
|
||||||
@@ -526,7 +511,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
CurrentDate = currentDateTime.Date;
|
CurrentDate = currentDateTime.Date;
|
||||||
CurrentTime = currentDateTime.TimeOfDay;
|
CurrentTime = currentDateTime.TimeOfDay;
|
||||||
|
|
||||||
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value;
|
MatchSystemTime = config.System.MatchSystemTime;
|
||||||
|
|
||||||
|
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval;
|
||||||
CustomVSyncInterval = config.Graphics.CustomVSyncInterval;
|
CustomVSyncInterval = config.Graphics.CustomVSyncInterval;
|
||||||
VSyncMode = config.Graphics.VSyncMode;
|
VSyncMode = config.Graphics.VSyncMode;
|
||||||
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
||||||
@@ -631,6 +618,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
config.System.TimeZone.Value = TimeZone;
|
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.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||||
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
|
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
|
||||||
config.System.DramSize.Value = DramSize;
|
config.System.DramSize.Value = DramSize;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:DataType="viewModels:SettingsViewModel">
|
x:DataType="viewModels:SettingsViewModel">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<CheckBox IsChecked="{Binding UseHypervisor}"
|
<CheckBox IsChecked="{Binding UseHypervisor}"
|
||||||
IsVisible="{Binding IsAppleSiliconMac}"
|
IsVisible="{x:Static helper:RunningPlatform.IsArmMac}"
|
||||||
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}">
|
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}">
|
||||||
<TextBlock Text="{ext:Locale SettingsTabSystemUseHypervisor}"
|
<TextBlock Text="{ext:Locale SettingsTabSystemUseHypervisor}"
|
||||||
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}" />
|
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}" />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||||
Design.Width="1000"
|
Design.Width="1000"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:DataType="viewModels:SettingsViewModel">
|
x:DataType="viewModels:SettingsViewModel">
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
|
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
|
||||||
<TextBlock Text="OpenGL" />
|
<TextBlock Text="OpenGL" />
|
||||||
</ComboBoxItem>
|
</ComboBoxItem>
|
||||||
<ComboBoxItem IsEnabled="{Binding IsAppleSiliconMac}">
|
<ComboBoxItem IsEnabled="{x:Static helper:RunningPlatform.IsArmMac}">
|
||||||
<TextBlock Text="Metal (ARM Mac only, Experimental)" />
|
<TextBlock Text="Metal (ARM Mac only, Experimental)" />
|
||||||
</ComboBoxItem>
|
</ComboBoxItem>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|||||||
@@ -171,6 +171,7 @@
|
|||||||
Width="250"/>
|
Width="250"/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
IsEnabled="{Binding !MatchSystemTime}"
|
||||||
SelectedDate="{Binding CurrentDate}"
|
SelectedDate="{Binding CurrentDate}"
|
||||||
ToolTip.Tip="{ext:Locale TimeTooltip}"
|
ToolTip.Tip="{ext:Locale TimeTooltip}"
|
||||||
Width="350" />
|
Width="350" />
|
||||||
@@ -181,17 +182,21 @@
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ClockIdentifier="24HourClock"
|
ClockIdentifier="24HourClock"
|
||||||
|
IsEnabled="{Binding !MatchSystemTime}"
|
||||||
SelectedTime="{Binding CurrentTime}"
|
SelectedTime="{Binding CurrentTime}"
|
||||||
Width="350"
|
Width="350"
|
||||||
ToolTip.Tip="{ext:Locale TimeTooltip}" />
|
ToolTip.Tip="{ext:Locale TimeTooltip}" />
|
||||||
<Button
|
</StackPanel>
|
||||||
Margin="10, 0, 0, 0"
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Click="MatchSystemTime_OnClick"
|
Text="{ext:Locale SettingsTabSystemSystemTimeMatch}"
|
||||||
Background="{DynamicResource SystemAccentColor}"
|
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"
|
||||||
ToolTip.Tip="{ext:Locale MatchTimeTooltip}">
|
Width="250"/>
|
||||||
<TextBlock Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" />
|
<CheckBox
|
||||||
</Button>
|
VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding MatchSystemTime}"
|
||||||
|
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Separator />
|
<Separator />
|
||||||
<StackPanel Margin="0,10,0,10"
|
<StackPanel Margin="0,10,0,10"
|
||||||
|
|||||||
@@ -34,7 +34,5 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
|||||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MatchSystemTime_OnClick(object sender, RoutedEventArgs e) => ViewModel.MatchSystemTime();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
LinuxHelper.RecommendedVmMaxMapCount);
|
LinuxHelper.RecommendedVmMaxMapCount);
|
||||||
|
|
||||||
UserResult response = await ContentDialogHelper.ShowTextDialog(
|
UserResult response = await ContentDialogHelper.ShowTextDialog(
|
||||||
$"Ryujinx - {LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTitle]}",
|
RyujinxApp.FormatTitle(LocaleKeys.LinuxVmMaxMapCountDialogTitle, false),
|
||||||
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary],
|
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary],
|
||||||
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary],
|
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary],
|
||||||
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart],
|
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
|
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
|
||||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
|
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||||
Width="1100"
|
Width="1100"
|
||||||
Height="768"
|
Height="768"
|
||||||
MinWidth="800"
|
MinWidth="800"
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
Spacing="10"
|
Spacing="10"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
ReverseOrder="{Binding IsMacOS}">
|
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
|
||||||
<Button
|
<Button
|
||||||
Classes="accent"
|
Classes="accent"
|
||||||
Content="{ext:Locale SettingsButtonOk}"
|
Content="{ext:Locale SettingsButtonOk}"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using LibHac.Ns;
|
|||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Utilities.Compat;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
@@ -21,9 +23,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
|||||||
public bool Favorite { get; set; }
|
public bool Favorite { get; set; }
|
||||||
public byte[] Icon { get; set; }
|
public byte[] Icon { get; set; }
|
||||||
public string Name { get; set; } = "Unknown";
|
public string Name { get; set; } = "Unknown";
|
||||||
public ulong Id { get; set; }
|
|
||||||
|
private ulong _id;
|
||||||
|
|
||||||
|
public ulong Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_id = value;
|
||||||
|
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
public string Developer { get; set; } = "Unknown";
|
public string Developer { get; set; } = "Unknown";
|
||||||
public string Version { get; set; } = "0";
|
public string Version { get; set; } = "0";
|
||||||
|
|
||||||
|
public bool HasPlayabilityInfo => PlayabilityStatus != null;
|
||||||
|
|
||||||
|
public string LocalizedStatus =>
|
||||||
|
PlayabilityStatus.HasValue
|
||||||
|
? LocaleManager.Instance[PlayabilityStatus!.Value]
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
public LocaleKeys? PlayabilityStatus { get; set; }
|
||||||
|
|
||||||
public int PlayerCount { get; set; }
|
public int PlayerCount { get; set; }
|
||||||
public int GameCount { get; set; }
|
public int GameCount { get; set; }
|
||||||
public TimeSpan TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
|
|||||||
@@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
|||||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Unload()
|
|
||||||
{
|
|
||||||
_entries = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CompatibilityEntry[] _entries;
|
private static CompatibilityEntry[] _entries;
|
||||||
|
|
||||||
public static CompatibilityEntry[] Entries
|
public static CompatibilityEntry[] Entries
|
||||||
@@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
|
|||||||
return _entries;
|
return _entries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LocaleKeys? GetStatus(string titleId)
|
||||||
|
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status;
|
||||||
|
|
||||||
|
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CompatibilityEntry
|
public class CompatibilityEntry
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
|||||||
contentDialog.Styles.Add(closeButtonParent);
|
contentDialog.Styles.Add(closeButtonParent);
|
||||||
|
|
||||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||||
|
|
||||||
CompatibilityCsv.Unload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompatibilityList()
|
public CompatibilityList()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current version of the file format
|
/// The current version of the file format
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int CurrentVersion = 62;
|
public const int CurrentVersion = 63;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version of the configuration file format
|
/// Version of the configuration file format
|
||||||
@@ -142,6 +142,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public long SystemTimeOffset { get; set; }
|
public long SystemTimeOffset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instead of setting the time via configuration, use the values provided by the system.
|
||||||
|
/// </summary>
|
||||||
|
public bool MatchSystemTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables or disables Docked Mode
|
/// Enables or disables Docked Mode
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -429,7 +429,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
(62, static cff => cff.RainbowSpeed = 1f)
|
(62, static cff => cff.RainbowSpeed = 1f),
|
||||||
|
(63, static cff => cff.MatchSystemTime = false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,6 +313,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ReactiveObject<long> SystemTimeOffset { get; private set; }
|
public ReactiveObject<long> SystemTimeOffset { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instead of setting the time via configuration, use the values provided by the system.
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveObject<bool> MatchSystemTime { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables or disables Docked Mode
|
/// Enables or disables Docked Mode
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -388,6 +393,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
|||||||
TimeZone.LogChangesToValue(nameof(TimeZone));
|
TimeZone.LogChangesToValue(nameof(TimeZone));
|
||||||
SystemTimeOffset = new ReactiveObject<long>();
|
SystemTimeOffset = new ReactiveObject<long>();
|
||||||
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
|
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
|
||||||
|
MatchSystemTime = new ReactiveObject<bool>();
|
||||||
|
MatchSystemTime.LogChangesToValue(nameof(MatchSystemTime));
|
||||||
EnableDockedMode = new ReactiveObject<bool>();
|
EnableDockedMode = new ReactiveObject<bool>();
|
||||||
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
|
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
|
||||||
EnablePtc = new ReactiveObject<bool>();
|
EnablePtc = new ReactiveObject<bool>();
|
||||||
|
|||||||
85
src/Ryujinx/Utilities/PlayReport.cs
Normal file
85
src/Ryujinx/Utilities/PlayReport.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities
|
||||||
|
{
|
||||||
|
public static class PlayReport
|
||||||
|
{
|
||||||
|
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
|
||||||
|
.AddSpec(
|
||||||
|
"01007ef00011e000",
|
||||||
|
spec => spec
|
||||||
|
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||||
|
// reset to normal status when switching between normal & master mode in title screen
|
||||||
|
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
|
||||||
|
)
|
||||||
|
.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)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
|
||||||
|
value.PackedValue.AsDouble() switch
|
||||||
|
{
|
||||||
|
> 800d => "Exploring the Sky Islands",
|
||||||
|
< -201d => "Exploring the Depths",
|
||||||
|
_ => "Roaming Hyrule"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
|
||||||
|
=> value.BoxedValue switch
|
||||||
|
{
|
||||||
|
// Single Player
|
||||||
|
"Single" => "Single Player",
|
||||||
|
// Multiplayer
|
||||||
|
"Multi-2players" => "Multiplayer 2 Players",
|
||||||
|
"Multi-3players" => "Multiplayer 3 Players",
|
||||||
|
"Multi-4players" => "Multiplayer 4 Players",
|
||||||
|
// Wireless/LAN Play
|
||||||
|
"Local-Single" => "Wireless/LAN Play",
|
||||||
|
"Local-2players" => "Wireless/LAN Play 2 Players",
|
||||||
|
// CC Classes
|
||||||
|
"50cc" => "50cc",
|
||||||
|
"100cc" => "100cc",
|
||||||
|
"150cc" => "150cc",
|
||||||
|
"Mirror" => "Mirror (150cc)",
|
||||||
|
"200cc" => "200cc",
|
||||||
|
// Modes
|
||||||
|
"GrandPrix" => "Grand Prix",
|
||||||
|
"TimeAttack" => "Time Trials",
|
||||||
|
"VS" => "VS Races",
|
||||||
|
"Battle" => "Battle Mode",
|
||||||
|
"RaceStart" => "Selecting a Course",
|
||||||
|
"Race" => "Racing",
|
||||||
|
_ => PlayReportFormattedValue.ForceReset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
274
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
274
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
using Gommon;
|
||||||
|
using MsgPack;
|
||||||
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entrypoint for the Play Report analysis system.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayReportAnalyzer
|
||||||
|
{
|
||||||
|
private readonly List<PlayReportGameSpec> _specs = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds,
|
||||||
|
Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
string[] tids = titleIds.ToArray();
|
||||||
|
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
string[] tids = titleIds.ToArray();
|
||||||
|
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runningGameId">The game currently running.</param>
|
||||||
|
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
|
||||||
|
/// <param name="playReport">The Play Report received from HLE.</param>
|
||||||
|
/// <returns>A struct representing a possible formatted value.</returns>
|
||||||
|
public FormattedValue FormatPlayReportValue(
|
||||||
|
string runningGameId,
|
||||||
|
ApplicationMetadata appMeta,
|
||||||
|
MessagePackObject playReport
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!playReport.IsDictionary)
|
||||||
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
|
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
||||||
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
|
foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
||||||
|
{
|
||||||
|
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return formatSpec.ValueFormatter(new PlayReportValue
|
||||||
|
{
|
||||||
|
Application = appMeta, PackedValue = valuePackObject
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormattedValue.Unhandled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct FormattedValue
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Was any handler able to match anything in the Play Report?
|
||||||
|
/// </summary>
|
||||||
|
public bool Handled { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Did the handler request the caller of the <see cref="PlayReportAnalyzer"/> to reset the existing value?
|
||||||
|
/// </summary>
|
||||||
|
public bool Reset { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||||
|
/// </summary>
|
||||||
|
public string FormattedString { get; private init; }
|
||||||
|
|
||||||
|
public void Match(out bool wasHandled, Action onReset, Action<string> onSuccess)
|
||||||
|
{
|
||||||
|
if (!Handled)
|
||||||
|
{
|
||||||
|
wasHandled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Reset)
|
||||||
|
onReset();
|
||||||
|
else
|
||||||
|
onSuccess(FormattedString);
|
||||||
|
|
||||||
|
wasHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The intended path of execution for having a string to return: simply return the string.
|
||||||
|
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||||
|
///
|
||||||
|
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formattedValue">The formatted string value.</param>
|
||||||
|
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||||
|
public static implicit operator FormattedValue(string formattedValue)
|
||||||
|
=> formattedValue is not null
|
||||||
|
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
||||||
|
: Unhandled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this to tell the caller there is no value to return.
|
||||||
|
/// </summary>
|
||||||
|
public static FormattedValue Unhandled => default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this to suggest the caller reset the value it's using the <see cref="PlayReportAnalyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="PlayReportValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A mapping of title IDs to value formatter specs.
|
||||||
|
///
|
||||||
|
/// <remarks>Generally speaking, use the <see cref="PlayReportAnalyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class PlayReportGameSpec
|
||||||
|
{
|
||||||
|
public required string[] TitleIds { get; init; }
|
||||||
|
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter to the current <see cref="PlayReportGameSpec"/>
|
||||||
|
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKey">The key name to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportGameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SimpleValueFormatters.Add(new FormatterSpec
|
||||||
|
{
|
||||||
|
Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter at a specific priority to the current <see cref="PlayReportGameSpec"/>
|
||||||
|
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKey">The key name to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportGameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
|
||||||
|
PlayReportValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SimpleValueFormatters.Add(new FormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
|
||||||
|
/// </summary>
|
||||||
|
public struct FormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string ReportKey { get; init; }
|
||||||
|
public PlayReportValueFormatter ValueFormatter { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The input data to a <see cref="PlayReportValueFormatter"/>,
|
||||||
|
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||||
|
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayReportValue
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ApplicationMetadata Application { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The matched value from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public MessagePackObject PackedValue { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||||
|
///
|
||||||
|
/// Does not seem to work well with comparing numeric types,
|
||||||
|
/// so use <see cref="PackedValue"/> and the AsX (where X is a numerical type name i.e. Int32) methods for that.
|
||||||
|
/// </summary>
|
||||||
|
public object BoxedValue => PackedValue.ToObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers the entire analysis system (as it currently is).<br/>
|
||||||
|
/// Takes in the result value from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="PlayReportAnalyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user