Merge branch 'master'
# Conflicts: # src/Ryujinx/Utilities/Configuration/ConfigurationFileFormat.cs # src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs
This commit is contained in:
commit
5a6d532579
@ -2483,7 +2483,7 @@
|
||||
0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23
|
||||
0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56
|
||||
010003A00D0B4000,"SaGa SCARLET GRACE: AMBITIONS™",,playable,2022-10-06 13:20:31
|
||||
01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN,playable,2023-12-04 18:33:37
|
||||
01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN;deadlock,ingame,2025-02-02 16:57:53
|
||||
0100DE600BEEE000,"SAINTS ROW®: THE THIRD™ - THE FULL PACKAGE",slow;LAN,playable,2023-08-24 02:40:58
|
||||
01007F000EB36000,"Sakai and...",nvdec,playable,2022-12-15 13:53:19
|
||||
0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13
|
||||
|
|
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
|
||||
}
|
||||
}
|
@ -10,14 +10,18 @@ namespace Ryujinx.Common.Helper
|
||||
public static bool IsMacOS => OperatingSystem.IsMacOS();
|
||||
public static bool IsWindows => OperatingSystem.IsWindows();
|
||||
public static bool IsLinux => OperatingSystem.IsLinux();
|
||||
|
||||
public static bool IsArm => RuntimeInformation.OSArchitecture is Architecture.Arm64;
|
||||
|
||||
public static bool IsX64 => RuntimeInformation.OSArchitecture is Architecture.X64;
|
||||
|
||||
public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64;
|
||||
public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64;
|
||||
public static bool IsIntelMac => IsMacOS && IsX64;
|
||||
public static bool IsArmMac => IsMacOS && IsArm;
|
||||
|
||||
public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64);
|
||||
public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
|
||||
public static bool IsX64Windows => IsWindows && IsX64;
|
||||
public static bool IsArmWindows => IsWindows && IsArm;
|
||||
|
||||
public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64);
|
||||
public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
|
||||
public static bool IsX64Linux => IsLinux && IsX64;
|
||||
public static bool IsArmLinux => IsLinux && IsArmMac;
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,8 @@ namespace Ryujinx.Common
|
||||
"01006f8002326000", // Animal Crossings: New Horizons
|
||||
"01009bf0072d4000", // Captain Toad: Treasure Tracker
|
||||
"01009510001ca000", // Fast RMX
|
||||
"01005CA01580E000", // Persona 5 Royale
|
||||
"01005CA01580E000", // Persona 5 Royal
|
||||
"0100b880154fc000", // Persona 5 The Royal (Japan)
|
||||
"010015100b514000", // Super Mario Bros. Wonder
|
||||
"0100000000010000", // Super Mario Odyssey
|
||||
|
||||
|
@ -87,7 +87,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
|
||||
}
|
||||
|
||||
using ManualResetEvent waitEvent = new(false);
|
||||
SyncpointWaiterHandle info = _syncpoints[id].RegisterCallback(threshold, (x) => waitEvent.Set());
|
||||
SyncpointWaiterHandle info = _syncpoints[id].RegisterCallback(threshold, _ => waitEvent.Set());
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
@ -96,7 +96,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
|
||||
|
||||
bool signaled = waitEvent.WaitOne(timeout);
|
||||
|
||||
if (!signaled && info != null)
|
||||
if (!signaled)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Wait on syncpoint {id} for threshold {threshold} took more than {timeout.TotalMilliseconds}ms, resuming execution...");
|
||||
|
||||
|
@ -16,14 +16,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
Unknown,
|
||||
}
|
||||
|
||||
static partial class VendorUtils
|
||||
static class VendorUtils
|
||||
{
|
||||
[GeneratedRegex("Radeon (((HD|R(5|7|9|X)) )?((M?[2-6]\\d{2}(\\D|$))|([7-8]\\d{3}(\\D|$))|Fury|Nano))|(Pro Duo)")]
|
||||
public static partial Regex AmdGcnRegex();
|
||||
|
||||
[GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")]
|
||||
public static partial Regex NvidiaConsumerClassRegex();
|
||||
|
||||
public static Vendor FromId(uint id)
|
||||
{
|
||||
return id switch
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
@ -375,11 +376,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}";
|
||||
|
||||
IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer);
|
||||
IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && Patterns.AmdGcn.IsMatch(GpuRenderer);
|
||||
|
||||
if (Vendor == Vendor.Nvidia)
|
||||
{
|
||||
Match match = VendorUtils.NvidiaConsumerClassRegex().Match(GpuRenderer);
|
||||
Match match = Patterns.NvidiaConsumerClass.Match(GpuRenderer);
|
||||
|
||||
if (match != null && int.TryParse(match.Groups[2].Value, out int gpuNumber))
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using LibHac.FsSystem;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
||||
using Ryujinx.HLE.HOS.SystemState;
|
||||
@ -30,9 +31,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error
|
||||
|
||||
public event EventHandler AppletStateChanged;
|
||||
|
||||
[GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")]
|
||||
private static partial Regex CleanTextRegex();
|
||||
|
||||
public ErrorApplet(Horizon horizon)
|
||||
{
|
||||
_horizon = horizon;
|
||||
@ -107,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Applets.Error
|
||||
|
||||
private static string CleanText(string value)
|
||||
{
|
||||
return CleanTextRegex().Replace(value, string.Empty).Replace("\0", string.Empty);
|
||||
return Patterns.CleanText.Replace(value, string.Empty).Replace("\0", string.Empty);
|
||||
}
|
||||
|
||||
private string GetMessageText(uint module, uint description, string key)
|
||||
|
@ -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;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Proxy
|
||||
{
|
||||
static partial class DnsBlacklist
|
||||
static class DnsBlacklist
|
||||
{
|
||||
const RegexOptions RegexOpts = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;
|
||||
|
||||
[GeneratedRegex(@"^(.*)\-lp1\.(n|s)\.n\.srv\.nintendo\.net$", RegexOpts)]
|
||||
private static partial Regex BlockedHost1();
|
||||
[GeneratedRegex(@"^(.*)\-lp1\.lp1\.t\.npln\.srv\.nintendo\.net$", RegexOpts)]
|
||||
private static partial Regex BlockedHost2();
|
||||
[GeneratedRegex(@"^(.*)\-lp1\.(znc|p)\.srv\.nintendo\.net$", RegexOpts)]
|
||||
private static partial Regex BlockedHost3();
|
||||
[GeneratedRegex(@"^(.*)\-sb\-api\.accounts\.nintendo\.com$", RegexOpts)]
|
||||
private static partial Regex BlockedHost4();
|
||||
[GeneratedRegex(@"^(.*)\-sb\.accounts\.nintendo\.com$", RegexOpts)]
|
||||
private static partial Regex BlockedHost5();
|
||||
[GeneratedRegex(@"^accounts\.nintendo\.com$", RegexOpts)]
|
||||
private static partial Regex BlockedHost6();
|
||||
|
||||
private static readonly Regex[] _blockedHosts =
|
||||
[
|
||||
BlockedHost1(),
|
||||
BlockedHost2(),
|
||||
BlockedHost3(),
|
||||
BlockedHost4(),
|
||||
BlockedHost5(),
|
||||
BlockedHost6()
|
||||
];
|
||||
|
||||
public static bool IsHostBlocked(string host)
|
||||
{
|
||||
foreach (Regex regex in _blockedHosts)
|
||||
foreach (Regex regex in Patterns.BlockedHosts)
|
||||
{
|
||||
if (regex.IsMatch(host))
|
||||
{
|
||||
|
@ -2,6 +2,7 @@ using LibHac.Common.FixedArrays;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Loader;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
@ -29,13 +30,6 @@ namespace Ryujinx.HLE.Loaders.Executables
|
||||
public string Name;
|
||||
public Array32<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)
|
||||
{
|
||||
NsoReader reader = new();
|
||||
@ -90,7 +84,7 @@ namespace Ryujinx.HLE.Loaders.Executables
|
||||
|
||||
if (string.IsNullOrEmpty(modulePath))
|
||||
{
|
||||
Match moduleMatch = ModuleRegex().Match(rawTextBuffer);
|
||||
Match moduleMatch = Patterns.Module.Match(rawTextBuffer);
|
||||
if (moduleMatch.Success)
|
||||
{
|
||||
modulePath = moduleMatch.Value;
|
||||
@ -99,13 +93,13 @@ namespace Ryujinx.HLE.Loaders.Executables
|
||||
|
||||
stringBuilder.AppendLine($" Module: {modulePath}");
|
||||
|
||||
Match fsSdkMatch = FsSdkRegex().Match(rawTextBuffer);
|
||||
Match fsSdkMatch = Patterns.FsSdk.Match(rawTextBuffer);
|
||||
if (fsSdkMatch.Success)
|
||||
{
|
||||
stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", string.Empty)}");
|
||||
}
|
||||
|
||||
MatchCollection sdkMwMatches = SdkMwRegex().Matches(rawTextBuffer);
|
||||
MatchCollection sdkMwMatches = Patterns.SdkMw.Matches(rawTextBuffer);
|
||||
if (sdkMwMatches.Count != 0)
|
||||
{
|
||||
string libHeader = " SDK Libraries: ";
|
||||
|
@ -1,31 +1,37 @@
|
||||
using MsgPack;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Horizon
|
||||
{
|
||||
public static class HorizonStatic
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static HorizonOptions _options;
|
||||
internal static void HandlePlayReport(MessagePackObject report) =>
|
||||
new Thread(() => PlayReport?.Invoke(report))
|
||||
{
|
||||
Name = "HLE.PlayReportEvent",
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.AboveNormal
|
||||
}.Start();
|
||||
|
||||
public static event Action<MessagePackObject> PlayReport;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ISyscallApi _syscall;
|
||||
[field: ThreadStatic]
|
||||
public static HorizonOptions Options { get; private set; }
|
||||
|
||||
[ThreadStatic]
|
||||
private static IVirtualMemoryManager _addressSpace;
|
||||
[field: ThreadStatic]
|
||||
public static ISyscallApi Syscall { get; private set; }
|
||||
|
||||
[ThreadStatic]
|
||||
private static IThreadContext _threadContext;
|
||||
[field: ThreadStatic]
|
||||
public static IVirtualMemoryManager AddressSpace { get; private set; }
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _threadHandle;
|
||||
[field: ThreadStatic]
|
||||
public static IThreadContext ThreadContext { get; private set; }
|
||||
|
||||
public static HorizonOptions Options => _options;
|
||||
public static ISyscallApi Syscall => _syscall;
|
||||
public static IVirtualMemoryManager AddressSpace => _addressSpace;
|
||||
public static IThreadContext ThreadContext => _threadContext;
|
||||
public static int CurrentThreadHandle => _threadHandle;
|
||||
[field: ThreadStatic]
|
||||
public static int CurrentThreadHandle { get; private set; }
|
||||
|
||||
public static void Register(
|
||||
HorizonOptions options,
|
||||
@ -34,11 +40,11 @@ namespace Ryujinx.Horizon
|
||||
IThreadContext threadContext,
|
||||
int threadHandle)
|
||||
{
|
||||
_options = options;
|
||||
_syscall = syscallApi;
|
||||
_addressSpace = addressSpace;
|
||||
_threadContext = threadContext;
|
||||
_threadHandle = threadHandle;
|
||||
Options = options;
|
||||
Syscall = syscallApi;
|
||||
AddressSpace = addressSpace;
|
||||
ThreadContext = threadContext;
|
||||
CurrentThreadHandle = threadHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using Gommon;
|
||||
using MsgPack;
|
||||
using MsgPack.Serialization;
|
||||
using Ryujinx.Common.Logging;
|
||||
@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf;
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
|
||||
|
||||
namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
@ -230,6 +232,8 @@ namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
|
||||
builder.AppendLine($" Room: {gameRoom}");
|
||||
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
|
||||
|
||||
HorizonStatic.HandlePlayReport(deserializedReport);
|
||||
|
||||
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
|
||||
|
||||
|
@ -925,7 +925,9 @@ namespace Ryujinx.Ava
|
||||
ConfigurationState.Instance.System.EnableInternetAccess,
|
||||
ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
|
||||
ConfigurationState.Instance.System.FsGlobalAccessLogMode,
|
||||
ConfigurationState.Instance.System.SystemTimeOffset,
|
||||
ConfigurationState.Instance.System.MatchSystemTime
|
||||
? 0
|
||||
: ConfigurationState.Instance.System.SystemTimeOffset,
|
||||
ConfigurationState.Instance.System.TimeZone,
|
||||
ConfigurationState.Instance.System.MemoryManagerMode,
|
||||
ConfigurationState.Instance.System.IgnoreMissingServices,
|
||||
|
@ -1524,6 +1524,156 @@
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderDeveloper",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Developed by {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "Έκδοση: {0}",
|
||||
"en_US": "Version: {0}",
|
||||
"es_ES": "Versión: {0}",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "Versione: {0}",
|
||||
"ja_JP": "バージョン: {0}",
|
||||
"ko_KR": "버전: {0}",
|
||||
"no_NO": "Versjon: {0}",
|
||||
"pl_PL": "Wersja: {0}",
|
||||
"pt_BR": "Versão: {0}",
|
||||
"ru_RU": "Версия: {0}",
|
||||
"sv_SE": "",
|
||||
"th_TH": "เวอร์ชั่น: {0}",
|
||||
"tr_TR": "Sürüm: {0}",
|
||||
"uk_UA": "Версія: {0}",
|
||||
"zh_CN": "版本: {0}",
|
||||
"zh_TW": "版本: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTimePlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Spielzeit: {0}",
|
||||
"el_GR": "Χρόνος: {0}",
|
||||
"en_US": "Play Time: {0}",
|
||||
"es_ES": "Tiempo jugado: {0}",
|
||||
"fr_FR": "Temps de jeu: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Tempo di gioco: {0}",
|
||||
"ja_JP": "プレイ時間: {0}",
|
||||
"ko_KR": "플레이 타임: {0}",
|
||||
"no_NO": "Spilletid: {0}",
|
||||
"pl_PL": "Czas w grze: {0}",
|
||||
"pt_BR": "Tempo de jogo: {0}",
|
||||
"ru_RU": "Время в игре: {0}",
|
||||
"sv_SE": "Speltid: {0}",
|
||||
"th_TH": "เล่นไปแล้ว: {0}",
|
||||
"tr_TR": "Oynama Süresi: {0}",
|
||||
"uk_UA": "Зіграно часу: {0}",
|
||||
"zh_CN": "游玩时长: {0}",
|
||||
"zh_TW": "遊玩時數: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderLastPlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Zuletzt gespielt: {0}",
|
||||
"el_GR": "Παίχτηκε: {0}",
|
||||
"en_US": "Last Played: {0}",
|
||||
"es_ES": "Jugado por última vez: {0}",
|
||||
"fr_FR": "Dernière partie jouée: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Ultima partita: {0}",
|
||||
"ja_JP": "最終プレイ日時: {0}",
|
||||
"ko_KR": "마지막 플레이: {0}",
|
||||
"no_NO": "Sist Spilt: {0}",
|
||||
"pl_PL": "Ostatnio grane: {0}",
|
||||
"pt_BR": "Último jogo: {0}",
|
||||
"ru_RU": "Последний запуск: {0}",
|
||||
"sv_SE": "Senast spelad: {0}",
|
||||
"th_TH": "เล่นล่าสุด: {0}",
|
||||
"tr_TR": "Son Oynama Tarihi: {0}",
|
||||
"uk_UA": "Востаннє зіграно: {0}",
|
||||
"zh_CN": "最近游玩: {0}",
|
||||
"zh_TW": "最近遊玩: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileExtension",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Dateiformat: {0}",
|
||||
"el_GR": "Κατάληξη: {0}",
|
||||
"en_US": "Extension: {0}",
|
||||
"es_ES": "Extensión: {0}",
|
||||
"fr_FR": "Extension du Fichier: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Estensione: {0}",
|
||||
"ja_JP": "ファイル拡張子: {0}",
|
||||
"ko_KR": "파일 확장자: {0}",
|
||||
"no_NO": "Fil Eks.: {0}",
|
||||
"pl_PL": "Rozszerzenie pliku: {0}",
|
||||
"pt_BR": "Extensão: {0}",
|
||||
"ru_RU": "Расширение файла: {0}",
|
||||
"sv_SE": "Filänd: {0}",
|
||||
"th_TH": "นามสกุลไฟล์: {0}",
|
||||
"tr_TR": "Dosya Uzantısı: {0}",
|
||||
"uk_UA": "Розширення файлу: {0}",
|
||||
"zh_CN": "扩展名: {0}",
|
||||
"zh_TW": "副檔名: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileSize",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Dateigröße: {0}",
|
||||
"el_GR": "Μέγεθος Αρχείου: {0}",
|
||||
"en_US": "File Size: {0}",
|
||||
"es_ES": "Tamaño del archivo: {0}",
|
||||
"fr_FR": "Taille du Fichier: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Dimensione file: {0}",
|
||||
"ja_JP": "ファイルサイズ: {0}",
|
||||
"ko_KR": "파일 크기: {0}",
|
||||
"no_NO": "Fil Størrelse: {0}",
|
||||
"pl_PL": "Rozmiar pliku: {0}",
|
||||
"pt_BR": "Tamanho: {0}",
|
||||
"ru_RU": "Размер файла: {0}",
|
||||
"sv_SE": "Filstorlek: {0}",
|
||||
"th_TH": "ขนาดไฟล์: {0}",
|
||||
"tr_TR": "Dosya Boyutu: {0}",
|
||||
"uk_UA": "Розмір файлу: {0}",
|
||||
"zh_CN": "大小: {0}",
|
||||
"zh_TW": "檔案大小: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListSortDeveloper",
|
||||
"Translations": {
|
||||
"ar_SA": "المطور",
|
||||
"de_DE": "Entwickler",
|
||||
@ -1548,32 +1698,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "الإصدار",
|
||||
"de_DE": "",
|
||||
"el_GR": "Έκδοση",
|
||||
"en_US": "Version",
|
||||
"es_ES": "Versión",
|
||||
"fr_FR": "",
|
||||
"he_IL": "גרסה",
|
||||
"it_IT": "Versione",
|
||||
"ja_JP": "バージョン",
|
||||
"ko_KR": "버전",
|
||||
"no_NO": "Versjon",
|
||||
"pl_PL": "Wersja",
|
||||
"pt_BR": "Versão",
|
||||
"ru_RU": "Версия",
|
||||
"sv_SE": "",
|
||||
"th_TH": "เวอร์ชั่น",
|
||||
"tr_TR": "Sürüm",
|
||||
"uk_UA": "Версія",
|
||||
"zh_CN": "版本",
|
||||
"zh_TW": "版本"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTimePlayed",
|
||||
"ID": "GameListSortTimePlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "وقت اللعب",
|
||||
"de_DE": "Spielzeit",
|
||||
@ -1598,7 +1723,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderLastPlayed",
|
||||
"ID": "GameListSortLastPlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "آخر مرة لُعبت",
|
||||
"de_DE": "Zuletzt gespielt",
|
||||
@ -1623,7 +1748,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileExtension",
|
||||
"ID": "GameListSortFileExtension",
|
||||
"Translations": {
|
||||
"ar_SA": "صيغة الملف",
|
||||
"de_DE": "Dateiformat",
|
||||
@ -1648,7 +1773,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileSize",
|
||||
"ID": "GameListSortFileSize",
|
||||
"Translations": {
|
||||
"ar_SA": "حجم الملف",
|
||||
"de_DE": "Dateigröße",
|
||||
@ -1673,7 +1798,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderPath",
|
||||
"ID": "GameListSortPath",
|
||||
"Translations": {
|
||||
"ar_SA": "المسار",
|
||||
"de_DE": "Pfad",
|
||||
@ -1697,6 +1822,106 @@
|
||||
"zh_TW": "路徑"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderCompatibilityStatus",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Compatibility:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTitleId",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Title ID:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderHostedGames",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Hosted Games: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderPlayerCount",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Online Players: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuOpenUserSaveDirectory",
|
||||
"Translations": {
|
||||
@ -2522,6 +2747,106 @@
|
||||
"zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowCompatEntry",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show Compatibility Entry",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowCompatEntryToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show the selected game in the Compatibility List you can normally access via the Help menu.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowGameData",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show Game Info",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowGameDataToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show stats & details about the currently selected game.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuOpenModsDirectory",
|
||||
"Translations": {
|
||||
@ -4153,23 +4478,23 @@
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Resync to PC Date & Time",
|
||||
"en_US": "Match System Time",
|
||||
"es_ES": "",
|
||||
"fr_FR": "Resynchronier la Date à celle du PC",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "Sincronizza data e ora con il PC",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "PC 날짜와 시간에 동기화",
|
||||
"no_NO": "Resynkroniser til PC-dato og -klokkeslett",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Повторная синхронизация с датой и временем на компьютере",
|
||||
"sv_SE": "Återsynka till datorns datum och tid",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Синхронізувати з датою та часом ПК",
|
||||
"zh_CN": "与 PC 日期和时间重新同步",
|
||||
"zh_TW": "重新同步至 PC 的日期和時間"
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -15528,23 +15853,23 @@
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Resync System Time to match your PC's current date & time.\n\nThis is not an active setting, it can still fall out of sync; in which case just click this button again.",
|
||||
"en_US": "Sync System Time to match your PC's current date & time.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.\n\nCeci n'est pas un paramètrage automatique, la date peut se désynchroniser; dans ce cas là, rappuyer sur le boutton.",
|
||||
"fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.",
|
||||
"he_IL": "",
|
||||
"it_IT": "Sincronizza data e ora del sistema con quelle del PC.\n\nQuesta non è un'opzione attiva, perciò data e ora potrebbero tornare a non essere sincronizzate: in tal caso basterà cliccare nuovamente questo pulsante.",
|
||||
"it_IT": "Sincronizza data e ora del sistema con quelle del PC.",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.\n\n이 설정은 활성 설정이 아니므로 여전히 동기화되지 않을 수 있으며, 이 경우 이 버튼을 다시 클릭하면 됩니다.",
|
||||
"no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett. \\Dette er ikke en aktiv innstilling, men den kan likevel komme ut av synkronisering; i så fall er det bare å klikke på denne knappen igjen.",
|
||||
"ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.",
|
||||
"no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.\n\nЭто не активная настройка, она все еще может рассинхронизироваться; в этом случае просто нажмите эту кнопку еще раз.",
|
||||
"sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.\n\nDetta är inte en aktiv inställning och den kan tappa synken och om det händer så kan du klicka på denna knapp igen.",
|
||||
"ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.",
|
||||
"sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.",
|
||||
"zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间,时间仍然可能不同步;在这种情况下,只需再次单击此按钮即可。",
|
||||
"zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。\n\n這不是一個主動設定,它仍然可能會失去同步;在這種情況下,只需再次點擊此按鈕。"
|
||||
"uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.",
|
||||
"zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。",
|
||||
"zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23044,7 +23369,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "可游玩",
|
||||
"zh_TW": "可暢順遊玩 (Playable)"
|
||||
"zh_TW": "可暢順遊玩"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23069,7 +23394,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "进入游戏",
|
||||
"zh_TW": "大致可遊玩 (Ingame)"
|
||||
"zh_TW": "大致可遊玩"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23094,7 +23419,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "菜单",
|
||||
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
|
||||
"zh_TW": "只開啟至遊戲開始功能表"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23119,7 +23444,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "启动",
|
||||
"zh_TW": "只能啟動 (Boots)"
|
||||
"zh_TW": "只能啟動"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23144,7 +23469,132 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "什么都没有",
|
||||
"zh_TW": "無法啟動 (Nothing)"
|
||||
"zh_TW": "無法啟動"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListPlayableTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListIngameTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListMenusTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots and goes past the title screen but does not make it into main gameplay.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListBootsTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots but does not make it past the title screen.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListNothingTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Does not boot or shows no signs of activity.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,11 +1,19 @@
|
||||
using DiscordRPC;
|
||||
using Gommon;
|
||||
using MsgPack;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Configuration;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.Loaders.Processes;
|
||||
using Ryujinx.Horizon;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava
|
||||
@ -16,12 +24,12 @@ namespace Ryujinx.Ava
|
||||
public static Timestamps GuestAppStartedAt { get; set; }
|
||||
|
||||
private static string VersionString
|
||||
=> (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}";
|
||||
=> (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}";
|
||||
|
||||
private static readonly string _description =
|
||||
ReleaseInformation.IsValid
|
||||
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
|
||||
: "dev build";
|
||||
private static readonly string _description =
|
||||
ReleaseInformation.IsValid
|
||||
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
|
||||
: "dev build";
|
||||
|
||||
private const string ApplicationId = "1293250299716173864";
|
||||
|
||||
@ -30,6 +38,8 @@ namespace Ryujinx.Ava
|
||||
|
||||
private static DiscordRpcClient _discordClient;
|
||||
private static RichPresence _discordPresenceMain;
|
||||
private static RichPresence _discordPresencePlaying;
|
||||
private static ApplicationMetadata _currentApp;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
@ -37,8 +47,7 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Assets = new Assets
|
||||
{
|
||||
LargeImageKey = "ryujinx",
|
||||
LargeImageText = TruncateToByteLength(_description)
|
||||
LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description)
|
||||
},
|
||||
Details = "Main Menu",
|
||||
State = "Idling",
|
||||
@ -47,6 +56,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
|
||||
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
|
||||
HorizonStatic.PlayReport += HandlePlayReport;
|
||||
}
|
||||
|
||||
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
|
||||
@ -77,16 +87,15 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
if (titleId.TryGet(out string tid))
|
||||
SwitchToPlayingState(
|
||||
ApplicationLibrary.LoadAndSaveMetaData(tid),
|
||||
ApplicationLibrary.LoadAndSaveMetaData(tid),
|
||||
Switch.Shared.Processes.ActiveApplication
|
||||
);
|
||||
else
|
||||
else
|
||||
SwitchToMainState();
|
||||
}
|
||||
|
||||
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
|
||||
{
|
||||
_discordClient?.SetPresence(new RichPresence
|
||||
private static RichPresence CreatePlayingState(ApplicationMetadata appMeta, ProcessResult procRes) =>
|
||||
new()
|
||||
{
|
||||
Assets = new Assets
|
||||
{
|
||||
@ -100,10 +109,42 @@ namespace Ryujinx.Ava
|
||||
? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}"
|
||||
: "Never played",
|
||||
Timestamps = GuestAppStartedAt ??= Timestamps.Now
|
||||
});
|
||||
};
|
||||
|
||||
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
|
||||
{
|
||||
_discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes));
|
||||
_currentApp = appMeta;
|
||||
}
|
||||
|
||||
private static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain);
|
||||
private static void SwitchToMainState()
|
||||
{
|
||||
_discordClient?.SetPresence(_discordPresenceMain);
|
||||
_discordPresencePlaying = null;
|
||||
_currentApp = null;
|
||||
}
|
||||
|
||||
private static void HandlePlayReport(MessagePackObject playReport)
|
||||
{
|
||||
if (_discordClient is null) return;
|
||||
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
||||
if (_discordPresencePlaying is null) return;
|
||||
|
||||
PlayReportAnalyzer.FormattedValue formattedValue =
|
||||
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||
|
||||
if (!formattedValue.Handled) return;
|
||||
|
||||
_discordPresencePlaying.Details = formattedValue.Reset
|
||||
? $"Playing {_currentApp.Title}"
|
||||
: formattedValue.FormattedString;
|
||||
|
||||
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
||||
return; //don't trigger an update if the set presence Details are identical to current
|
||||
|
||||
_discordClient.SetPresence(_discordPresencePlaying);
|
||||
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||
}
|
||||
|
||||
private static string TruncateToByteLength(string input)
|
||||
{
|
||||
|
@ -32,6 +32,9 @@ namespace Ryujinx.Ava
|
||||
public static MainWindow MainWindow => Current!
|
||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
|
||||
.MainWindow.Cast<MainWindow>();
|
||||
|
||||
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
|
||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
|
||||
|
||||
public static bool IsClipboardAvailable(out IClipboard clipboard)
|
||||
{
|
||||
|
@ -144,12 +144,12 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
case KeyboardMode.Numeric:
|
||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric);
|
||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
||||
_checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
|
||||
_checkInput = text => text.All(CharacterValidation.IsNumeric);
|
||||
break;
|
||||
case KeyboardMode.Alphabet:
|
||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet);
|
||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
||||
_checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value));
|
||||
_checkInput = text => text.All(value => !CharacterValidation.IsCJK(value));
|
||||
break;
|
||||
case KeyboardMode.ASCII:
|
||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII);
|
||||
|
@ -19,6 +19,17 @@
|
||||
Header="{ext:Locale GameListContextMenuCreateShortcut}"
|
||||
Icon="{ext:Icon fa-solid fa-bookmark}"
|
||||
ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasCompatibilityEntry}"
|
||||
Click="OpenApplicationCompatibility_Click"
|
||||
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
|
||||
Icon="{ext:Icon mdi-gamepad}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
|
||||
<MenuItem
|
||||
Click="OpenApplicationData_Click"
|
||||
Header="{ext:Locale GameListContextMenuShowGameData}"
|
||||
Icon="{ext:Icon mdi-chart-line}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenUserSaveDirectory_Click"
|
||||
@ -74,7 +85,6 @@
|
||||
Header="{ext:Locale GameListContextMenuTrimXCI}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon mdi-cached}">
|
||||
<MenuItem
|
||||
Click="PurgePtcCache_Click"
|
||||
@ -112,6 +122,7 @@
|
||||
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Click="ExtractAocRomFs_Click"
|
||||
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
|
||||
|
@ -12,6 +12,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.HLE.HOS;
|
||||
@ -333,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
return;
|
||||
|
||||
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
|
||||
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
|
||||
|
||||
if (selectedDlc is not null)
|
||||
{
|
||||
@ -385,6 +386,18 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
viewModel.SelectedApplication.Icon
|
||||
);
|
||||
}
|
||||
|
||||
public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
|
||||
}
|
||||
|
||||
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await ApplicationDataView.Show(viewModel.SelectedApplication);
|
||||
}
|
||||
|
||||
public async void RunApplication_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
@ -394,12 +407,8 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
114
src/Ryujinx/UI/Controls/ApplicationDataView.axaml
Normal file
114
src/Ryujinx/UI/Controls/ApplicationDataView.axaml
Normal file
@ -0,0 +1,114 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
|
||||
x:DataType="viewModels:ApplicationDataViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Margin="0"
|
||||
MaxWidth="256"
|
||||
MinWidth="256"
|
||||
Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<Border Margin="5, 0" Width="1" Height="256" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnDefinitions="*">
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock HorizontalAlignment="Left"
|
||||
Text="{Binding FormattedVersion}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Left"
|
||||
Text="{Binding FormattedDeveloper}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedFileExtension}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedFileSize}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel Grid.Row="2"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding AppData.HasPlayabilityInfo}">
|
||||
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderCompatibilityStatus}" />
|
||||
<Button
|
||||
Click="PlayabilityStatus_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Padding="0">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
Tag="{Binding AppData.IdString}"
|
||||
Text="{Binding AppData.LocalizedStatus}"
|
||||
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
ToolTip.Tip="{Binding AppData.LocalizedStatusTooltip}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinWidth"
|
||||
Value="0" />
|
||||
<!-- avoids very wide buttons from the overall project avalonia style -->
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderTitleId}" />
|
||||
<Button
|
||||
Click="IdString_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Padding="0">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding AppData.IdString}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding AppData.HasLdnGames}"
|
||||
Text="{Binding FormattedLdnInfo}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedLastPlayed}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedPlayTime}"
|
||||
IsVisible="{Binding AppData.HasPlayedPreviously}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
86
src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
Normal file
86
src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class ApplicationDataView : UserControl
|
||||
{
|
||||
public static async Task Show(ApplicationData appData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = appData.Name,
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
MinWidth = 256,
|
||||
Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) }
|
||||
};
|
||||
|
||||
Style closeButton = new(x => x.Name("CloseButton"));
|
||||
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
|
||||
|
||||
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
|
||||
Avalonia.Layout.HorizontalAlignment.Center));
|
||||
|
||||
contentDialog.Styles.Add(closeButton);
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
public ApplicationDataView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Content: TextBlock playabilityLabel })
|
||||
return;
|
||||
|
||||
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
|
||||
window.Close(ContentDialogResult.None);
|
||||
|
||||
await CompatibilityList.Show((string)playabilityLabel.Tag);
|
||||
}
|
||||
|
||||
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel mwvm)
|
||||
return;
|
||||
|
||||
if (sender is not Button { Content: TextBlock idText })
|
||||
return;
|
||||
|
||||
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
|
||||
return;
|
||||
|
||||
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
|
||||
if (appData is null)
|
||||
return;
|
||||
|
||||
await clipboard.SetTextAsync(appData.IdString);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
"Copied Title ID",
|
||||
$"{appData.Name} ({appData.IdString})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,30 @@
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
Click="PlayabilityStatus_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding HasPlayabilityInfo}"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Margin="-1, 0, 0, 0"
|
||||
Padding="0"
|
||||
ToolTip.Tip="{Binding LocalizedStatusTooltip}">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
Tag="{Binding IdString}"
|
||||
Text="{Binding LocalizedStatus}"
|
||||
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinWidth"
|
||||
Value="0" />
|
||||
<!-- avoids very wide buttons from the overall project avalonia style -->
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel
|
||||
@ -117,7 +141,8 @@
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
|
||||
IsVisible="{Binding HasLdnGames}"
|
||||
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
@ -5,7 +5,9 @@ using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
@ -28,6 +30,17 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (sender is ListBox { SelectedItem: ApplicationData selected })
|
||||
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
|
||||
}
|
||||
|
||||
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel mwvm)
|
||||
return;
|
||||
|
||||
if (sender is not Button { Content: TextBlock playabilityLabel })
|
||||
return;
|
||||
|
||||
await CompatibilityList.Show((string)playabilityLabel.Tag);
|
||||
}
|
||||
|
||||
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
@ -159,6 +159,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
Symbol = (Symbol)symbol,
|
||||
Margin = new Thickness(10),
|
||||
FontSize = 40,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
|
@ -1,27 +1,31 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly MultiplayerInfoConverter _instance = new();
|
||||
public static readonly MultiplayerInfoConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ApplicationData applicationData)
|
||||
{
|
||||
if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
|
||||
{
|
||||
return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
if (value is not ApplicationData { HasLdnGames: true } applicationData)
|
||||
return "";
|
||||
|
||||
return new StringBuilder()
|
||||
.AppendLine(
|
||||
LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames]
|
||||
.Format(applicationData.GameCount))
|
||||
.Append(
|
||||
LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount]
|
||||
.Format(applicationData.PlayerCount))
|
||||
.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
@ -31,7 +35,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
return Instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using Ryujinx.Common.Helper;
|
||||
using SharpMetal.QuartzCore;
|
||||
using System;
|
||||
|
||||
@ -7,14 +8,12 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
{
|
||||
public CAMetalLayer CreateSurface()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
if (OperatingSystem.IsMacOS() && RunningPlatform.IsArm)
|
||||
{
|
||||
return new CAMetalLayer(MetalLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Cannot create a {nameof(CAMetalLayer)} without being on ARM Mac.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,19 +43,19 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
|
||||
public RendererHost(string titleId)
|
||||
{
|
||||
switch (TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend))
|
||||
{
|
||||
case GraphicsBackend.OpenGl:
|
||||
EmbeddedWindow = new EmbeddedWindowOpenGL();
|
||||
break;
|
||||
case GraphicsBackend.Metal:
|
||||
EmbeddedWindow = new EmbeddedWindowMetal();
|
||||
break;
|
||||
case GraphicsBackend.Vulkan:
|
||||
EmbeddedWindow = new EmbeddedWindowVulkan();
|
||||
break;
|
||||
}
|
||||
|
||||
Focusable = true;
|
||||
FlowDirection = FlowDirection.LeftToRight;
|
||||
|
||||
EmbeddedWindow =
|
||||
#pragma warning disable CS8509
|
||||
TitleIDs.SelectGraphicsBackend(titleId, ConfigurationState.Instance.Graphics.GraphicsBackend) switch
|
||||
#pragma warning restore CS8509
|
||||
{
|
||||
GraphicsBackend.OpenGl => new EmbeddedWindowOpenGL(),
|
||||
GraphicsBackend.Metal => new EmbeddedWindowMetal(),
|
||||
GraphicsBackend.Vulkan => new EmbeddedWindowVulkan(),
|
||||
};
|
||||
|
||||
string backendText = EmbeddedWindow switch
|
||||
{
|
||||
EmbeddedWindowVulkan => "Vulkan",
|
||||
|
26
src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
Normal file
26
src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class ApplicationDataViewModel : BaseModel
|
||||
{
|
||||
public ApplicationData AppData { get; }
|
||||
|
||||
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
|
||||
|
||||
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
|
||||
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
|
||||
|
||||
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
|
||||
public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
|
||||
public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
|
||||
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
|
||||
|
||||
public string FormattedLdnInfo =>
|
||||
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" +
|
||||
$"\n" +
|
||||
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}";
|
||||
}
|
||||
}
|
@ -14,9 +14,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
|
||||
{
|
||||
_dlcs = appLibrary.DownloadableContents.Items
|
||||
.Where(x => x.Dlc.TitleIdBase == titleId)
|
||||
.Select(x => x.Dlc)
|
||||
_dlcs = appLibrary.FindDlcsFor(titleId)
|
||||
.OrderBy(it => it.IsBundled ? 0 : 1)
|
||||
.ThenBy(it => it.TitleId)
|
||||
.ToArray();
|
||||
|
@ -69,8 +69,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
|
||||
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
|
||||
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
|
||||
|
@ -349,6 +349,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
|
||||
|
||||
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
|
||||
|
||||
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
|
||||
public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||
@ -629,15 +633,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
return SortMode switch
|
||||
{
|
||||
ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
|
||||
ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper],
|
||||
ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed],
|
||||
ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed],
|
||||
ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension],
|
||||
ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize],
|
||||
ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath],
|
||||
ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite],
|
||||
ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel],
|
||||
ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
|
||||
ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper],
|
||||
ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed],
|
||||
ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed],
|
||||
ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListSortFileExtension],
|
||||
ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListSortFileSize],
|
||||
ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListSortPath],
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Gommon;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL3;
|
||||
@ -16,6 +16,7 @@ using Ryujinx.Ava.Utilities.Configuration.System;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
@ -27,8 +28,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
|
||||
@ -115,10 +114,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS();
|
||||
|
||||
public bool IsAppleSiliconMac => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
|
||||
|
||||
public bool IsMacOS => OperatingSystem.IsMacOS();
|
||||
|
||||
public bool EnableDiscordIntegration { get; set; }
|
||||
public bool CheckUpdatesOnStart { get; set; }
|
||||
public bool ShowConfirmExit { get; set; }
|
||||
@ -200,7 +195,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public bool EnableTextureRecompression { get; set; }
|
||||
public bool EnableMacroHLE { get; set; }
|
||||
public bool EnableColorSpacePassthrough { get; set; }
|
||||
public bool ColorSpacePassthroughAvailable => IsMacOS;
|
||||
public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS;
|
||||
public bool EnableFileLog { get; set; }
|
||||
public bool EnableStub { get; set; }
|
||||
public bool EnableInfo { get; set; }
|
||||
@ -296,6 +291,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty] private bool _matchSystemTime;
|
||||
|
||||
public DateTimeOffset CurrentDate { get; set; }
|
||||
|
||||
public TimeSpan CurrentTime { get; set; }
|
||||
@ -330,9 +327,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("Ryujinx-[0-9a-f]{8}")]
|
||||
private static partial Regex LdnPassphraseRegex();
|
||||
|
||||
public bool IsInvalidLdnPassphraseVisible { get; set; }
|
||||
|
||||
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
|
||||
@ -414,17 +408,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex)));
|
||||
}
|
||||
|
||||
public void MatchSystemTime()
|
||||
{
|
||||
(DateTimeOffset dto, TimeSpan timeOfDay) = DateTimeOffset.Now.Extract();
|
||||
|
||||
CurrentDate = dto;
|
||||
CurrentTime = timeOfDay;
|
||||
|
||||
OnPropertyChanged(nameof(CurrentDate));
|
||||
OnPropertyChanged(nameof(CurrentTime));
|
||||
}
|
||||
|
||||
public async Task LoadTimeZones()
|
||||
{
|
||||
_timeZoneContentManager = new TimeZoneContentManager();
|
||||
@ -470,7 +453,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private bool ValidateLdnPassphrase(string passphrase)
|
||||
{
|
||||
return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase));
|
||||
return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && Patterns.LdnPassphrase.IsMatch(passphrase));
|
||||
}
|
||||
|
||||
public void ValidateAndSetTimeZone(string location)
|
||||
@ -526,7 +509,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
CurrentDate = currentDateTime.Date;
|
||||
CurrentTime = currentDateTime.TimeOfDay;
|
||||
|
||||
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value;
|
||||
MatchSystemTime = config.System.MatchSystemTime;
|
||||
|
||||
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval;
|
||||
CustomVSyncInterval = config.Graphics.CustomVSyncInterval;
|
||||
VSyncMode = config.Graphics.VSyncMode;
|
||||
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
||||
@ -631,6 +616,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
config.System.TimeZone.Value = TimeZone;
|
||||
}
|
||||
|
||||
config.System.MatchSystemTime.Value = MatchSystemTime;
|
||||
config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
|
||||
config.System.DramSize.Value = DramSize;
|
||||
@ -734,6 +720,25 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
CloseWindow?.Invoke();
|
||||
}
|
||||
|
||||
[ObservableProperty] private bool _wantsToReset;
|
||||
|
||||
public AsyncRelayCommand ResetButton => Commands.Create(async () =>
|
||||
{
|
||||
if (!WantsToReset) return;
|
||||
|
||||
CloseWindow?.Invoke();
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
RyujinxApp.MainWindow.LoadApplications();
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
$"Your {RyujinxApp.FullAppName} configuration has been reset.",
|
||||
"",
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
"Configuration Reset");
|
||||
});
|
||||
|
||||
public void CancelButton()
|
||||
{
|
||||
RevertIfNotSaved();
|
||||
|
@ -41,8 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
|
||||
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
|
||||
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
SelectedUpdate = new TitleUpdateViewModelNoUpdate();
|
||||
|
@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
|
||||
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
|
||||
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
|
||||
CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show);
|
||||
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show());
|
||||
|
||||
UpdateMenuItem.Command = Commands.Create(async () =>
|
||||
{
|
||||
|
@ -113,37 +113,37 @@
|
||||
Tag="TitleId" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderDeveloper}"
|
||||
Content="{ext:Locale GameListSortDeveloper}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
|
||||
Tag="Developer" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderTimePlayed}"
|
||||
Content="{ext:Locale GameListSortTimePlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
|
||||
Tag="TotalTimePlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderLastPlayed}"
|
||||
Content="{ext:Locale GameListSortLastPlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
|
||||
Tag="LastPlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderFileExtension}"
|
||||
Content="{ext:Locale GameListSortFileExtension}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
|
||||
Tag="FileType" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderFileSize}"
|
||||
Content="{ext:Locale GameListSortFileSize}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
|
||||
Tag="FileSize" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderPath}"
|
||||
Content="{ext:Locale GameListSortPath}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
|
||||
Tag="Path" />
|
||||
|
@ -6,6 +6,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||
mc:Ignorable="d"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
@ -69,7 +70,7 @@
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding UseHypervisor}"
|
||||
IsVisible="{Binding IsAppleSiliconMac}"
|
||||
IsVisible="{x:Static helper:RunningPlatform.IsArmMac}"
|
||||
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}">
|
||||
<TextBlock Text="{ext:Locale SettingsTabSystemUseHypervisor}"
|
||||
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}" />
|
||||
|
@ -8,6 +8,7 @@
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||
Design.Width="1000"
|
||||
mc:Ignorable="d"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
@ -48,7 +49,7 @@
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
|
||||
<TextBlock Text="OpenGL" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsAppleSiliconMac}">
|
||||
<ComboBoxItem IsEnabled="{x:Static helper:RunningPlatform.IsArmMac}">
|
||||
<TextBlock Text="Metal (ARM Mac only, Experimental)" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
@ -170,7 +170,8 @@
|
||||
ToolTip.Tip="{ext:Locale TimeTooltip}"
|
||||
Width="250"/>
|
||||
<DatePicker
|
||||
VerticalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding !MatchSystemTime}"
|
||||
SelectedDate="{Binding CurrentDate}"
|
||||
ToolTip.Tip="{ext:Locale TimeTooltip}"
|
||||
Width="350" />
|
||||
@ -181,17 +182,21 @@
|
||||
<TimePicker
|
||||
VerticalAlignment="Center"
|
||||
ClockIdentifier="24HourClock"
|
||||
IsEnabled="{Binding !MatchSystemTime}"
|
||||
SelectedTime="{Binding CurrentTime}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{ext:Locale TimeTooltip}" />
|
||||
<Button
|
||||
Margin="10, 0, 0, 0"
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Click="MatchSystemTime_OnClick"
|
||||
Background="{DynamicResource SystemAccentColor}"
|
||||
ToolTip.Tip="{ext:Locale MatchTimeTooltip}">
|
||||
<TextBlock Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" />
|
||||
</Button>
|
||||
Text="{ext:Locale SettingsTabSystemSystemTimeMatch}"
|
||||
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"
|
||||
Width="250"/>
|
||||
<CheckBox
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding MatchSystemTime}"
|
||||
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"/>
|
||||
</StackPanel>
|
||||
<Separator />
|
||||
<StackPanel Margin="0,10,0,10"
|
||||
|
@ -34,7 +34,5 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
|
||||
private void MatchSystemTime_OnClick(object sender, RoutedEventArgs e) => ViewModel.MatchSystemTime();
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||
Width="1100"
|
||||
Height="768"
|
||||
MinWidth="800"
|
||||
@ -107,24 +108,36 @@
|
||||
</Style>
|
||||
</ui:NavigationView.Styles>
|
||||
</ui:NavigationView>
|
||||
<ReversibleStackPanel
|
||||
Grid.Row="2"
|
||||
Margin="10"
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
ReverseOrder="{Binding IsMacOS}">
|
||||
<Button
|
||||
Classes="accent"
|
||||
Content="{ext:Locale SettingsButtonOk}"
|
||||
Command="{Binding OkButton}" />
|
||||
<Button
|
||||
HotKey="Escape"
|
||||
Content="{ext:Locale SettingsButtonCancel}"
|
||||
Command="{Binding CancelButton}" />
|
||||
<Button
|
||||
Content="{ext:Locale SettingsButtonApply}"
|
||||
Command="{Binding ApplyButton}" />
|
||||
</ReversibleStackPanel>
|
||||
<Grid Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Button
|
||||
IsEnabled="{Binding WantsToReset}"
|
||||
Margin="10"
|
||||
Content="Reset Settings"
|
||||
Command="{Binding ResetButton}" />
|
||||
<CheckBox IsChecked="{Binding WantsToReset}"/>
|
||||
<TextBlock Text="I want to reset my settings."/>
|
||||
</StackPanel>
|
||||
<ReversibleStackPanel
|
||||
Grid.Column="2"
|
||||
Margin="10"
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
|
||||
<Button
|
||||
Classes="accent"
|
||||
Content="{ext:Locale SettingsButtonOk}"
|
||||
Command="{Binding OkButton}" />
|
||||
<Button
|
||||
HotKey="Escape"
|
||||
Content="{ext:Locale SettingsButtonCancel}"
|
||||
Command="{Binding CancelButton}" />
|
||||
<Button
|
||||
Content="{ext:Locale SettingsButtonApply}"
|
||||
Command="{Binding ApplyButton}" />
|
||||
</ReversibleStackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableAppWindow>
|
||||
|
@ -7,6 +7,8 @@ using LibHac.Ns;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
@ -21,11 +23,47 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
public bool Favorite { get; set; }
|
||||
public byte[] Icon { get; set; }
|
||||
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 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 string LocalizedStatusTooltip =>
|
||||
PlayabilityStatus.HasValue
|
||||
#pragma warning disable CS8509 // It is exhaustive for any value this property can contain.
|
||||
? LocaleManager.Instance[PlayabilityStatus!.Value switch
|
||||
#pragma warning restore CS8509
|
||||
{
|
||||
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
|
||||
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
|
||||
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
|
||||
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
|
||||
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
|
||||
}]
|
||||
: string.Empty;
|
||||
public int PlayerCount { get; set; }
|
||||
public int GameCount { get; set; }
|
||||
|
||||
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
||||
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
|
@ -128,13 +128,50 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
|
||||
if (appData.HasValue)
|
||||
return appData.Value.Name;
|
||||
|
||||
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
|
||||
return Path.GetFileNameWithoutExtension(dlcData.FileName);
|
||||
|
||||
return id.ToString("X16");
|
||||
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
|
||||
return id.ToString("X16");
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
|
||||
int idx = name.IndexOf('[');
|
||||
if (idx != -1)
|
||||
name = name[..idx];
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public bool FindApplication(ulong id, out ApplicationData foundData)
|
||||
{
|
||||
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
|
||||
foundData = appData.HasValue ? appData.Value : null;
|
||||
|
||||
return appData.HasValue;
|
||||
}
|
||||
|
||||
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
|
||||
{
|
||||
Gommon.Optional<TitleUpdateModel> appData =
|
||||
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
|
||||
foundData = appData.HasValue ? appData.Value : null;
|
||||
|
||||
return appData.HasValue;
|
||||
}
|
||||
|
||||
public TitleUpdateModel[] FindUpdatesFor(ulong id)
|
||||
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
|
||||
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public DownloadableContentModel[] FindDlcsFor(ulong id)
|
||||
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
|
||||
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public bool HasDlcs(ulong id)
|
||||
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
|
||||
|
||||
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
|
||||
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
|
||||
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception>
|
||||
|
@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
||||
}
|
||||
|
||||
public static void Unload()
|
||||
{
|
||||
_entries = null;
|
||||
}
|
||||
|
||||
private static CompatibilityEntry[] _entries;
|
||||
|
||||
public static CompatibilityEntry[] Entries
|
||||
@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
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
|
||||
@ -100,12 +100,25 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
public Optional<string> TitleId { get; }
|
||||
public string[] Labels { get; }
|
||||
public LocaleKeys? Status { get; }
|
||||
|
||||
public LocaleKeys? StatusDescription
|
||||
=> Status switch
|
||||
{
|
||||
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
|
||||
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
|
||||
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
|
||||
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
|
||||
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public DateTime LastUpdated { get; }
|
||||
|
||||
public string LocalizedLastUpdated =>
|
||||
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
|
||||
|
||||
|
||||
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
|
||||
public string LocalizedStatusDescription => LocaleManager.Instance[StatusDescription!.Value];
|
||||
public string FormattedTitleId => TitleId
|
||||
.OrElse(new string(' ', 16));
|
||||
|
||||
@ -113,20 +126,17 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
.Select(FormatLabelName)
|
||||
.JoinToString(", ");
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new("CompatibilityEntry: {");
|
||||
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
|
||||
sb.Append($"{nameof(TitleId)}={TitleId}, ");
|
||||
sb.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ");
|
||||
sb.Append($"{nameof(Status)}=\"{Status}\", ");
|
||||
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
|
||||
sb.Append('}');
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
public override string ToString() =>
|
||||
new StringBuilder("CompatibilityEntry: {")
|
||||
.Append($"{nameof(GameName)}=\"{GameName}\", ")
|
||||
.Append($"{nameof(TitleId)}={TitleId}, ")
|
||||
.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ")
|
||||
.Append($"{nameof(Status)}=\"{Status}\", ")
|
||||
.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"")
|
||||
.Append('}')
|
||||
.ToString();
|
||||
|
||||
public static string FormatLabelName(string labelName) => labelName.ToLower() switch
|
||||
{
|
||||
|
@ -34,7 +34,7 @@
|
||||
Text="{ext:Locale CompatibilityListWarning}" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
|
||||
<TextBox Name="SearchBox" Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
|
||||
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
|
||||
<TextBlock Grid.Column="2" Margin="-10, 0, 0, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
|
||||
</Grid>
|
||||
@ -64,6 +64,8 @@
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding LocalizedStatus}"
|
||||
Width="85"
|
||||
Background="Transparent"
|
||||
ToolTip.Tip="{Binding LocalizedStatusDescription}"
|
||||
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Column="3"
|
||||
|
@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public partial class CompatibilityList : UserControl
|
||||
{
|
||||
public static async Task Show()
|
||||
public static async Task Show(string titleId = null)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
@ -18,7 +18,10 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
Content = new CompatibilityList
|
||||
{
|
||||
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
|
||||
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary),
|
||||
SearchBox = {
|
||||
Text = titleId ?? ""
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -32,8 +35,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
|
||||
CompatibilityCsv.Unload();
|
||||
}
|
||||
|
||||
public CompatibilityList()
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Ryujinx.Ava.Utilities.Configuration.System;
|
||||
using Ryujinx.Ava.Utilities.Configuration.UI;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
@ -8,7 +7,6 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.Configuration
|
||||
{
|
||||
@ -17,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 60;
|
||||
public const int CurrentVersion = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
@ -143,6 +141,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
/// Change System Time Offset in seconds
|
||||
/// </summary>
|
||||
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>
|
||||
/// Enables or disables Docked Mode
|
||||
@ -376,24 +379,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
/// </summary>
|
||||
public KeyboardHotkeys Hotkeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy keyboard control bindings
|
||||
/// </summary>
|
||||
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
|
||||
/// TODO: Remove this when those older versions aren't in use anymore.
|
||||
public List<JsonObject> KeyboardConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy controller control bindings
|
||||
/// </summary>
|
||||
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
|
||||
/// TODO: Remove this when those older versions aren't in use anymore.
|
||||
public List<JsonObject> ControllerConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Input configurations
|
||||
/// </summary>
|
||||
public List<InputConfig> InputConfig { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Graphics backend
|
||||
|
@ -1,3 +1,4 @@
|
||||
using Avalonia.Media;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Utilities.Configuration.System;
|
||||
using Ryujinx.Ava.Utilities.Configuration.UI;
|
||||
@ -263,15 +264,12 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
}),
|
||||
(30, static cff =>
|
||||
{
|
||||
foreach (InputConfig config in cff.InputConfig)
|
||||
foreach (StandardControllerInputConfig config in cff.InputConfig.OfType<StandardControllerInputConfig>())
|
||||
{
|
||||
if (config is StandardControllerInputConfig controllerConfig)
|
||||
config.Rumble = new RumbleConfigController
|
||||
{
|
||||
controllerConfig.Rumble = new RumbleConfigController
|
||||
{
|
||||
EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f,
|
||||
};
|
||||
}
|
||||
EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f,
|
||||
};
|
||||
}
|
||||
}),
|
||||
(31, static cff => cff.BackendThreading = BackendThreading.Auto),
|
||||
@ -416,7 +414,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
// so as a compromise users who want to use it will simply need to re-enable it once after updating.
|
||||
cff.IgnoreApplet = false;
|
||||
}),
|
||||
(60, static cff => cff.StartNoUI = false)
|
||||
(60, static cff => cff.StartNoUI = false),
|
||||
(61, static cff => cff.MatchSystemTime = false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -311,6 +311,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
/// System Time Offset in Seconds
|
||||
/// </summary>
|
||||
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>
|
||||
/// Enables or disables Docked Mode
|
||||
@ -387,6 +392,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
|
||||
TimeZone.LogChangesToValue(nameof(TimeZone));
|
||||
SystemTimeOffset = new ReactiveObject<long>();
|
||||
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
|
||||
MatchSystemTime = new ReactiveObject<bool>();
|
||||
MatchSystemTime.LogChangesToValue(nameof(MatchSystemTime));
|
||||
EnableDockedMode = new ReactiveObject<bool>();
|
||||
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
|
||||
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.DoubleValue 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.StringValue 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
|
||||
};
|
||||
}
|
||||
}
|
282
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
282
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
@ -0,0 +1,282 @@
|
||||
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 Format(
|
||||
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; }
|
||||
|
||||
/// <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 delegate factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="PlayReportValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
|
||||
#region AsX accessors
|
||||
|
||||
public bool BooleanValue => PackedValue.AsBoolean();
|
||||
public byte ByteValye => PackedValue.AsByte();
|
||||
public sbyte SByteValye => PackedValue.AsSByte();
|
||||
public short ShortValye => PackedValue.AsInt16();
|
||||
public ushort UShortValye => PackedValue.AsUInt16();
|
||||
public int IntValye => PackedValue.AsInt32();
|
||||
public uint UIntValye => PackedValue.AsUInt32();
|
||||
public long LongValye => PackedValue.AsInt64();
|
||||
public ulong ULongValye => PackedValue.AsUInt64();
|
||||
public float FloatValue => PackedValue.AsSingle();
|
||||
public double DoubleValue => PackedValue.AsDouble();
|
||||
public string StringValue => PackedValue.AsString();
|
||||
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user