Compare commits
18 Commits
Canary-1.2
...
Canary-1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55536f5d78 | ||
|
|
b2eecd28ce | ||
|
|
fe43c32e60 | ||
|
|
8117e160c2 | ||
|
|
bf713a80d6 | ||
|
|
b38b5a1e70 | ||
|
|
2d7700949c | ||
|
|
ea2287af03 | ||
|
|
37af8c70aa | ||
|
|
50cee3fd19 | ||
|
|
a46aacf2e2 | ||
|
|
ad9d6588e8 | ||
|
|
38ef65aae0 | ||
|
|
9f94aa1c79 | ||
|
|
2c9a26c11c | ||
|
|
a4a15a4c80 | ||
|
|
cc3b95eee1 | ||
|
|
2ab806f759 |
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
using MsgPack;
|
||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
@@ -6,6 +7,10 @@ namespace Ryujinx.Horizon
|
|||||||
{
|
{
|
||||||
public static class HorizonStatic
|
public static class HorizonStatic
|
||||||
{
|
{
|
||||||
|
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
|
||||||
|
|
||||||
|
public static event Action<MessagePackObject> PlayReportPrinted;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static HorizonOptions _options;
|
private static HorizonOptions _options;
|
||||||
|
|
||||||
|
|||||||
@@ -231,6 +231,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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.PlayReportPrinted += 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;
|
||||||
|
|
||||||
|
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||||
|
|
||||||
|
if (!value.Handled) return;
|
||||||
|
|
||||||
|
if (value.Reset)
|
||||||
|
{
|
||||||
|
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
|
||||||
|
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_discordPresencePlaying.Details = value.FormattedString;
|
||||||
|
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||||
|
}
|
||||||
|
UpdatePlayingState();
|
||||||
|
}
|
||||||
|
|
||||||
private static string TruncateToByteLength(string input)
|
private static string TruncateToByteLength(string input)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
EmbeddedWindow =
|
||||||
|
#pragma warning disable CS8509
|
||||||
|
TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend) switch
|
||||||
|
#pragma warning restore CS8509
|
||||||
{
|
{
|
||||||
case GraphicsBackend.OpenGl:
|
GraphicsBackend.OpenGl => new EmbeddedWindowOpenGL(),
|
||||||
EmbeddedWindow = new EmbeddedWindowOpenGL();
|
GraphicsBackend.Metal => new EmbeddedWindowMetal(),
|
||||||
break;
|
GraphicsBackend.Vulkan => new EmbeddedWindowVulkan(),
|
||||||
case GraphicsBackend.Metal:
|
};
|
||||||
EmbeddedWindow = new EmbeddedWindowMetal();
|
|
||||||
break;
|
|
||||||
case GraphicsBackend.Vulkan:
|
|
||||||
EmbeddedWindow = new EmbeddedWindowVulkan();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -330,9 +331,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()
|
||||||
@@ -470,7 +468,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)
|
||||||
|
|||||||
198
src/Ryujinx/Utilities/PlayReport.cs
Normal file
198
src/Ryujinx/Utilities/PlayReport.cs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
using Gommon;
|
||||||
|
using MsgPack;
|
||||||
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities
|
||||||
|
{
|
||||||
|
public static class PlayReport
|
||||||
|
{
|
||||||
|
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
|
||||||
|
.AddSpec(
|
||||||
|
"01007ef00011e000",
|
||||||
|
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||||
|
)
|
||||||
|
.AddSpec( // Super Mario Odyssey
|
||||||
|
"0100000000010000",
|
||||||
|
spec =>
|
||||||
|
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
||||||
|
)
|
||||||
|
.AddSpec( // Super Mario Odyssey (China)
|
||||||
|
"010075000ECBE000",
|
||||||
|
spec =>
|
||||||
|
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
||||||
|
)
|
||||||
|
.AddSpec( // Super Mario 3D World + Bowser's Fury
|
||||||
|
"010028600EBDA000",
|
||||||
|
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
||||||
|
)
|
||||||
|
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China)
|
||||||
|
["0100152000022000", "010075100E8EC000"],
|
||||||
|
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Analyzer implementation
|
||||||
|
|
||||||
|
public class PlayReportAnalyzer
|
||||||
|
{
|
||||||
|
private readonly List<PlayReportGameSpec> _specs = [];
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
|
||||||
|
{
|
||||||
|
if (!playReport.IsDictionary)
|
||||||
|
return PlayReportFormattedValue.Unhandled;
|
||||||
|
|
||||||
|
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
||||||
|
return PlayReportFormattedValue.Unhandled;
|
||||||
|
|
||||||
|
foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
|
||||||
|
{
|
||||||
|
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PlayReportValue value = new()
|
||||||
|
{
|
||||||
|
Application = appMeta,
|
||||||
|
BoxedValue = valuePackObject.ToObject()
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatSpec.ValueFormatter(ref value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlayReportFormattedValue.Unhandled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayReportGameSpec
|
||||||
|
{
|
||||||
|
public required string[] TitleIds { get; init; }
|
||||||
|
public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
|
||||||
|
|
||||||
|
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
Analyses.Add(new PlayReportValueFormatterSpec
|
||||||
|
{
|
||||||
|
Priority = Analyses.Count,
|
||||||
|
ReportKey = reportKey,
|
||||||
|
ValueFormatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
Analyses.Add(new PlayReportValueFormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority,
|
||||||
|
ReportKey = reportKey,
|
||||||
|
ValueFormatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PlayReportValue
|
||||||
|
{
|
||||||
|
public ApplicationMetadata Application { get; init; }
|
||||||
|
public object BoxedValue { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PlayReportFormattedValue
|
||||||
|
{
|
||||||
|
public bool Handled { get; private init; }
|
||||||
|
|
||||||
|
public bool Reset { get; private init; }
|
||||||
|
|
||||||
|
public string FormattedString { get; private init; }
|
||||||
|
|
||||||
|
public static implicit operator PlayReportFormattedValue(string formattedValue)
|
||||||
|
=> new() { Handled = true, FormattedString = formattedValue };
|
||||||
|
|
||||||
|
public static PlayReportFormattedValue Unhandled => default;
|
||||||
|
public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true };
|
||||||
|
|
||||||
|
public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl;
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PlayReportValueFormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string ReportKey { get; init; }
|
||||||
|
public PlayReportValueFormatter ValueFormatter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user