Compare commits

..

2 Commits

Author SHA1 Message Date
Frog Business
da137542b6 Merge 02026c3d38 into 6d75410bd2 2025-01-30 22:52:37 -08:00
Barış Hamil
02026c3d38 Ability to assign hotkeys to cycle controllers for players 2025-01-29 21:49:33 +03:00
39 changed files with 446 additions and 580 deletions

View File

@@ -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;deadlock,ingame,2025-02-02 16:57:53
01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN,playable,2023-12-04 18:33:37
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
1 title_id game_name labels status last_updated
2483 0100A5200C2E0000 Safety First! playable 2021-01-06 09:05:23
2484 0100A51013530000 SaGa Frontier Remastered nvdec playable 2022-11-03 13:54:56
2485 010003A00D0B4000 SaGa SCARLET GRACE: AMBITIONS™ playable 2022-10-06 13:20:31
2486 01008D100D43E000 Saints Row IV®: Re-Elected™ ldn-untested;LAN;deadlock ldn-untested;LAN ingame playable 2025-02-02 16:57:53 2023-12-04 18:33:37
2487 0100DE600BEEE000 SAINTS ROW®: THE THIRD™ - THE FULL PACKAGE slow;LAN playable 2023-08-24 02:40:58
2488 01007F000EB36000 Sakai and... nvdec playable 2022-12-15 13:53:19
2489 0100B1400E8FE000 Sakuna: Of Rice and Ruin playable 2023-07-24 13:47:13

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardHotkeys
@@ -13,5 +15,6 @@ namespace Ryujinx.Common.Configuration.Hid
public Key VolumeDown { get; set; }
public Key CustomVSyncIntervalIncrement { get; set; }
public Key CustomVSyncIntervalDecrement { get; set; }
public List<Key> CycleControllers { get; set; }
}
}

View File

@@ -1,118 +0,0 @@
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
}
}

View File

@@ -10,18 +10,14 @@ 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 && IsX64;
public static bool IsArmMac => IsMacOS && IsArm;
public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64;
public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64;
public static bool IsX64Windows => IsWindows && IsX64;
public static bool IsArmWindows => IsWindows && IsArm;
public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
public static bool IsX64Linux => IsLinux && IsX64;
public static bool IsArmLinux => IsLinux && IsArmMac;
public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
}
}

View File

@@ -47,8 +47,7 @@ namespace Ryujinx.Common
"01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker
"01009510001ca000", // Fast RMX
"01005CA01580E000", // Persona 5 Royal
"0100b880154fc000", // Persona 5 The Royal (Japan)
"01005CA01580E000", // Persona 5 Royale
"010015100b514000", // Super Mario Bros. Wonder
"0100000000010000", // Super Mario Odyssey

View File

@@ -87,7 +87,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
}
using ManualResetEvent waitEvent = new(false);
SyncpointWaiterHandle info = _syncpoints[id].RegisterCallback(threshold, _ => waitEvent.Set());
SyncpointWaiterHandle info = _syncpoints[id].RegisterCallback(threshold, (x) => waitEvent.Set());
if (info == null)
{
@@ -96,7 +96,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
bool signaled = waitEvent.WaitOne(timeout);
if (!signaled)
if (!signaled && info != null)
{
Logger.Error?.Print(LogClass.Gpu, $"Wait on syncpoint {id} for threshold {threshold} took more than {timeout.TotalMilliseconds}ms, resuming execution...");

View File

@@ -16,8 +16,14 @@ namespace Ryujinx.Graphics.Vulkan
Unknown,
}
static class VendorUtils
static partial 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

View File

@@ -1,6 +1,5 @@
using Gommon;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
@@ -376,11 +375,11 @@ namespace Ryujinx.Graphics.Vulkan
GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}";
IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && Patterns.AmdGcn.IsMatch(GpuRenderer);
IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer);
if (Vendor == Vendor.Nvidia)
{
Match match = Patterns.NvidiaConsumerClass.Match(GpuRenderer);
Match match = VendorUtils.NvidiaConsumerClassRegex().Match(GpuRenderer);
if (match != null && int.TryParse(match.Groups[2].Value, out int gpuNumber))
{

View File

@@ -5,7 +5,6 @@ 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;
@@ -31,6 +30,9 @@ 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;
@@ -105,7 +107,7 @@ namespace Ryujinx.HLE.HOS.Applets.Error
private static string CleanText(string value)
{
return Patterns.CleanText.Replace(value, string.Empty).Replace("\0", string.Empty);
return CleanTextRegex().Replace(value, string.Empty).Replace("\0", string.Empty);
}
private string GetMessageText(uint module, uint description, string key)

View File

@@ -0,0 +1,17 @@
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();
}
}

View File

@@ -1,10 +0,0 @@
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());
}
}

View File

@@ -0,0 +1,17 @@
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();
}
}

View File

@@ -1,13 +1,37 @@
using Ryujinx.Common.Helper;
using System.Text.RegularExpressions;
namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Proxy
{
static class DnsBlacklist
static partial 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 Patterns.BlockedHosts)
foreach (Regex regex in _blockedHosts)
{
if (regex.IsMatch(host))
{

View File

@@ -2,7 +2,6 @@ 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;
@@ -30,6 +29,13 @@ 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();
@@ -84,7 +90,7 @@ namespace Ryujinx.HLE.Loaders.Executables
if (string.IsNullOrEmpty(modulePath))
{
Match moduleMatch = Patterns.Module.Match(rawTextBuffer);
Match moduleMatch = ModuleRegex().Match(rawTextBuffer);
if (moduleMatch.Success)
{
modulePath = moduleMatch.Value;
@@ -93,13 +99,13 @@ namespace Ryujinx.HLE.Loaders.Executables
stringBuilder.AppendLine($" Module: {modulePath}");
Match fsSdkMatch = Patterns.FsSdk.Match(rawTextBuffer);
Match fsSdkMatch = FsSdkRegex().Match(rawTextBuffer);
if (fsSdkMatch.Success)
{
stringBuilder.AppendLine($" FS SDK Version: {fsSdkMatch.Value.Replace("sdk_version: ", string.Empty)}");
}
MatchCollection sdkMwMatches = Patterns.SdkMw.Matches(rawTextBuffer);
MatchCollection sdkMwMatches = SdkMwRegex().Matches(rawTextBuffer);
if (sdkMwMatches.Count != 0)
{
string libHeader = " SDK Libraries: ";

View File

@@ -1,4 +1,3 @@
using MsgPack;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
@@ -7,10 +6,6 @@ namespace Ryujinx.Horizon
{
public static class HorizonStatic
{
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
public static event Action<MessagePackObject> PlayReportPrinted;
[ThreadStatic]
private static HorizonOptions _options;

View File

@@ -230,8 +230,6 @@ 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());

View File

@@ -40,6 +40,7 @@ using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
@@ -49,6 +50,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -938,9 +940,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.EnableInternetAccess,
ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
ConfigurationState.Instance.System.FsGlobalAccessLogMode,
ConfigurationState.Instance.System.MatchSystemTime
? 0
: ConfigurationState.Instance.System.SystemTimeOffset,
ConfigurationState.Instance.System.SystemTimeOffset,
ConfigurationState.Instance.System.TimeZone,
ConfigurationState.Instance.System.MemoryManagerMode,
ConfigurationState.Instance.System.IgnoreMissingServices,
@@ -1308,6 +1308,18 @@ namespace Ryujinx.Ava
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.CycleControllersPlayer1:
case KeyboardHotkeyState.CycleControllersPlayer2:
case KeyboardHotkeyState.CycleControllersPlayer3:
case KeyboardHotkeyState.CycleControllersPlayer4:
case KeyboardHotkeyState.CycleControllersPlayer5:
case KeyboardHotkeyState.CycleControllersPlayer6:
case KeyboardHotkeyState.CycleControllersPlayer7:
case KeyboardHotkeyState.CycleControllersPlayer8:
var player = currentHotkeyState - KeyboardHotkeyState.CycleControllersPlayer1;
var ivm = new UI.ViewModels.Input.InputViewModel();
Dispatcher.UIThread.Invoke(() => ivm.CyclePlayerDevice(player));
break;
case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear();
break;
@@ -1390,6 +1402,15 @@ namespace Ryujinx.Ava
state = KeyboardHotkeyState.CustomVSyncIntervalDecrement;
}
foreach (var cycle in ConfigurationState.Instance.Hid.Hotkeys.Value.CycleControllers?.Select((value, index) => (value, index)) ?? [])
{
if (_keyboardInterface.IsPressed((Key)cycle.value))
{
state = KeyboardHotkeyState.CycleControllersPlayer1 + cycle.index;
break;
}
}
return state;
}
}

View File

@@ -4153,23 +4153,23 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Match System Time",
"en_US": "Resync to PC Date & Time",
"es_ES": "",
"fr_FR": "",
"fr_FR": "Resynchronier la Date à celle du PC",
"he_IL": "",
"it_IT": "",
"it_IT": "Sincronizza data e ora con il PC",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"ko_KR": "PC 날짜와 시간에 동기화",
"no_NO": "Resynkroniser til PC-dato og -klokkeslett",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"ru_RU": "Повторная синхронизация с датой и временем на компьютере",
"sv_SE": "Återsynka till datorns datum och tid",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
"uk_UA": "Синхронізувати з датою та часом ПК",
"zh_CN": "与 PC 日期和时间重新同步",
"zh_TW": "重新同步至 PC 的日期和時間"
}
},
{
@@ -5347,6 +5347,31 @@
"zh_TW": "啟用警告日誌"
}
},
{
"ID": "SettingsTabHotkeysCycleControllers",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Cycle Controllers",
"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": "SettingsTabLoggingEnableErrorLogs",
"Translations": {
@@ -15553,23 +15578,23 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Sync System Time to match your PC's current date & time.",
"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.",
"es_ES": "",
"fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.",
"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.",
"he_IL": "",
"it_IT": "Sincronizza data e ora del sistema con quelle del PC.",
"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.",
"ja_JP": "",
"ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.",
"no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.",
"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.",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.",
"sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.",
"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.",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.",
"zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。",
"zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。"
"uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.",
"zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间时间仍然可能不同步在这种情况下只需再次单击此按钮即可。",
"zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。\n\n這不是一個主動設定它仍然可能會失去同步在這種情況下只需再次點擊此按鈕。"
}
},
{

View File

@@ -14,5 +14,13 @@ namespace Ryujinx.Ava.Common
VolumeDown,
CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement,
CycleControllersPlayer1,
CycleControllersPlayer2,
CycleControllersPlayer3,
CycleControllersPlayer4,
CycleControllersPlayer5,
CycleControllersPlayer6,
CycleControllersPlayer7,
CycleControllersPlayer8
}
}

View File

@@ -1,19 +1,11 @@
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
@@ -24,12 +16,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";
@@ -38,8 +30,6 @@ namespace Ryujinx.Ava
private static DiscordRpcClient _discordClient;
private static RichPresence _discordPresenceMain;
private static RichPresence _discordPresencePlaying;
private static ApplicationMetadata _currentApp;
public static void Initialize()
{
@@ -47,7 +37,8 @@ namespace Ryujinx.Ava
{
Assets = new Assets
{
LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description)
LargeImageKey = "ryujinx",
LargeImageText = TruncateToByteLength(_description)
},
Details = "Main Menu",
State = "Idling",
@@ -56,7 +47,6 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
HorizonStatic.PlayReportPrinted += HandlePlayReport;
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@@ -87,15 +77,16 @@ 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 RichPresence CreatePlayingState(ApplicationMetadata appMeta, ProcessResult procRes) =>
new()
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
{
_discordClient?.SetPresence(new RichPresence
{
Assets = new Assets
{
@@ -109,48 +100,10 @@ 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 UpdatePlayingState()
{
_discordClient?.SetPresence(_discordPresencePlaying);
}
private static void SwitchToMainState()
{
_discordClient?.SetPresence(_discordPresenceMain);
_discordPresencePlaying = null;
_currentApp = null;
}
private static void HandlePlayReport(MessagePackObject playReport)
{
if (_discordClient is null) return;
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!value.Handled) return;
if (value.Reset)
{
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
}
else
{
_discordPresencePlaying.Details = value.FormattedString;
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
}
UpdatePlayingState();
}
private static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain);
private static string TruncateToByteLength(string input)
{

View File

@@ -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(CharacterValidation.IsNumeric);
_checkInput = text => text.All(NumericCharacterValidation.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 => !CharacterValidation.IsCJK(value));
_checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value));
break;
case KeyboardMode.ASCII:
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII);

View File

@@ -159,7 +159,6 @@ namespace Ryujinx.Ava.UI.Helpers
Symbol = (Symbol)symbol,
Margin = new Thickness(10),
FontSize = 40,
FlowDirection = FlowDirection.LeftToRight,
VerticalAlignment = VerticalAlignment.Center,
};

View File

@@ -1,6 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Ryujinx.Ava.UI.Models.Input
{
@@ -28,8 +33,15 @@ namespace Ryujinx.Ava.UI.Models.Input
[ObservableProperty] private Key _customVSyncIntervalDecrement;
public ObservableCollection<CycleController> CycleControllers { get; set; } = new ObservableCollection<CycleController>();
public ICommand AddCycleController { get; set; }
public ICommand RemoveCycleController { get; set; }
public bool CanRemoveCycleController => CycleControllers.Count > 0 && CycleControllers.Count < 8;
public HotkeyConfig(KeyboardHotkeys config)
{
AddCycleController = MiniCommand.Create(() => CycleControllers.Add(new CycleController(CycleControllers.Count + 1, Key.Unbound)));
RemoveCycleController = MiniCommand.Create(() => CycleControllers.Remove(CycleControllers.Last()));
if (config == null)
return;
@@ -44,6 +56,7 @@ namespace Ryujinx.Ava.UI.Models.Input
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
CycleControllers.AddRange((config.CycleControllers ?? []).Select((x, i) => new CycleController(i + 1, x)));
}
public KeyboardHotkeys GetConfig() =>
@@ -60,6 +73,7 @@ namespace Ryujinx.Ava.UI.Models.Input
VolumeDown = VolumeDown,
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
CycleControllers = CycleControllers.Select(x => x.Hotkey).ToList()
};
}
}

View File

@@ -1,4 +1,3 @@
using Ryujinx.Common.Helper;
using SharpMetal.QuartzCore;
using System;
@@ -8,12 +7,14 @@ namespace Ryujinx.Ava.UI.Renderer
{
public CAMetalLayer CreateSurface()
{
if (OperatingSystem.IsMacOS() && RunningPlatform.IsArm)
if (OperatingSystem.IsMacOS())
{
return new CAMetalLayer(MetalLayer);
}
throw new NotSupportedException($"Cannot create a {nameof(CAMetalLayer)} without being on ARM Mac.");
else
{
throw new NotSupportedException();
}
}
}
}

View File

@@ -43,19 +43,19 @@ namespace Ryujinx.Ava.UI.Renderer
public RendererHost(string titleId)
{
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(),
};
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;
}
string backendText = EmbeddedWindow switch
{
EmbeddedWindowVulkan => "Vulkan",

View File

@@ -0,0 +1,48 @@
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.ViewModels
{
public class CycleController : BaseModel
{
private string _player;
private Key _hotkey;
public string Player
{
get => _player;
set
{
_player = value;
OnPropertyChanged(nameof(Player));
}
}
public Key Hotkey
{
get => _hotkey;
set
{
_hotkey = value;
OnPropertyChanged(nameof(Hotkey));
}
}
public CycleController(int v, Key x)
{
Player = v switch
{
1 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer1],
2 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer2],
3 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer3],
4 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer4],
5 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer5],
6 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer6],
7 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer7],
8 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer8],
_ => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer] + " " + v
};
Hotkey = x;
}
}
}

View File

@@ -897,5 +897,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
AvaloniaKeyboardDriver.Dispose();
}
public void CyclePlayerDevice(int player)
{
LoadDevices();
PlayerId = (PlayerIndex)player;
Device = (Device + 1) % Devices.Count;
Save();
}
}
}

View File

@@ -16,7 +16,6 @@ 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;
@@ -116,6 +115,10 @@ 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; }
@@ -197,7 +200,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTextureRecompression { get; set; }
public bool EnableMacroHLE { get; set; }
public bool EnableColorSpacePassthrough { get; set; }
public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS;
public bool ColorSpacePassthroughAvailable => IsMacOS;
public bool EnableFileLog { get; set; }
public bool EnableStub { get; set; }
public bool EnableInfo { get; set; }
@@ -293,8 +296,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
[ObservableProperty] private bool _matchSystemTime;
public DateTimeOffset CurrentDate { get; set; }
public TimeSpan CurrentTime { get; set; }
@@ -329,6 +330,9 @@ 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()
@@ -410,6 +414,17 @@ 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();
@@ -455,7 +470,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool ValidateLdnPassphrase(string passphrase)
{
return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && Patterns.LdnPassphrase.IsMatch(passphrase));
return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase));
}
public void ValidateAndSetTimeZone(string location)
@@ -511,9 +526,7 @@ namespace Ryujinx.Ava.UI.ViewModels
CurrentDate = currentDateTime.Date;
CurrentTime = currentDateTime.TimeOfDay;
MatchSystemTime = config.System.MatchSystemTime;
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval;
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value;
CustomVSyncInterval = config.Graphics.CustomVSyncInterval;
VSyncMode = config.Graphics.VSyncMode;
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
@@ -618,7 +631,6 @@ 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;

View File

@@ -6,7 +6,6 @@
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>
@@ -70,7 +69,7 @@
</ComboBox>
</StackPanel>
<CheckBox IsChecked="{Binding UseHypervisor}"
IsVisible="{x:Static helper:RunningPlatform.IsArmMac}"
IsVisible="{Binding IsAppleSiliconMac}"
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemUseHypervisor}"
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}" />

View File

@@ -8,7 +8,6 @@
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">
@@ -49,7 +48,7 @@
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
<TextBlock Text="OpenGL" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{x:Static helper:RunningPlatform.IsArmMac}">
<ComboBoxItem IsEnabled="{Binding IsAppleSiliconMac}">
<TextBlock Text="Metal (ARM Mac only, Experimental)" />
</ComboBoxItem>
</ComboBox>

View File

@@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -15,17 +15,18 @@
<viewModels:SettingsViewModel />
</Design.DataContext>
<UserControl.Styles>
<Style Selector="StackPanel > StackPanel">
<Style Selector="StackPanel StackPanel">
<Setter Property="Margin" Value="10, 0, 0, 0" />
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style Selector="StackPanel > StackPanel > TextBlock">
<Style Selector="StackPanel StackPanel > TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="230" />
</Style>
<Style Selector="ToggleButton">
<Style Selector="ToggleButton, Button">
<Setter Property="Width" Value="90" />
<Setter Property="Height" Value="27" />
<Setter Property="Padding" Value="0,5,0,5" /> <!-- Added vertical padding -->
</Style>
<Style Selector="ToggleButton > TextBlock">
<Setter Property="TextAlignment" Value="Center" />
@@ -39,79 +40,123 @@
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Name="SettingButtons"
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
Spacing="10"
Name="SettingButtons">
<TextBlock
Classes="h1"
Text="{ext:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" />
<ToggleButton Name="ToggleVSyncMode">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
<StackPanel
Margin="10,0,0,0"
Spacing="10"
Orientation="Vertical">
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" />
<ToggleButton Name="ToggleVSyncMode">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
<Separator Height="1" />
<StackPanel Margin="0">
<TextBlock
Classes="h1"
Text="{ext:Locale SettingsTabHotkeysCycleControllers}" />
<StackPanel Orientation="Horizontal" Spacing="10">
<Button
Content="{ext:Locale SettingsTabGeneralAdd}"
Margin="10,0,0,0"
Command="{Binding KeyboardHotkey.AddCycleController}" />
<Button
Content="{ext:Locale SettingsTabGeneralRemove}"
IsEnabled="{Binding KeyboardHotkey.CanRemoveCycleController}"
Command="{Binding KeyboardHotkey.RemoveCycleController}" />
</StackPanel>
</StackPanel>
<ItemsControl ItemsSource="{Binding KeyboardHotkey.CycleControllers}"
Name="CycleControllers">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Margin="10,0,0,0"
Orientation="Vertical"
Spacing="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Player}" />
<ToggleButton>
<TextBlock
Text="{Binding Hotkey, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</ScrollViewer>

View File

@@ -3,11 +3,14 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using DynamicData;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System.Linq;
using Button = Ryujinx.Input.Button;
using Key = Ryujinx.Common.Configuration.Hid.Key;
@@ -21,16 +24,21 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsHotkeysView()
{
InitializeComponent();
RegisterEvents();
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
CycleControllers.LayoutUpdated += (_, _1) => RegisterEvents();
}
private void RegisterEvents()
{
foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
{
if (visual is ToggleButton button and not CheckBox)
{
button.IsCheckedChanged -= Button_IsCheckedChanged;
button.IsCheckedChanged += Button_IsCheckedChanged;
}
}
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
@@ -116,6 +124,13 @@ namespace Ryujinx.Ava.UI.Views.Settings
case "CustomVSyncIntervalDecrement":
viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType<Key>();
break;
default:
var index = button.FindAncestorOfType<ItemsControl>().GetLogicalDescendants().OfType<ToggleButton>().IndexOf(button);
if (index >= 0 && viewModel.KeyboardHotkey.CycleControllers != null)
{
viewModel.KeyboardHotkey.CycleControllers[index].Hotkey = buttonValue.AsHidType<Key>();
}
break;
}
}
};

View File

@@ -170,8 +170,7 @@
ToolTip.Tip="{ext:Locale TimeTooltip}"
Width="250"/>
<DatePicker
VerticalAlignment="Center"
IsEnabled="{Binding !MatchSystemTime}"
VerticalAlignment="Center"
SelectedDate="{Binding CurrentDate}"
ToolTip.Tip="{ext:Locale TimeTooltip}"
Width="350" />
@@ -182,21 +181,17 @@
<TimePicker
VerticalAlignment="Center"
ClockIdentifier="24HourClock"
IsEnabled="{Binding !MatchSystemTime}"
SelectedTime="{Binding CurrentTime}"
Width="350"
ToolTip.Tip="{ext:Locale TimeTooltip}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
<Button
Margin="10, 0, 0, 0"
VerticalAlignment="Center"
Text="{ext:Locale SettingsTabSystemSystemTimeMatch}"
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"
Width="250"/>
<CheckBox
VerticalAlignment="Center"
IsChecked="{Binding MatchSystemTime}"
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"/>
Click="MatchSystemTime_OnClick"
Background="{DynamicResource SystemAccentColor}"
ToolTip.Tip="{ext:Locale MatchTimeTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" />
</Button>
</StackPanel>
<Separator />
<StackPanel Margin="0,10,0,10"

View File

@@ -34,5 +34,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
}
}
private void MatchSystemTime_OnClick(object sender, RoutedEventArgs e) => ViewModel.MatchSystemTime();
}
}

View File

@@ -10,7 +10,6 @@
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"
@@ -114,7 +113,7 @@
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
ReverseOrder="{Binding IsMacOS}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonOk}"

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 63;
public const int CurrentVersion = 62;
/// <summary>
/// Version of the configuration file format
@@ -141,11 +141,6 @@ 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

View File

@@ -429,8 +429,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
};
}
}),
(62, static cff => cff.RainbowSpeed = 1f),
(63, static cff => cff.MatchSystemTime = false)
(62, static cff => cff.RainbowSpeed = 1f)
);
}
}

View File

@@ -312,11 +312,6 @@ 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
@@ -393,8 +388,6 @@ 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>();

View File

@@ -1,198 +0,0 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities
{
public static class PlayReport
{
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
.AddSpec(
"01007ef00011e000",
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
)
.AddSpec( // Super Mario Odyssey
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec( // Super Mario Odyssey (China)
"010075000ECBE000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec( // Super Mario 3D World + Bowser's Fury
"010028600EBDA000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China)
["0100152000022000", "010075100E8EC000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value)
=> value.BoxedValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => PlayReportFormattedValue.ForceReset
};
}
#region Analyzer implementation
public class PlayReportAnalyzer
{
private readonly List<PlayReportGameSpec> _specs = [];
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
return this;
}
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
{
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
return this;
}
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
return this;
}
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
{
_specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
return this;
}
public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
{
if (!playReport.IsDictionary)
return PlayReportFormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
return PlayReportFormattedValue.Unhandled;
foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
{
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue;
PlayReportValue value = new()
{
Application = appMeta,
BoxedValue = valuePackObject.ToObject()
};
return formatSpec.ValueFormatter(ref value);
}
return PlayReportFormattedValue.Unhandled;
}
}
public class PlayReportGameSpec
{
public required string[] TitleIds { get; init; }
public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
{
Analyses.Add(new PlayReportValueFormatterSpec
{
Priority = Analyses.Count,
ReportKey = reportKey,
ValueFormatter = valueFormatter
});
return this;
}
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter)
{
Analyses.Add(new PlayReportValueFormatterSpec
{
Priority = priority,
ReportKey = reportKey,
ValueFormatter = valueFormatter
});
return this;
}
}
public struct PlayReportValue
{
public ApplicationMetadata Application { get; init; }
public object BoxedValue { get; init; }
}
public struct PlayReportFormattedValue
{
public bool Handled { get; private init; }
public bool Reset { get; private init; }
public string FormattedString { get; private init; }
public static implicit operator PlayReportFormattedValue(string formattedValue)
=> new() { Handled = true, FormattedString = formattedValue };
public static PlayReportFormattedValue Unhandled => default;
public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true };
public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl;
private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset;
}
public struct PlayReportValueFormatterSpec
{
public required int Priority { get; init; }
public required string ReportKey { get; init; }
public PlayReportValueFormatter ValueFormatter { get; init; }
}
public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
#endregion
}