Merge branch 'master' into Alpha

This commit is contained in:
Vova 2025-02-12 18:08:54 +10:00
commit 62f7974f14
64 changed files with 1069 additions and 433 deletions

View File

@ -2063,7 +2063,7 @@
010002700C34C000,"Numbala",,playable,2020-05-11 12:01:07 010002700C34C000,"Numbala",,playable,2020-05-11 12:01:07
010020500C8C8000,"Number Place 10000",gpu,menus,2021-11-24 09:14:23 010020500C8C8000,"Number Place 10000",gpu,menus,2021-11-24 09:14:23
010003701002C000,"Nurse Love Syndrome",,playable,2022-10-13 10:05:22 010003701002C000,"Nurse Love Syndrome",,playable,2022-10-13 10:05:22
0000000000000000,"nx-hbmenu",Needs Update;homebrew,boots,2024-04-06 22:05:32 ,"nx-hbmenu",Needs Update;homebrew,boots,2024-04-06 22:05:32
,"nxquake2",services;crash;homebrew,nothing,2022-08-04 23:14:04 ,"nxquake2",services;crash;homebrew,nothing,2022-08-04 23:14:04
010049F00EC30000,"Nyan Cat: Lost in Space",online,playable,2021-06-12 13:22:03 010049F00EC30000,"Nyan Cat: Lost in Space",online,playable,2021-06-12 13:22:03
01002E6014FC4000,"O---O",,playable,2022-10-29 12:12:14 01002E6014FC4000,"O---O",,playable,2022-10-29 12:12:14
@ -2471,7 +2471,7 @@
0100AFE00DDAC000,"Royal Roads",,playable,2020-11-17 12:54:38 0100AFE00DDAC000,"Royal Roads",,playable,2020-11-17 12:54:38
0100E2C00B414000,"RPG Maker MV",nvdec,playable,2021-01-05 20:12:01 0100E2C00B414000,"RPG Maker MV",nvdec,playable,2021-01-05 20:12:01
01005CD015986000,"rRootage Reloaded",,playable,2022-08-05 23:20:18 01005CD015986000,"rRootage Reloaded",,playable,2022-08-05 23:20:18
0000000000000000,"RSDKv5u",homebrew,ingame,2024-04-01 16:25:34 ,"RSDKv5u",homebrew,ingame,2024-04-01 16:25:34
010009B00D33C000,"Rugby Challenge 4",slow;online-broken;UE4,playable,2022-10-06 12:45:53 010009B00D33C000,"Rugby Challenge 4",slow;online-broken;UE4,playable,2022-10-06 12:45:53
01006EC00F2CC000,"RUINER",UE4,playable,2022-10-03 14:11:33 01006EC00F2CC000,"RUINER",UE4,playable,2022-10-03 14:11:33
010074F00DE4A000,"Run the Fan",,playable,2021-02-27 13:36:28 010074F00DE4A000,"Run the Fan",,playable,2021-02-27 13:36:28
@ -2480,6 +2480,7 @@
010081C0191D8000,"Rune Factory 3 Special",,playable,2023-10-15 08:32:49 010081C0191D8000,"Rune Factory 3 Special",,playable,2023-10-15 08:32:49
010051D00E3A4000,"Rune Factory 4 Special",32-bit;crash;nvdec,ingame,2023-05-06 08:49:17 010051D00E3A4000,"Rune Factory 4 Special",32-bit;crash;nvdec,ingame,2023-05-06 08:49:17
010014D01216E000,"Rune Factory 5 (JP)",gpu,ingame,2021-06-01 12:00:36 010014D01216E000,"Rune Factory 5 (JP)",gpu,ingame,2021-06-01 12:00:36
010071E0145F8000,"Rustler",,playable,2025-02-10 20:17:12
0100E21013908000,"RWBY: Grimm Eclipse - Definitive Edition",online-broken,playable,2022-11-03 10:44:01 0100E21013908000,"RWBY: Grimm Eclipse - Definitive Edition",online-broken,playable,2022-11-03 10:44:01
010012C0060F0000,"RXN -Raijin-",nvdec,playable,2021-01-10 16:05:43 010012C0060F0000,"RXN -Raijin-",nvdec,playable,2021-01-10 16:05:43
0100B8B012ECA000,"S.N.I.P.E.R. - Hunter Scope",,playable,2021-04-19 15:58:09 0100B8B012ECA000,"S.N.I.P.E.R. - Hunter Scope",,playable,2021-04-19 15:58:09
@ -2673,10 +2674,10 @@
01004F401BEBE000,"Song of Nunu: A League of Legends Story",,ingame,2024-07-12 18:53:44 01004F401BEBE000,"Song of Nunu: A League of Legends Story",,ingame,2024-07-12 18:53:44
0100E5400BF94000,"Songbird Symphony",,playable,2021-02-27 02:44:04 0100E5400BF94000,"Songbird Symphony",,playable,2021-02-27 02:44:04
010031D00A604000,"Songbringer",,playable,2020-06-22 10:42:02 010031D00A604000,"Songbringer",,playable,2020-06-22 10:42:02
0000000000000000,"Sonic 1 (2013)",crash;homebrew,ingame,2024-04-06 18:31:20 ,"Sonic 1 (2013)",crash;homebrew,ingame,2024-04-06 18:31:20
0000000000000000,"Sonic 2 (2013)",crash;homebrew,ingame,2024-04-01 16:25:30 ,"Sonic 2 (2013)",crash;homebrew,ingame,2024-04-01 16:25:30
0000000000000000,"Sonic A.I.R",homebrew,ingame,2024-04-01 16:25:32 ,"Sonic A.I.R",homebrew,ingame,2024-04-01 16:25:32
0000000000000000,"Sonic CD",crash;homebrew,ingame,2024-04-01 16:25:31 ,"Sonic CD",crash;homebrew,ingame,2024-04-01 16:25:31
010040E0116B8000,"Sonic Colors: Ultimate",,playable,2022-11-12 21:24:26 010040E0116B8000,"Sonic Colors: Ultimate",,playable,2022-11-12 21:24:26
01001270012B6000,"SONIC FORCES™",,playable,2024-07-28 13:11:21 01001270012B6000,"SONIC FORCES™",,playable,2024-07-28 13:11:21
01004AD014BF0000,"Sonic Frontiers",gpu;deadlock;amd-vendor-bug;intel-vendor-bug,ingame,2024-09-05 09:18:53 01004AD014BF0000,"Sonic Frontiers",gpu;deadlock;amd-vendor-bug;intel-vendor-bug,ingame,2024-09-05 09:18:53
@ -2693,7 +2694,7 @@
0100707011722000,"Space Elite Force",,playable,2020-11-27 15:21:05 0100707011722000,"Space Elite Force",,playable,2020-11-27 15:21:05
010047B010260000,"Space Pioneer",,playable,2022-10-20 12:24:37 010047B010260000,"Space Pioneer",,playable,2022-10-20 12:24:37
010010A009830000,"Space Ribbon",,playable,2022-08-15 17:17:10 010010A009830000,"Space Ribbon",,playable,2022-08-15 17:17:10
0000000000000000,"SpaceCadetPinball",homebrew,ingame,2024-04-18 19:30:04 ,"SpaceCadetPinball",homebrew,ingame,2024-04-18 19:30:04
0100D9B0041CE000,"Spacecats with Lasers",,playable,2022-08-15 17:22:44 0100D9B0041CE000,"Spacecats with Lasers",,playable,2022-08-15 17:22:44
010034800FB60000,"Spaceland",,playable,2020-11-01 14:31:56 010034800FB60000,"Spaceland",,playable,2020-11-01 14:31:56
010028D0045CE000,"Sparkle 2",,playable,2020-10-19 11:51:39 010028D0045CE000,"Sparkle 2",,playable,2020-10-19 11:51:39
@ -2838,7 +2839,7 @@
0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug,playable,2024-08-25 01:32:34 0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug,playable,2024-08-25 01:32:34
010036B0034E4000,"Super Mario Party™",gpu;Needs Update;ldn-works,ingame,2024-06-21 05:10:16 010036B0034E4000,"Super Mario Party™",gpu;Needs Update;ldn-works,ingame,2024-06-21 05:10:16
0100BC0018138000,"Super Mario RPG™",gpu;audio;nvdec,ingame,2024-06-19 17:43:42 0100BC0018138000,"Super Mario RPG™",gpu;audio;nvdec,ingame,2024-06-19 17:43:42
0000000000000000,"Super Mario World",homebrew,boots,2024-06-13 01:40:31 ,"Super Mario World",homebrew,boots,2024-06-13 01:40:31
010049900F546000,"Super Mario™ 3D All-Stars",services-horizon;slow;vulkan;amd-vendor-bug,ingame,2024-05-07 02:38:16 010049900F546000,"Super Mario™ 3D All-Stars",services-horizon;slow;vulkan;amd-vendor-bug,ingame,2024-05-07 02:38:16
010028600EBDA000,"Super Mario™ 3D World + Bowsers Fury",ldn-works,playable,2024-07-31 10:45:37 010028600EBDA000,"Super Mario™ 3D World + Bowsers Fury",ldn-works,playable,2024-07-31 10:45:37
01004F8006A78000,"Super Meat Boy",services,playable,2020-04-02 23:10:07 01004F8006A78000,"Super Meat Boy",services,playable,2020-04-02 23:10:07

1 title_id game_name labels status last_updated
2063 010002700C34C000 Numbala playable 2020-05-11 12:01:07
2064 010020500C8C8000 Number Place 10000 gpu menus 2021-11-24 09:14:23
2065 010003701002C000 Nurse Love Syndrome playable 2022-10-13 10:05:22
2066 0000000000000000 nx-hbmenu Needs Update;homebrew boots 2024-04-06 22:05:32
2067 nxquake2 services;crash;homebrew nothing 2022-08-04 23:14:04
2068 010049F00EC30000 Nyan Cat: Lost in Space online playable 2021-06-12 13:22:03
2069 01002E6014FC4000 O---O playable 2022-10-29 12:12:14
2471 0100AFE00DDAC000 Royal Roads playable 2020-11-17 12:54:38
2472 0100E2C00B414000 RPG Maker MV nvdec playable 2021-01-05 20:12:01
2473 01005CD015986000 rRootage Reloaded playable 2022-08-05 23:20:18
2474 0000000000000000 RSDKv5u homebrew ingame 2024-04-01 16:25:34
2475 010009B00D33C000 Rugby Challenge 4 slow;online-broken;UE4 playable 2022-10-06 12:45:53
2476 01006EC00F2CC000 RUINER UE4 playable 2022-10-03 14:11:33
2477 010074F00DE4A000 Run the Fan playable 2021-02-27 13:36:28
2480 010081C0191D8000 Rune Factory 3 Special playable 2023-10-15 08:32:49
2481 010051D00E3A4000 Rune Factory 4 Special 32-bit;crash;nvdec ingame 2023-05-06 08:49:17
2482 010014D01216E000 Rune Factory 5 (JP) gpu ingame 2021-06-01 12:00:36
2483 010071E0145F8000 Rustler playable 2025-02-10 20:17:12
2484 0100E21013908000 RWBY: Grimm Eclipse - Definitive Edition online-broken playable 2022-11-03 10:44:01
2485 010012C0060F0000 RXN -Raijin- nvdec playable 2021-01-10 16:05:43
2486 0100B8B012ECA000 S.N.I.P.E.R. - Hunter Scope playable 2021-04-19 15:58:09
2674 01004F401BEBE000 Song of Nunu: A League of Legends Story ingame 2024-07-12 18:53:44
2675 0100E5400BF94000 Songbird Symphony playable 2021-02-27 02:44:04
2676 010031D00A604000 Songbringer playable 2020-06-22 10:42:02
2677 0000000000000000 Sonic 1 (2013) crash;homebrew ingame 2024-04-06 18:31:20
2678 0000000000000000 Sonic 2 (2013) crash;homebrew ingame 2024-04-01 16:25:30
2679 0000000000000000 Sonic A.I.R homebrew ingame 2024-04-01 16:25:32
2680 0000000000000000 Sonic CD crash;homebrew ingame 2024-04-01 16:25:31
2681 010040E0116B8000 Sonic Colors: Ultimate playable 2022-11-12 21:24:26
2682 01001270012B6000 SONIC FORCES™ playable 2024-07-28 13:11:21
2683 01004AD014BF0000 Sonic Frontiers gpu;deadlock;amd-vendor-bug;intel-vendor-bug ingame 2024-09-05 09:18:53
2694 0100707011722000 Space Elite Force playable 2020-11-27 15:21:05
2695 010047B010260000 Space Pioneer playable 2022-10-20 12:24:37
2696 010010A009830000 Space Ribbon playable 2022-08-15 17:17:10
2697 0000000000000000 SpaceCadetPinball homebrew ingame 2024-04-18 19:30:04
2698 0100D9B0041CE000 Spacecats with Lasers playable 2022-08-15 17:22:44
2699 010034800FB60000 Spaceland playable 2020-11-01 14:31:56
2700 010028D0045CE000 Sparkle 2 playable 2020-10-19 11:51:39
2839 0100000000010000 Super Mario Odyssey™ nvdec;intel-vendor-bug;mac-bug playable 2024-08-25 01:32:34
2840 010036B0034E4000 Super Mario Party™ gpu;Needs Update;ldn-works ingame 2024-06-21 05:10:16
2841 0100BC0018138000 Super Mario RPG™ gpu;audio;nvdec ingame 2024-06-19 17:43:42
2842 0000000000000000 Super Mario World homebrew boots 2024-06-13 01:40:31
2843 010049900F546000 Super Mario™ 3D All-Stars services-horizon;slow;vulkan;amd-vendor-bug ingame 2024-05-07 02:38:16
2844 010028600EBDA000 Super Mario™ 3D World + Bowser’s Fury ldn-works playable 2024-07-31 10:45:37
2845 01004F8006A78000 Super Meat Boy services playable 2020-04-02 23:10:07

View File

@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
public const int DefaultGranularity = 65536; // Mapping granularity in Windows. public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
public IJitMemoryBlock Block { get; } public IJitMemoryBlock Block { get; }
public IJitMemoryAllocator Allocator { get; }
public nint Pointer => Block.Pointer; public nint Pointer => Block.Pointer;
@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity; granularity = DefaultGranularity;
} }
Allocator = allocator;
Block = allocator.Reserve(maxSize); Block = allocator.Reserve(maxSize);
_maxSize = maxSize; _maxSize = maxSize;
_sizeGranularity = granularity; _sizeGranularity = granularity;

View File

@ -2,6 +2,8 @@ using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Unwinding; using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Native; using ARMeilleure.Native;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -18,9 +20,8 @@ namespace ARMeilleure.Translation.Cache
private static readonly int _pageMask = _pageSize - 1; private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024; private const int CacheSize = 256 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator; private static JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator; private static CacheMemoryAllocator _cacheAllocator;
@ -30,6 +31,9 @@ namespace ARMeilleure.Translation.Cache
private static readonly Lock _lock = new(); private static readonly Lock _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize); public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
@ -48,7 +52,9 @@ namespace ARMeilleure.Translation.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, CacheSize); ReservedRegion firstRegion = new(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{ {
@ -59,7 +65,9 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize)); JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
} }
_initialized = true; _initialized = true;
@ -75,8 +83,8 @@ namespace ARMeilleure.Translation.Cache
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length); int funcOffset = Allocate(code.Length);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = _jitRegion.Pointer + funcOffset; nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -90,9 +98,9 @@ namespace ARMeilleure.Translation.Cache
} }
else else
{ {
ReprotectAsWritable(funcOffset, code.Length); ReprotectAsWritable(targetRegion, funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -116,52 +124,83 @@ namespace ARMeilleure.Translation.Cache
{ {
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); foreach (ReservedRegion region in _jitRegions)
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{ {
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); if (pointer.ToInt64() < region.Pointer.ToInt64() ||
_cacheEntries.RemoveAt(entryIndex); pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
} }
} }
} }
private static void ReprotectAsWritable(int offset, int size) private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static void ReprotectAsExecutable(int offset, int size) private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static int Allocate(int codeSize) private static int Allocate(int codeSize)
{ {
codeSize = AlignCodeSize(codeSize); codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize); for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
if (allocOffset < 0)
{ {
throw new OutOfMemoryException("JIT Cache exhausted."); int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
} }
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
return allocOffset; int newRegionNumber = _activeRegionIndex;
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
} }
private static int AlignCodeSize(int codeSize) private static int AlignCodeSize(int codeSize)
{ {
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1); return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
@ -185,18 +224,21 @@ namespace ARMeilleure.Translation.Cache
{ {
lock (_lock) lock (_lock)
{ {
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default)); foreach (ReservedRegion _ in _jitRegions)
if (index < 0)
{ {
index = ~index - 1; int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
}
if (index >= 0) if (index < 0)
{ {
entry = _cacheEntries[index]; index = ~index - 1;
entryIndex = index; }
return true;
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
} }
} }

View File

@ -144,17 +144,15 @@ namespace ARMeilleure.Translation.PTC
public List<ulong> GetBlacklistedFunctions() public List<ulong> GetBlacklistedFunctions()
{ {
List<ulong> funcs = new List<ulong>(); List<ulong> funcs = [];
foreach (var profiledFunc in ProfiledFuncs) foreach ((ulong ptr, FuncProfile funcProfile) in ProfiledFuncs)
{ {
if (profiledFunc.Value.Blacklist) if (!funcProfile.Blacklist)
{ continue;
if (!funcs.Contains(profiledFunc.Key))
{ if (!funcs.Contains(ptr))
funcs.Add(profiledFunc.Key); funcs.Add(ptr);
}
}
} }
return funcs; return funcs;

View File

@ -219,6 +219,7 @@ namespace Ryujinx.Common
//Misc Games //Misc Games
"010056e00853a000", // A Hat in Time "010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance "0100fd1014726000", // Baldurs Gate: Dark Alliance
"01008c2019598000", // Bluey: The Video Game
"0100c6800b934000", // Brawlhalla "0100c6800b934000", // Brawlhalla
"0100dbf01000a000", // Burnout Paradise Remastered "0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win "0100744001588000", // Cars 3: Driven to Win

View File

@ -1,4 +1,6 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -15,9 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly int _pageMask = _pageSize - 1; private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024; private const int CacheSize = 256 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator; private static JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator; private static CacheMemoryAllocator _cacheAllocator;
@ -26,6 +27,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly Lock _lock = new(); private static readonly Lock _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
@ -45,7 +48,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, CacheSize); ReservedRegion firstRegion = new(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{ {
@ -65,8 +70,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length); int funcOffset = Allocate(code.Length);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = _jitRegion.Pointer + funcOffset; nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -80,18 +85,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
} }
else else
{ {
ReprotectAsWritable(funcOffset, code.Length); ReprotectAsWritable(targetRegion, funcOffset, code.Length);
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) _jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
{
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
}
else
{
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
} }
Add(funcOffset, code.Length); Add(funcOffset, code.Length);
@ -106,50 +104,80 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); foreach (ReservedRegion region in _jitRegions)
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{ {
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); if (pointer.ToInt64() < region.Pointer.ToInt64() ||
_cacheEntries.RemoveAt(entryIndex); pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
} }
} }
} }
private static void ReprotectAsWritable(int offset, int size) private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static void ReprotectAsExecutable(int offset, int size) private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static int Allocate(int codeSize) private static int Allocate(int codeSize)
{ {
codeSize = AlignCodeSize(codeSize); codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize); for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
if (allocOffset < 0)
{ {
throw new OutOfMemoryException("JIT Cache exhausted."); int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
} }
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); int exhaustedRegion = _activeRegionIndex;
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
return allocOffset; int newRegionNumber = _activeRegionIndex;
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
} }
private static int AlignCodeSize(int codeSize) private static int AlignCodeSize(int codeSize)

View File

@ -12,7 +12,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int SharedCacheSize = 2047 * 1024 * 1024; private const int SharedCacheSize = 2047 * 1024 * 1024;
private const int LocalCacheSize = 128 * 1024 * 1024; private const int LocalCacheSize = 256 * 1024 * 1024;
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there // How many calls to the same function we allow until we pad the shared cache to force the function to become available there
// and allow the guest to take the fast path. // and allow the guest to take the fast path.

View File

@ -1,5 +1,4 @@
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System.Text.RegularExpressions;
namespace Ryujinx.Graphics.Vulkan namespace Ryujinx.Graphics.Vulkan
{ {

View File

@ -15,7 +15,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
namespace Ryujinx.HLE.HOS.Applets.Error namespace Ryujinx.HLE.HOS.Applets.Error
{ {
@ -159,13 +158,15 @@ namespace Ryujinx.HLE.HOS.Applets.Error
string[] buttons = GetButtonsText(module, description, "DlgBtn"); string[] buttons = GetButtonsText(module, description, "DlgBtn");
bool showDetails = _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons); (uint Module, uint Description) errorCodeTuple = (module, uint.Parse(description.ToString("0000")));
bool showDetails = _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons, errorCodeTuple);
if (showDetails) if (showDetails)
{ {
message = GetMessageText(module, description, "FlvMsg"); message = GetMessageText(module, description, "FlvMsg");
buttons = GetButtonsText(module, description, "FlvBtn"); buttons = GetButtonsText(module, description, "FlvBtn");
_horizon.Device.UIHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons); _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons, errorCodeTuple);
} }
} }

View File

@ -150,6 +150,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{ BsdSocketOption.SoLinger, SocketOptionName.Linger }, { BsdSocketOption.SoLinger, SocketOptionName.Linger },
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline }, { BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress }, { BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
{ BsdSocketOption.SoNoSigpipe, SocketOptionName.DontLinger },
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer }, { BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer }, { BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater }, { BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },

View File

@ -45,10 +45,12 @@ namespace Ryujinx.HLE.UI
/// <param name="value">The value associated to the <paramref name="kind"/>.</param> /// <param name="value">The value associated to the <paramref name="kind"/>.</param>
void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value); void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value);
/// <summary>
/// Displays a Message Dialog box specific to Error Applet and blocks until it is closed. /// Displays a Message Dialog box specific to Error Applet and blocks until it is closed.
/// </summary> /// </summary>
/// <returns>False when OK is pressed, True when another button (Details) is pressed.</returns> /// <returns>False when OK is pressed, True when another button (Details) is pressed.</returns>
bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText); // ReSharper disable once UnusedParameter.Global
bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText, (uint Module, uint Description)? errorCode = null);
/// <summary> /// <summary>
/// Creates a handler to process keyboard inputs into text strings. /// Creates a handler to process keyboard inputs into text strings.

View File

@ -1,4 +1,3 @@
using MsgPack;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Memory; using Ryujinx.Memory;

View File

@ -185,6 +185,15 @@ namespace Ryujinx.Input.HLE
} }
} }
public bool InputUpdatesBlocked
{
get
{
lock (_lock)
return _blockInputUpdates;
}
}
public void BlockInputUpdates() public void BlockInputUpdates()
{ {
lock (_lock) lock (_lock)

View File

@ -1048,6 +1048,7 @@ namespace Ryujinx.Ava
if (_viewModel.StartGamesInFullscreen) if (_viewModel.StartGamesInFullscreen)
{ {
_viewModel.WindowState = WindowState.FullScreen; _viewModel.WindowState = WindowState.FullScreen;
_viewModel.Window.TitleBar.ExtendsContentIntoTitleBar = true;
} }
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI) if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI)

View File

@ -3425,26 +3425,101 @@
{ {
"ID": "SettingsTabGeneralCheckUpdatesOnLaunch", "ID": "SettingsTabGeneralCheckUpdatesOnLaunch",
"Translations": { "Translations": {
"ar_SA": "التحقق من وجود تحديثات عند التشغيل", "ar_SA": "",
"de_DE": "Beim Start nach Updates suchen", "de_DE": "",
"el_GR": "Έλεγχος για Ενημερώσεις στην Εκκίνηση", "el_GR": "",
"en_US": "Check for Updates on Launch", "en_US": "Check for Updates:",
"es_ES": "Buscar actualizaciones al iniciar", "es_ES": "",
"fr_FR": "Vérifier les mises à jour au démarrage", "fr_FR": "",
"he_IL": "בדוק אם קיימים עדכונים בהפעלה", "he_IL": "",
"it_IT": "Controlla aggiornamenti all'avvio", "it_IT": "",
"ja_JP": "起動時にアップデートを確認する", "ja_JP": "",
"ko_KR": "시작 시, 업데이트 확인", "ko_KR": "",
"no_NO": "Se etter oppdateringer ved oppstart", "no_NO": "",
"pl_PL": "Sprawdzaj aktualizacje przy uruchomieniu", "pl_PL": "",
"pt_BR": "Verificar se há atualizações ao iniciar", "pt_BR": "",
"ru_RU": "Проверять наличие обновлений при запуске", "ru_RU": "",
"sv_SE": "Leta efter uppdatering vid uppstart", "sv_SE": "",
"th_TH": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม", "th_TH": "",
"tr_TR": "Her Açılışta Güncellemeleri Denetle", "tr_TR": "",
"uk_UA": "Перевіряти наявність оновлень під час запуску", "uk_UA": "",
"zh_CN": "启动时检查更新", "zh_CN": "",
"zh_TW": "啟動時檢查更新" "zh_TW": ""
}
},
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchOff",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Off",
"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": "SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prompt",
"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": "SettingsTabGeneralCheckUpdatesOnLaunchBackground",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Background",
"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": ""
} }
}, },
{ {
@ -3497,6 +3572,31 @@
"zh_TW": "記住視窗大小/位置" "zh_TW": "記住視窗大小/位置"
} }
}, },
{
"ID": "SettingsTabGeneralDisableInputWhenOutOfFocus",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Disable Input when Out of Focus",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "Deaktiver inndata når vinduet er ute av fokus",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "SettingsTabGeneralShowTitleBar", "ID": "SettingsTabGeneralShowTitleBar",
"Translations": { "Translations": {
@ -5822,6 +5922,31 @@
"zh_TW": "啟用客體日誌" "zh_TW": "啟用客體日誌"
} }
}, },
{
"ID": "SettingsTabLoggingEnableAvaloniaLogs",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Enable UI Logs",
"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": "SettingsTabLoggingEnableFsAccessLogs", "ID": "SettingsTabLoggingEnableFsAccessLogs",
"Translations": { "Translations": {
@ -12472,6 +12597,31 @@
"zh_TW": "正在下載更新..." "zh_TW": "正在下載更新..."
} }
}, },
{
"ID": "DialogRebooterMessage",
"Translations": {
"ar_SA": "من فضلك انتظر، المحاكي في طور إعادة التشغيل",
"de_DE": "Bitte warten Sie, der Emulator wird neu gestartet",
"el_GR": "Παρακαλώ περιμένετε, ο εξομοιωτής επανεκκινείται",
"en_US": "Please wait, the emulator is restarting",
"es_ES": "Por favor, espere, el emulador se está reiniciando",
"fr_FR": "Veuillez patienter, l'émulateur est en train de redémarrer",
"he_IL": "אנא המתן, המחקה מתארגן מחדש",
"it_IT": "Attendere prego, l'emulatore si sta riavviando",
"ja_JP": "お待ちください、エミュレーターが再起動しています",
"ko_KR": "잠시만 기다려 주세요, 에뮬레이터가 재시작 중입니다",
"no_NO": "Vennligst vent, emulatoren starter på nytt",
"pl_PL": "Proszę czekać, emulator jest w trakcie ponownego uruchamiania",
"pt_BR": "Por favor, aguarde, o emulador está reiniciando",
"ru_RU": "Пожалуйста, подождите, эмулятор перезапускается",
"sv_SE": "Vänligen vänta, emulatorn startar om",
"th_TH": "กรุณารอสักครู่, ตัวจำลองกำลังเริ่มใหม่",
"tr_TR": "Lütfen bekleyin, emülatör yeniden başlatılıyor",
"uk_UA": "Будь ласка, зачекайте, емулятор перезавантажується",
"zh_CN": "请稍等,模拟器正在重新启动",
"zh_TW": "請稍候,模擬器正在重新啟動"
}
},
{ {
"ID": "DialogUpdaterExtractionMessage", "ID": "DialogUpdaterExtractionMessage",
"Translations": { "Translations": {
@ -16797,6 +16947,31 @@
"zh_TW": "謹慎使用" "zh_TW": "謹慎使用"
} }
}, },
{
"ID": "AvaloniaLogTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prints Avalonia (UI) log messages in the console.",
"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": "OpenGlLogLevel", "ID": "OpenGlLogLevel",
"Translations": { "Translations": {
@ -17697,6 +17872,31 @@
"zh_TW": "更新已停用!" "zh_TW": "更新已停用!"
} }
}, },
{
"ID": "UpdaterBackgroundStatusBarButtonText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Update Available!",
"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": "ControllerSettingsRotate90", "ID": "ControllerSettingsRotate90",
"Translations": { "Translations": {
@ -19272,6 +19472,31 @@
"zh_TW": "{0} 更新程式" "zh_TW": "{0} 更新程式"
} }
}, },
{
"ID": "RyujinxRebooter",
"Translations": {
"ar_SA": "إعادة تشغيل {0}",
"de_DE": "Neustart von {0}",
"el_GR": "Επανεκκίνηση {0}",
"en_US": "{0} Reboot",
"es_ES": "Reinicio de {0}",
"fr_FR": "Redémarrage de {0}",
"he_IL": "אתחול {0}",
"it_IT": "Riavvio di {0}",
"ja_JP": "{0} 再起動",
"ko_KR": "{0} 재부팅",
"no_NO": "Omstart av {0}",
"pl_PL": "Ponowne uruchomienie {0}",
"pt_BR": "Reinício de {0}",
"ru_RU": "{0} Перезагрузка",
"sv_SE": "Ominläsning av {0}",
"th_TH": "เริ่มต้นใหม่ {0}",
"tr_TR": "{0} Yeniden Başlatma",
"uk_UA": "Перезавантаження {0}",
"zh_CN": "{0} 重启",
"zh_TW": "{0} 重新啟動"
}
},
{ {
"ID": "SettingsTabHotkeys", "ID": "SettingsTabHotkeys",
"Translations": { "Translations": {

View File

@ -54,6 +54,7 @@ namespace Ryujinx.Ava.Common.Locale
SetDynamicValues(LocaleKeys.RyujinxInfo, RyujinxApp.FullAppName); SetDynamicValues(LocaleKeys.RyujinxInfo, RyujinxApp.FullAppName);
SetDynamicValues(LocaleKeys.RyujinxConfirm, RyujinxApp.FullAppName); SetDynamicValues(LocaleKeys.RyujinxConfirm, RyujinxApp.FullAppName);
SetDynamicValues(LocaleKeys.RyujinxUpdater, RyujinxApp.FullAppName); SetDynamicValues(LocaleKeys.RyujinxUpdater, RyujinxApp.FullAppName);
SetDynamicValues(LocaleKeys.RyujinxRebooter, RyujinxApp.FullAppName);
} }
public string this[LocaleKeys key] public string this[LocaleKeys key]

View File

@ -1,6 +1,5 @@
using DiscordRPC; using DiscordRPC;
using Gommon; using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Ava.Utilities.Configuration;
@ -11,7 +10,6 @@ using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon; using Ryujinx.Horizon;
using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Horizon.Prepo.Types;
using System.Linq;
using System.Text; using System.Text;
namespace Ryujinx.Ava namespace Ryujinx.Ava

View File

@ -387,7 +387,7 @@ namespace Ryujinx.Headless
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")] [Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
public string GraphicsShadersDumpPath { get; set; } public string GraphicsShadersDumpPath { get; set; }
[Option("graphics-backend", Required = false, Default = GraphicsBackend.OpenGl, HelpText = "Change Graphics Backend to use.")] [Option("graphics-backend", Required = false, Default = GraphicsBackend.Vulkan, HelpText = "Change Graphics Backend to use.")]
public GraphicsBackend GraphicsBackend { get; set; } public GraphicsBackend GraphicsBackend { get; set; }
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")] [Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]

View File

@ -513,7 +513,7 @@ namespace Ryujinx.Headless
Exit(); Exit();
} }
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText) public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText, (uint Module, uint Description)? errorCode = null)
{ {
SDL_MessageBoxData data = new() SDL_MessageBoxData data = new()
{ {
@ -521,7 +521,7 @@ namespace Ryujinx.Headless
message = message, message = message,
buttons = new SDL_MessageBoxButtonData[buttonsText.Length], buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
numbuttons = buttonsText.Length, numbuttons = buttonsText.Length,
window = WindowHandle, window = WindowHandle
}; };
for (int i = 0; i < buttonsText.Length; i++) for (int i = 0; i < buttonsText.Length; i++)

View File

@ -36,6 +36,7 @@ namespace Ryujinx.Ava
public static string GlobalConfigurationPath { get; private set; } public static string GlobalConfigurationPath { get; private set; }
public static bool PreviewerDetached { get; private set; } public static bool PreviewerDetached { get; private set; }
public static bool UseHardwareAcceleration { get; private set; } public static bool UseHardwareAcceleration { get; private set; }
public static string BackendThreadingArg { get; private set; }
[LibraryImport("user32.dll", SetLastError = true)] [LibraryImport("user32.dll", SetLastError = true)]
public static partial int MessageBoxA(nint hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type); public static partial int MessageBoxA(nint hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
@ -251,6 +252,11 @@ namespace Ryujinx.Ava
_ => ConfigurationState.Instance.Graphics.BackendThreading _ => ConfigurationState.Instance.Graphics.BackendThreading
}; };
if (CommandLineState.OverrideBackendThreadingAfterReboot is not null)
{
BackendThreadingArg = CommandLineState.OverrideBackendThreadingAfterReboot;
}
// Check if docked mode was overriden. // Check if docked mode was overriden.
if (CommandLineState.OverrideDockedMode.HasValue) if (CommandLineState.OverrideDockedMode.HasValue)
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
@ -294,7 +300,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.Language.Value = (Utilities.Configuration.System.Language)result; ConfigurationState.Instance.System.Language.Value = (Utilities.Configuration.System.Language)result;
} }
// Check if hardware-acceleration was overridden. MemoryManagerMode ( outdated! ) // Check if hardware-acceleration was overridden.
if (CommandLineState.OverrideHardwareAcceleration != null) if (CommandLineState.OverrideHardwareAcceleration != null)
UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value; UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value;
} }

95
src/Ryujinx/Rebooter.cs Normal file
View File

@ -0,0 +1,95 @@
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava
{
internal static class Rebooter
{
private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
public static void RebootAppWithGame(string gamePath, List<string> args)
{
_ = Reboot(gamePath, args);
}
private static async Task Reboot(string gamePath, List<string> args)
{
bool shouldRestart = true;
TaskDialog taskDialog = new()
{
Header = LocaleManager.Instance[LocaleKeys.RyujinxRebooter],
SubHeader = LocaleManager.Instance[LocaleKeys.DialogRebooterMessage],
IconSource = new SymbolIconSource { Symbol = Symbol.Games },
XamlRoot = RyujinxApp.MainWindow,
};
if (shouldRestart)
{
List<string> arguments = CommandLineState.Arguments.ToList();
string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
// On macOS we perform the update at relaunch.
if (OperatingSystem.IsMacOS())
{
string baseBundlePath = Path.GetFullPath(Path.Combine(executableDirectory, "..", ".."));
string newBundlePath = Path.Combine(_updateDir, "Ryujinx.app");
string updaterScriptPath = Path.Combine(newBundlePath, "Contents", "Resources", "updater.sh");
string currentPid = Environment.ProcessId.ToString();
arguments.InsertRange(0, new List<string> { updaterScriptPath, baseBundlePath, newBundlePath, currentPid });
Process.Start("/bin/bash", arguments);
}
else
{
var dialogTask = taskDialog.ShowAsync(true);
await Task.Delay(500);
// Find the process name.
string ryuName = Path.GetFileName(Environment.ProcessPath) ?? string.Empty;
// Some operating systems can see the renamed executable, so strip off the .ryuold if found.
if (ryuName.EndsWith(".ryuold"))
{
ryuName = ryuName[..^7];
}
// Fallback if the executable could not be found.
if (ryuName.Length == 0 || !Path.Exists(Path.Combine(executableDirectory, ryuName)))
{
ryuName = OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx";
}
ProcessStartInfo processStart = new(ryuName)
{
UseShellExecute = true,
WorkingDirectory = executableDirectory,
};
foreach (var arg in args)
{
processStart.ArgumentList.Add(arg);
}
processStart.ArgumentList.Add(gamePath);
Process.Start(processStart);
}
Environment.Exit(0);
}
}
}
}

View File

@ -75,31 +75,32 @@ namespace Ryujinx.Ava.UI.Applet
bool opened = false; bool opened = false;
UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent, UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent,
title, title,
message, message,
string.Empty, string.Empty,
LocaleManager.Instance[LocaleKeys.DialogOpenSettingsWindowLabel], LocaleManager.Instance[LocaleKeys.DialogOpenSettingsWindowLabel],
string.Empty, string.Empty,
LocaleManager.Instance[LocaleKeys.SettingsButtonClose], LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
(int)Symbol.Important, (int)Symbol.Important,
deferEvent, deferEvent,
async window => async window =>
{ {
if (opened) if (opened)
{ {
return; return;
} }
opened = true; opened = true;
_parent.SettingsWindow = new SettingsWindow(_parent.VirtualFileSystem, _parent.ContentManager); _parent.SettingsWindow =
new SettingsWindow(_parent.VirtualFileSystem, _parent.ContentManager);
await _parent.SettingsWindow.ShowDialog(window); await _parent.SettingsWindow.ShowDialog(window);
_parent.SettingsWindow = null; _parent.SettingsWindow = null;
opened = false; opened = false;
}); });
if (response == UserResult.Ok) if (response == UserResult.Ok)
{ {
@ -110,7 +111,9 @@ namespace Ryujinx.Ava.UI.Applet
} }
catch (Exception ex) catch (Exception ex)
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex)); await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance.UpdateAndGetDynamicValue(
LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex));
dialogCloseEvent.Set(); dialogCloseEvent.Set();
} }
@ -134,7 +137,9 @@ namespace Ryujinx.Ava.UI.Applet
try try
{ {
_parent.ViewModel.AppHost.NpadManager.BlockInputUpdates(); _parent.ViewModel.AppHost.NpadManager.BlockInputUpdates();
(UserResult result, string userInput) = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args); (UserResult result, string userInput) =
await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard],
args);
if (result == UserResult.Ok) if (result == UserResult.Ok)
{ {
@ -146,7 +151,9 @@ namespace Ryujinx.Ava.UI.Applet
{ {
error = true; error = true;
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex)); await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance.UpdateAndGetDynamicValue(
LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex));
} }
finally finally
{ {
@ -177,7 +184,8 @@ namespace Ryujinx.Ava.UI.Applet
args.InitialText = "Ryujinx"; args.InitialText = "Ryujinx";
args.StringLengthMin = 1; args.StringLengthMin = 1;
args.StringLengthMax = 25; args.StringLengthMax = 25;
(UserResult result, string userInput) = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.CabinetDialog], args); (UserResult result, string userInput) =
await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.CabinetDialog], args);
if (result == UserResult.Ok) if (result == UserResult.Ok)
{ {
inputText = userInput; inputText = userInput;
@ -201,11 +209,13 @@ namespace Ryujinx.Ava.UI.Applet
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
dialogCloseEvent.Set(); dialogCloseEvent.Set();
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.CabinetScanDialog], await ContentDialogHelper.CreateInfoDialog(
string.Empty, LocaleManager.Instance[LocaleKeys.CabinetScanDialog],
LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty,
string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk],
LocaleManager.Instance[LocaleKeys.CabinetTitle]); string.Empty,
LocaleManager.Instance[LocaleKeys.CabinetTitle]
);
}); });
dialogCloseEvent.WaitOne(); dialogCloseEvent.WaitOne();
} }
@ -217,7 +227,8 @@ namespace Ryujinx.Ava.UI.Applet
_parent.ViewModel.AppHost?.Stop(); _parent.ViewModel.AppHost?.Stop();
} }
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons) public bool DisplayErrorAppletDialog(string title, string message, string[] buttons,
(uint Module, uint Description)? errorCode = null)
{ {
ManualResetEvent dialogCloseEvent = new(false); ManualResetEvent dialogCloseEvent = new(false);
@ -229,9 +240,7 @@ namespace Ryujinx.Ava.UI.Applet
{ {
ErrorAppletWindow msgDialog = new(_parent, buttons, message) ErrorAppletWindow msgDialog = new(_parent, buttons, message)
{ {
Title = title, Title = title, WindowStartupLocation = WindowStartupLocation.CenterScreen, Width = 400
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Width = 400
}; };
object response = await msgDialog.Run(); object response = await msgDialog.Run();
@ -249,7 +258,9 @@ namespace Ryujinx.Ava.UI.Applet
{ {
dialogCloseEvent.Set(); dialogCloseEvent.Set();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex)); await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance.UpdateAndGetDynamicValue(
LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex));
} }
}); });
@ -278,13 +289,11 @@ namespace Ryujinx.Ava.UI.Applet
.ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav))); .ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav)));
profiles.Add(new Models.UserProfile(guest, nav)); profiles.Add(new Models.UserProfile(guest, nav));
UserSelectorDialogViewModel viewModel = new() ProfileSelectorDialogViewModel viewModel = new()
{ {
Profiles = profiles, Profiles = profiles, SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
}; };
UserSelectorDialog content = new(viewModel); (selected, _) = await ProfileSelectorDialog.ShowInputDialog(viewModel);
(selected, _) = await UserSelectorDialog.ShowInputDialog(content);
dialogCloseEvent.Set(); dialogCloseEvent.Set();
}); });
@ -311,6 +320,7 @@ namespace Ryujinx.Ava.UI.Applet
} }
} }
} }
return profile; return profile;
} }
} }

View File

@ -1,5 +1,5 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Applet.UserSelectorDialog" x:Class="Ryujinx.Ava.UI.Applet.ProfileSelectorDialog"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@ -12,9 +12,9 @@
d:DesignWidth="800" d:DesignWidth="800"
mc:Ignorable="d" mc:Ignorable="d"
Focusable="True" Focusable="True"
x:DataType="viewModels:UserSelectorDialogViewModel"> x:DataType="viewModels:ProfileSelectorDialogViewModel">
<Design.DataContext> <Design.DataContext>
<viewModels:UserSelectorDialogViewModel /> <viewModels:ProfileSelectorDialogViewModel />
</Design.DataContext> </Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">

View File

@ -16,15 +16,15 @@ using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Ava.UI.Applet namespace Ryujinx.Ava.UI.Applet
{ {
public partial class UserSelectorDialog : UserControl, INotifyPropertyChanged public partial class ProfileSelectorDialog : UserControl
{ {
public UserSelectorDialogViewModel ViewModel { get; set; } public ProfileSelectorDialogViewModel ViewModel { get; set; }
public UserSelectorDialog(UserSelectorDialogViewModel viewModel) public ProfileSelectorDialog(ProfileSelectorDialogViewModel viewModel)
{ {
DataContext = ViewModel = viewModel;
InitializeComponent(); InitializeComponent();
ViewModel = viewModel;
DataContext = ViewModel;
} }
private void Grid_PointerEntered(object sender, PointerEventArgs e) private void Grid_PointerEntered(object sender, PointerEventArgs e)
@ -54,7 +54,7 @@ namespace Ryujinx.Ava.UI.Applet
if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile) if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
{ {
ViewModel.SelectedUserId = userProfile.UserId; ViewModel.SelectedUserId = userProfile.UserId;
Logger.Info?.Print(LogClass.UI, $"Selected user: {userProfile.UserId}"); Logger.Info?.Print(LogClass.UI, $"Selected: {userProfile.UserId}", "ProfileSelector");
ObservableCollection<BaseModel> newProfiles = []; ObservableCollection<BaseModel> newProfiles = [];
@ -79,7 +79,7 @@ namespace Ryujinx.Ava.UI.Applet
} }
} }
public static async Task<(UserId Id, bool Result)> ShowInputDialog(UserSelectorDialog content) public static async Task<(UserId Id, bool Result)> ShowInputDialog(ProfileSelectorDialogViewModel viewModel)
{ {
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
@ -87,22 +87,25 @@ namespace Ryujinx.Ava.UI.Applet
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue], PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty, SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel], CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
Content = content, Content = new ProfileSelectorDialog(viewModel),
Padding = new Thickness(0) Padding = new Thickness(0)
}; };
UserId result = UserId.Null; UserId result = UserId.Null;
bool input = false; bool input = false;
contentDialog.Closed += Handler;
await ContentDialogHelper.ShowAsync(contentDialog);
return (result, input);
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs) void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
{ {
if (eventArgs.Result == ContentDialogResult.Primary) if (eventArgs.Result == ContentDialogResult.Primary)
{ {
if (contentDialog.Content is UserSelectorDialog view) result = viewModel.SelectedUserId;
{ input = true;
result = view.ViewModel.SelectedUserId;
input = true;
}
} }
else else
{ {
@ -110,12 +113,6 @@ namespace Ryujinx.Ava.UI.Applet
input = false; input = false;
} }
} }
contentDialog.Closed += Handler;
await ContentDialogHelper.ShowAsync(contentDialog);
return (result, input);
} }
} }
} }

View File

@ -407,10 +407,10 @@ namespace Ryujinx.Ava.UI.Controls
{ {
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
{ {
await new UserConfigWindows(viewModel).ShowDialog((Window)viewModel.TopLevel); await new GameSpecificSettingsWindow(viewModel).ShowDialog((Window)viewModel.TopLevel);
//just checking for file presence //just checking for file presence
viewModel.SelectedApplication.UserConfig = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false)); viewModel.SelectedApplication.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false));
viewModel.RefreshView(); viewModel.RefreshView();
} }

View File

@ -55,9 +55,21 @@
Tag="{Binding AppData.IdString}" Tag="{Binding AppData.IdString}"
Text="{Binding AppData.LocalizedStatus}" Text="{Binding AppData.LocalizedStatus}"
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}" Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
ToolTip.Tip="{Binding AppData.LocalizedStatusTooltip}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap" /> TextWrapping="Wrap">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding AppData.LocalizedStatusTooltip}" />
<Separator
Margin="0, 10, 0, 10"
IsVisible="{Binding AppData.HasCompatibilityLabels}" />
<TextBlock
IsVisible="{Binding AppData.HasCompatibilityLabels}"
Text="{Binding AppData.FormattedCompatibilityLabels}" />
</StackPanel>
</ToolTip.Tip>
</TextBlock>
<Button.Styles> <Button.Styles>
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="MinWidth" <Setter Property="MinWidth"

View File

@ -1,12 +1,9 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Styling; using Avalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;

View File

@ -80,7 +80,7 @@
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
IsVisible="{Binding UserConfig}" IsVisible="{Binding HasIndependentConfiguration}"
Text="{ext:Locale UserConfigurationHeader}" Text="{ext:Locale UserConfigurationHeader}"
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" TextWrapping="Wrap"
@ -105,7 +105,7 @@
Width="90" Width="90"
Height="20" Height="20"
CornerRadius="4" CornerRadius="4"
IsVisible="{Binding UserConfig}" IsVisible="{Binding HasIndependentConfiguration}"
Background="{DynamicResource ThemeContentBackgroundColor}"> Background="{DynamicResource ThemeContentBackgroundColor}">
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"

View File

@ -94,8 +94,19 @@
IsVisible="{Binding HasPlayabilityInfo}" IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}" Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0" Margin="-1, 0, 0, 0"
Padding="0" Padding="0">
ToolTip.Tip="{Binding LocalizedStatusTooltip}"> <ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding LocalizedStatusTooltip}" />
<Separator
Margin="0, 10, 0, 10"
IsVisible="{Binding HasCompatibilityLabels}" />
<TextBlock
IsVisible="{Binding HasCompatibilityLabels}"
Text="{Binding FormattedCompatibilityLabels}" />
</StackPanel>
</ToolTip.Tip>
<TextBlock <TextBlock
Margin="1.5" Margin="1.5"
Tag="{Binding IdString}" Tag="{Binding IdString}"
@ -148,7 +159,7 @@
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding UserConfig}" IsVisible="{Binding HasIndependentConfiguration}"
Text="{ext:Locale UserConfigurationHeader}" Text="{ext:Locale UserConfigurationHeader}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap" TextWrapping="Wrap"

View File

@ -8,7 +8,6 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat; using Ryujinx.Ava.Utilities.Compat;
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Controls

View File

@ -1,6 +1,7 @@
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Utilities; using Avalonia.Utilities;
using Gommon; using Gommon;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using System; using System;
using System.Text; using System.Text;
@ -14,13 +15,19 @@ namespace Ryujinx.Ava.UI.Helpers
internal class LoggerAdapter : ILogSink internal class LoggerAdapter : ILogSink
{ {
private static bool _avaloniaLogsEnabled = ConfigurationState.Instance.Logger.EnableAvaloniaLog;
public static void Register() public static void Register()
{ {
AvaLogger.Sink = new LoggerAdapter(); AvaLogger.Sink = new LoggerAdapter();
ConfigurationState.Instance.Logger.EnableAvaloniaLog.Event
+= (_, e) => _avaloniaLogsEnabled = e.NewValue;
} }
private static RyuLogger.Log? GetLog(AvaLogLevel level, string area) private static RyuLogger.Log? GetLog(AvaLogLevel level, string area)
{ {
if (!_avaloniaLogsEnabled) return null;
return level switch return level switch
{ {
AvaLogLevel.Verbose => RyuLogger.Debug, AvaLogLevel.Verbose => RyuLogger.Debug,

View File

@ -2,6 +2,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common; using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Ava.Utilities.Configuration;
@ -32,15 +33,16 @@ namespace Ryujinx.Ava.UI.ViewModels
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value)); Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
} }
private const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
private void UpdateLogoTheme(string theme) private void UpdateLogoTheme(string theme)
{ {
bool isDarkTheme = theme == "Dark" || (theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark); bool isDarkTheme = theme == "Dark" || (theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
string basePath = "resm:Ryujinx.Assets.UIImages."; string themeName = isDarkTheme ? "Dark" : "Light";
string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png";
GithubLogo = LoadBitmap($"{basePath}Logo_GitHub_{themeSuffix}?assembly=Ryujinx"); GithubLogo = LoadBitmap(LogoPathFormat.Format("GitHub", themeName));
DiscordLogo = LoadBitmap($"{basePath}Logo_Discord_{themeSuffix}?assembly=Ryujinx"); DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
} }
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri))); private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
@ -48,6 +50,10 @@ namespace Ryujinx.Ava.UI.ViewModels
public void Dispose() public void Dispose()
{ {
ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged; ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
GithubLogo.Dispose();
DiscordLogo.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }

View File

@ -264,7 +264,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}"); Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}");
// Neither local or remote files are valid JSON, close window. // Neither local or remote files are valid JSON, close window.
ShowInfoDialog(); await ShowInfoDialog();
Close(); Close();
} }
else if (!remoteIsValid) else if (!remoteIsValid)
@ -273,7 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels
// Only the local file is valid, the local one should be used // Only the local file is valid, the local one should be used
// but the user should be warned. // but the user should be warned.
ShowInfoDialog(); await ShowInfoDialog();
} }
} }
@ -525,7 +525,7 @@ namespace Ryujinx.Ava.UI.ViewModels
AmiiboImage = bitmap; AmiiboImage = bitmap;
} }
private static async void ShowInfoDialog() private static async Task ShowInfoDialog()
{ {
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle], await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage], LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage],

View File

@ -7,6 +7,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -104,6 +105,13 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] private bool _isSubMenuOpen; [ObservableProperty] private bool _isSubMenuOpen;
[ObservableProperty] private ApplicationContextMenu _listAppContextMenu; [ObservableProperty] private ApplicationContextMenu _listAppContextMenu;
[ObservableProperty] private ApplicationContextMenu _gridAppContextMenu; [ObservableProperty] private ApplicationContextMenu _gridAppContextMenu;
[ObservableProperty] private bool _updateAvailable;
public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () =>
{
if (Updater.CanUpdate(true))
await Updater.BeginUpdateAsync(true);
});
private bool _showLoadProgress; private bool _showLoadProgress;
private bool _isGameRunning; private bool _isGameRunning;
@ -1147,10 +1155,10 @@ namespace Ryujinx.Ava.UI.ViewModels
List<string> dirs = result.Select(it => it.Path.LocalPath).ToList(); List<string> dirs = result.Select(it => it.Path.LocalPath).ToList();
int numAdded = onDirsSelected(dirs, out int numRemoved); int numAdded = onDirsSelected(dirs, out int numRemoved);
string msg = String.Join("\r\n", new string[] { string msg = string.Join("\n",
string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved), string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved),
string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded) string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded)
}); );
await Dispatcher.UIThread.InvokeAsync(async () => await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
@ -1523,10 +1531,16 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public void InitializeUserConfig(ApplicationData application) public bool InitializeUserConfig(ApplicationData application)
{ {
// Code where conditions will be met before loading the user configuration // Code where conditions will be met before loading the user configuration (Global Config)
BackendThreading backendThreadingValue = ConfigurationState.Instance.Graphics.BackendThreading.Value; BackendThreading backendThreadingValue = ConfigurationState.Instance.Graphics.BackendThreading.Value;
string BackendThreadingInit = Program.BackendThreadingArg;
if (BackendThreadingInit is null)
{
BackendThreadingInit = ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString();
}
// If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting. // If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting.
string idGame = application.IdBaseString; string idGame = application.IdBaseString;
@ -1537,19 +1551,29 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
// Code where conditions will be executed after loading user configuration // Code where conditions will be executed after loading user configuration
if (ConfigurationState.Instance.Graphics.BackendThreading != backendThreadingValue) if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != BackendThreadingInit)
{ {
/*
* The function to restart the emulator together with the selected game List<string> Arguments = new List<string>
Task.Run(async () => await Rebooter.RebootAppWithGame(application.Path)); {
*/ "--bt", ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() // BackendThreading
};
Rebooter.RebootAppWithGame(application.Path, Arguments);
return true;
} }
return false;
} }
public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct<ApplicationControlProperty>? customNacpData = null) public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{ {
InitializeUserConfig(application); if (InitializeUserConfig(application))
{
return;
}
if (AppHost != null) if (AppHost != null)
{ {

View File

@ -4,7 +4,7 @@ using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
public partial class UserSelectorDialogViewModel : BaseModel public partial class ProfileSelectorDialogViewModel : BaseModel
{ {
[ObservableProperty] private UserId _selectedUserId; [ObservableProperty] private UserId _selectedUserId;

View File

@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.System; using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.GraphicsDriver;
@ -73,45 +74,16 @@ namespace Ryujinx.Ava.UI.ViewModels
public string GamePath { get; } public string GamePath { get; }
public string GameName { get; } public string GameName { get; }
private readonly bool _isGameRunning;
private Bitmap _gameIcon; private Bitmap _gameIcon;
private string _gameTitle; private string _gameTitle;
private string _gamePath;
private string _gameId; private string _gameId;
public Bitmap GameIcon public bool IsGameRunning => _isGameRunning;
{ public Bitmap GameIcon => _gameIcon;
get => _gameIcon; public string GamePath => _gamePath;
set public string GameTitle => _gameTitle;
{ public string GameId => _gameId;
if (_gameIcon != value)
{
_gameIcon = value;
}
}
}
public string GameTitle
{
get => _gameTitle;
set
{
if (_gameTitle != value)
{
_gameTitle = value;
}
}
}
public string GameId
{
get => _gameId;
set
{
if (_gameId != value)
{
_gameId = value;
}
}
}
public int ResolutionScale public int ResolutionScale
{ {
@ -166,9 +138,12 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool RememberWindowState { get; set; } public bool RememberWindowState { get; set; }
public bool ShowTitleBar { get; set; } public bool ShowTitleBar { get; set; }
public int HideCursor { get; set; } public int HideCursor { get; set; }
public int UpdateCheckerType { get; set; }
public bool EnableDockedMode { get; set; } public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; } public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; } public bool EnableMouse { get; set; }
public bool DisableInputWhenOutOfFocus { get; set; }
public VSyncMode VSyncMode public VSyncMode VSyncMode
{ {
get => _vSyncMode; get => _vSyncMode;
@ -249,6 +224,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTrace { get; set; } public bool EnableTrace { get; set; }
public bool EnableGuest { get; set; } public bool EnableGuest { get; set; }
public bool EnableFsAccessLog { get; set; } public bool EnableFsAccessLog { get; set; }
public bool EnableAvaloniaLog { get; set; }
public bool EnableDebug { get; set; } public bool EnableDebug { get; set; }
public bool IsOpenAlEnabled { get; set; } public bool IsOpenAlEnabled { get; set; }
public bool IsSoundIoEnabled { get; set; } public bool IsSoundIoEnabled { get; set; }
@ -387,8 +363,10 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public SettingsViewModel(VirtualFileSystem virtualFileSystem, public SettingsViewModel(
VirtualFileSystem virtualFileSystem,
ContentManager contentManager, ContentManager contentManager,
bool gameRunning,
string gamePath, string gamePath,
string gameName, string gameName,
string gameId, string gameId,
@ -402,12 +380,14 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
using (var ms = new MemoryStream(gameIconData)) using (var ms = new MemoryStream(gameIconData))
{ {
GameIcon = new Bitmap(ms); _gameIcon = new Bitmap(ms);
} }
} }
GameTitle = gameName; _isGameRunning = gameRunning;
GameId = gameId; _gamePath = gamePath;
_gameTitle = gameName;
_gameId = gameId;
if (enableToLoadCustomConfig) // During the game. If there is no user config, then load the global config window if (enableToLoadCustomConfig) // During the game. If there is no user config, then load the global config window
{ {
@ -424,7 +404,6 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
Task.Run(LoadTimeZones); Task.Run(LoadTimeZones);
DirtyHacks = new SettingsHacksViewModel(this);
} }
} }
@ -558,6 +537,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
ConfigurationState config = ConfigurationState.Instance; ConfigurationState config = ConfigurationState.Instance;
//It is necessary that the data is used from the global configuration file //It is necessary that the data is used from the global configuration file
if (string.IsNullOrEmpty(GameId)) if (string.IsNullOrEmpty(GameId))
{ {
@ -568,6 +548,7 @@ namespace Ryujinx.Ava.UI.ViewModels
RememberWindowState = config.RememberWindowState; RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar; ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value; HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
GameDirectories.Clear(); GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value); GameDirectories.AddRange(config.UI.GameDirs.Value);
@ -589,6 +570,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDockedMode = config.System.EnableDockedMode; EnableDockedMode = config.System.EnableDockedMode;
EnableKeyboard = config.Hid.EnableKeyboard; EnableKeyboard = config.Hid.EnableKeyboard;
EnableMouse = config.Hid.EnableMouse; EnableMouse = config.Hid.EnableMouse;
DisableInputWhenOutOfFocus = config.Hid.DisableInputWhenOutOfFocus;
// Keyboard Hotkeys // Keyboard Hotkeys
KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value); KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value);
@ -654,6 +636,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableGuest = config.Logger.EnableGuest; EnableGuest = config.Logger.EnableGuest;
EnableDebug = config.Logger.EnableDebug; EnableDebug = config.Logger.EnableDebug;
EnableFsAccessLog = config.Logger.EnableFsAccessLog; EnableFsAccessLog = config.Logger.EnableFsAccessLog;
EnableAvaloniaLog = config.Logger.EnableAvaloniaLog;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
@ -668,6 +651,7 @@ namespace Ryujinx.Ava.UI.ViewModels
ConfigurationState config = ConfigurationState.Instance; ConfigurationState config = ConfigurationState.Instance;
bool userConfigFile = string.IsNullOrEmpty(GameId); bool userConfigFile = string.IsNullOrEmpty(GameId);
if (userConfigFile) if (userConfigFile)
{ {
// User Interface // User Interface
@ -677,6 +661,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.RememberWindowState.Value = RememberWindowState; config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar; config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor; config.HideCursor.Value = (HideCursorMode)HideCursor;
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
if (GameDirectoryChanged) if (GameDirectoryChanged)
{ {
@ -701,6 +686,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.EnableDockedMode.Value = EnableDockedMode; config.System.EnableDockedMode.Value = EnableDockedMode;
config.Hid.EnableKeyboard.Value = EnableKeyboard; config.Hid.EnableKeyboard.Value = EnableKeyboard;
config.Hid.EnableMouse.Value = EnableMouse; config.Hid.EnableMouse.Value = EnableMouse;
config.Hid.DisableInputWhenOutOfFocus.Value = DisableInputWhenOutOfFocus;
// Keyboard Hotkeys // Keyboard Hotkeys
config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig(); config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig();
@ -777,6 +763,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Logger.EnableGuest.Value = EnableGuest; config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableDebug.Value = EnableDebug; config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;

View File

@ -52,11 +52,7 @@ namespace Ryujinx.Ava.UI.Views.Main
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show); AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show()); CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show());
UpdateMenuItem.Command = Commands.Create(async () => UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
{
if (Updater.CanUpdate(true))
await Updater.BeginUpdateAsync(true);
});
FaqMenuItem.Command = FaqMenuItem.Command =
SetupGuideMenuItem.Command = SetupGuideMenuItem.Command =
@ -150,7 +146,7 @@ namespace Ryujinx.Ava.UI.Views.Main
else else
{ {
// If there is a custom configuration in the folder // If there is a custom configuration in the folder
await new UserConfigWindows(ViewModel, userConfigExist).ShowDialog((Window)ViewModel.TopLevel); await new GameSpecificSettingsWindow(ViewModel, userConfigExist).ShowDialog((Window)ViewModel.TopLevel);
} }
} }

View File

@ -23,7 +23,7 @@
Background="{DynamicResource ThemeContentBackgroundColor}" Background="{DynamicResource ThemeContentBackgroundColor}"
DockPanel.Dock="Bottom" DockPanel.Dock="Bottom"
IsVisible="{Binding ShowMenuAndStatusBar}" IsVisible="{Binding ShowMenuAndStatusBar}"
ColumnDefinitions="Auto,Auto,*,Auto,Auto"> ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<StackPanel <StackPanel
Grid.Column="0" Grid.Column="0"
Margin="5" Margin="5"
@ -283,6 +283,28 @@
<StackPanel <StackPanel
Grid.Column="4" Grid.Column="4"
Margin="0,0,5,0" Margin="0,0,5,0"
Orientation="Horizontal">
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="EnableNonGameRunningControls" />
<Binding Path="UpdateAvailable" />
</MultiBinding>
</StackPanel.IsVisible>
<Button Margin="0, 0, 5, -2"
Command="{Binding UpdateCommand}"
Background="{DynamicResource SystemAccentColor}">
<TextBlock
Margin="-5"
Foreground="{StaticResource SystemColorButtonTextColor}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{ext:Locale UpdaterBackgroundStatusBarButtonText}" />
</Button>
<controls:MiniVerticalSeparator Margin="5,0,0,0"/>
</StackPanel>
<StackPanel
Grid.Column="5"
Margin="0,0,5,0"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding ShowFirmwareStatus}" IsVisible="{Binding ShowFirmwareStatus}"
Orientation="Horizontal"> Orientation="Horizontal">

View File

@ -74,6 +74,10 @@
ToolTip.Tip="{ext:Locale DebugLogTooltip}"> ToolTip.Tip="{ext:Locale DebugLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableDebugLogs}" /> <TextBlock Text="{ext:Locale SettingsTabLoggingEnableDebugLogs}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding EnableAvaloniaLog}"
ToolTip.Tip="{ext:Locale AvaloniaLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableAvaloniaLogs}" />
</CheckBox>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch"> <StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale FSAccessLogModeTooltip}" ToolTip.Tip="{ext:Locale FSAccessLogModeTooltip}"

View File

@ -1,5 +1,4 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;

View File

@ -6,6 +6,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
mc:Ignorable="d" mc:Ignorable="d"
x:DataType="viewModels:SettingsViewModel"> x:DataType="viewModels:SettingsViewModel">
<Design.DataContext> <Design.DataContext>
@ -30,18 +31,36 @@
ToolTip.Tip="{ext:Locale ToggleDiscordTooltip}" ToolTip.Tip="{ext:Locale ToggleDiscordTooltip}"
Text="{ext:Locale SettingsTabGeneralEnableDiscordRichPresence}" /> Text="{ext:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
</CheckBox>
<CheckBox IsChecked="{Binding ShowConfirmExit}"> <CheckBox IsChecked="{Binding ShowConfirmExit}">
<TextBlock Text="{ext:Locale SettingsTabGeneralShowConfirmExitDialog}" /> <TextBlock Text="{ext:Locale SettingsTabGeneralShowConfirmExitDialog}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding RememberWindowState}"> <CheckBox IsChecked="{Binding RememberWindowState}">
<TextBlock Text="{ext:Locale SettingsTabGeneralRememberWindowState}" /> <TextBlock Text="{ext:Locale SettingsTabGeneralRememberWindowState}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding ShowTitleBar}" Name="ShowTitleBarBox"> <CheckBox IsChecked="{Binding DisableInputWhenOutOfFocus}">
<TextBlock Text="{ext:Locale SettingsTabGeneralDisableInputWhenOutOfFocus}" />
</CheckBox>
<CheckBox IsChecked="{Binding ShowTitleBar}" IsVisible="{x:Static helper:RunningPlatform.IsWindows}">
<TextBlock Text="{ext:Locale SettingsTabGeneralShowTitleBar}" /> <TextBlock Text="{ext:Locale SettingsTabGeneralShowTitleBar}" />
</CheckBox> </CheckBox>
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunch}"
Width="150" />
<ComboBox SelectedIndex="{Binding UpdateCheckerType}"
HorizontalContentAlignment="Left"
MinWidth="100">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabGeneralCheckUpdatesOnLaunchBackground}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal"> <StackPanel Margin="0, 15, 0, 0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralHideCursor}" Text="{ext:Locale SettingsTabGeneralHideCursor}"

View File

@ -21,7 +21,6 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsUiView() public SettingsUiView()
{ {
InitializeComponent(); InitializeComponent();
ShowTitleBarBox.IsVisible = OperatingSystem.IsWindows();
AddGameDirButton.Command = AddGameDirButton.Command =
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true)); Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true));
AddAutoloadDirButton.Command = AddAutoloadDirButton.Command =

View File

@ -125,7 +125,7 @@
Background="Transparent" Background="Transparent"
Click="Button_OnClick" Click="Button_OnClick"
CornerRadius="15" CornerRadius="15"
Tag="https://discord.gg/dHPrkBkkyA" Tag="https://discord.gg/PEuzjrFXUA"
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}"> ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
<Image Source="{Binding DiscordLogo}" /> <Image Source="{Binding DiscordLogo}" />
</Button> </Button>
@ -142,42 +142,40 @@
<Grid <Grid
Grid.Column="2" Grid.Column="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" RowDefinitions="Auto,Auto"> VerticalAlignment="Stretch" RowDefinitions="Auto,Auto,Auto">
<StackPanel <StackPanel
Grid.Row="0" Grid.Row="0"
Margin="0,10,0,0"
Spacing="2"> Spacing="2">
<TextBlock <TextBlock
FontSize="15" Classes="h1"
FontWeight="Bold" FontWeight="Bold"
Text="{ext:Locale AboutRyujinxAboutTitle}" /> Text="{ext:Locale AboutRyujinxAboutTitle}" />
<TextBlock <TextBlock
FontSize="10"
Text="{ext:Locale AboutRyujinxAboutContent}" Text="{ext:Locale AboutRyujinxAboutContent}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<Separator Grid.Row="1" Margin="0,20" />
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="2"
Margin="0,10,0,0"
Spacing="2"> Spacing="2">
<TextBlock <TextBlock
FontSize="15" Classes="h1"
FontWeight="Bold" FontWeight="Bold"
Text="{ext:Locale AboutRyujinxMaintainersTitle}" /> Text="{ext:Locale AboutRyujinxMaintainersTitle}" />
<TextBlock <TextBlock
FontSize="10"
Margin="0, 0, 0, 5" Margin="0, 0, 0, 5"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="{Binding Developers}"/> Text="{Binding Developers}"/>
<TextBlock <TextBlock
FontSize="15" Classes="h1"
FontWeight="Bold" FontWeight="Bold"
Text="{ext:Locale AboutRyujinxFormerMaintainersTitle}" /> Text="{ext:Locale AboutRyujinxFormerMaintainersTitle}" />
<TextBlock <TextBlock
FontSize="10" FontSize="11"
Text="{Binding FormerDevelopers}" Text="{Binding FormerDevelopers}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<Button <Button
Margin="0, 5, 0, 0"
Padding="5" Padding="5"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Background="Transparent" Background="Transparent"

View File

@ -18,8 +18,6 @@ namespace Ryujinx.Ava.UI.Windows
{ {
public AboutWindow() public AboutWindow()
{ {
DataContext = new AboutWindowViewModel();
InitializeComponent(); InitializeComponent();
GitHubRepoButton.Tag = GitHubRepoButton.Tag =
@ -28,12 +26,14 @@ namespace Ryujinx.Ava.UI.Windows
public static async Task Show() public static async Task Show()
{ {
using AboutWindowViewModel viewModel = new();
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
PrimaryButtonText = string.Empty, PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty, SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
Content = new AboutWindow() Content = new AboutWindow { DataContext = viewModel }
}; };
Style closeButton = new(x => x.Name("CloseButton")); Style closeButton = new(x => x.Name("CloseButton"));

View File

@ -1,5 +1,5 @@
<window:StyleableAppWindow <window:StyleableAppWindow
x:Class="Ryujinx.Ava.UI.Windows.UserConfigWindows" x:Class="Ryujinx.Ava.UI.Windows.GameSpecificSettingsWindow"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -136,6 +136,11 @@
Content="{ext:Locale SettingsButtonClose}" Content="{ext:Locale SettingsButtonClose}"
Command="{Binding CancelButton}" /> Command="{Binding CancelButton}" />
<Button <Button
IsVisible="{Binding IsGameRunning}"
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
<Button
IsVisible="{Binding !IsGameRunning}"
Content="{ext:Locale UserProfilesDelete}" Content="{ext:Locale UserProfilesDelete}"
Command="{Binding DeleteConfigGame}" Command="{Binding DeleteConfigGame}"
Classes="red"/> Classes="red"/>

View File

@ -24,17 +24,18 @@ using Key = Avalonia.Input.Key;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
{ {
public partial class UserConfigWindows : StyleableAppWindow public partial class GameSpecificSettingsWindow : StyleableAppWindow
{ {
internal readonly SettingsViewModel ViewModel; internal readonly SettingsViewModel ViewModel;
public UserConfigWindows(MainWindowViewModel viewModel, bool findUserConfigDir = true) public GameSpecificSettingsWindow(MainWindowViewModel viewModel, bool findUserConfigDir = true)
{ {
Title = string.Format(LocaleManager.Instance[LocaleKeys.SettingsWithInfo], viewModel.SelectedApplication.Name, viewModel.SelectedApplication.IdString); Title = string.Format(LocaleManager.Instance[LocaleKeys.SettingsWithInfo], viewModel.SelectedApplication.Name, viewModel.SelectedApplication.IdString);
DataContext = ViewModel = new SettingsViewModel( DataContext = ViewModel = new SettingsViewModel(
viewModel.VirtualFileSystem, viewModel.VirtualFileSystem,
viewModel.ContentManager, viewModel.ContentManager,
viewModel.IsGameRunning,
viewModel.SelectedApplication.Path, viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name, viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.IdString, viewModel.SelectedApplication.IdString,

View File

@ -21,7 +21,9 @@
x:DataType="viewModels:MainWindowViewModel" x:DataType="viewModels:MainWindowViewModel"
mc:Ignorable="d" mc:Ignorable="d"
WindowStartupLocation="Manual" WindowStartupLocation="Manual"
Focusable="True"> Focusable="True"
GotFocus="InputElement_OnGotFocus"
LostFocus="InputElement_OnLostFocus">
<Window.Styles> <Window.Styles>
<Style Selector="TitleBar:fullscreen"> <Style Selector="TitleBar:fullscreen">
<Setter Property="Background" Value="#000000" /> <Setter Property="Background" Value="#000000" />

View File

@ -1,6 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
@ -19,6 +20,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Helper; using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@ -400,10 +402,21 @@ namespace Ryujinx.Ava.UI.Windows
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
} }
if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate()) if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates)
return;
switch (ConfigurationState.Instance.UpdateCheckerType.Value)
{ {
await Updater.BeginUpdateAsync() case UpdaterType.PromptAtStartup:
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}")); await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
break;
case UpdaterType.CheckInBackground:
if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions))
{
Dispatcher.UIThread.Post(() => RyujinxApp.MainWindow.ViewModel.UpdateAvailable = versions.Current < versions.Incoming);
}
break;
} }
} }
@ -754,5 +767,34 @@ namespace Ryujinx.Ava.UI.Windows
_intelMacWarningShown = true; _intelMacWarningShown = true;
} }
private void InputElement_OnGotFocus(object sender, GotFocusEventArgs e)
{
if (!_didDisableInputUpdates)
return;
if (!ConfigurationState.Instance.Hid.DisableInputWhenOutOfFocus)
return;
if (ViewModel.AppHost is not { NpadManager.InputUpdatesBlocked: true } appHost)
return;
appHost.NpadManager.UnblockInputUpdates();
_didDisableInputUpdates = appHost.NpadManager.InputUpdatesBlocked;
}
private bool _didDisableInputUpdates;
private void InputElement_OnLostFocus(object sender, RoutedEventArgs e)
{
if (!ConfigurationState.Instance.Hid.DisableInputWhenOutOfFocus)
return;
if (ViewModel.AppHost is not { NpadManager.InputUpdatesBlocked: false } appHost)
return;
appHost.NpadManager.BlockInputUpdates();
_didDisableInputUpdates = appHost.NpadManager.InputUpdatesBlocked;
}
} }
} }

View File

@ -43,7 +43,18 @@ namespace Ryujinx.Ava
private const int ConnectionCount = 4; private const int ConnectionCount = 4;
private static string _buildVer; private static string _buildVer;
private static string _platformExt;
private static readonly string _platformExt =
RunningPlatform.IsMacOS
? "macos_universal.app.tar.gz"
: RunningPlatform.IsWindows
? "win_x64.zip"
: RunningPlatform.IsX64Linux
? "linux_x64.tar.gz"
: RunningPlatform.IsArmLinux
? "linux_arm64.tar.gz"
: throw new PlatformNotSupportedException();
private static string _buildUrl; private static string _buildUrl;
private static long _buildSize; private static long _buildSize;
private static bool _updateSuccessful; private static bool _updateSuccessful;
@ -51,30 +62,8 @@ namespace Ryujinx.Ava
private static readonly string[] _windowsDependencyDirs = []; private static readonly string[] _windowsDependencyDirs = [];
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false) public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
{ {
if (_running)
{
return;
}
_running = true;
// Detect current platform
if (OperatingSystem.IsMacOS())
{
_platformExt = "macos_universal.app.tar.gz";
}
else if (OperatingSystem.IsWindows())
{
_platformExt = "win_x64.zip";
}
else if (OperatingSystem.IsLinux())
{
string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
_platformExt = $"linux_{arch}.tar.gz";
}
if (!Version.TryParse(Program.Version, out Version currentVersion)) if (!Version.TryParse(Program.Version, out Version currentVersion))
{ {
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!"); Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
@ -85,7 +74,7 @@ namespace Ryujinx.Ava
_running = false; _running = false;
return; return default;
} }
Logger.Info?.Print(LogClass.Application, "Checking for updates."); Logger.Info?.Print(LogClass.Application, "Checking for updates.");
@ -123,7 +112,7 @@ namespace Ryujinx.Ava
_running = false; _running = false;
return; return default;
} }
break; break;
@ -149,7 +138,7 @@ namespace Ryujinx.Ava
_running = false; _running = false;
return; return default;
} }
} }
catch (Exception exception) catch (Exception exception)
@ -161,7 +150,7 @@ namespace Ryujinx.Ava
_running = false; _running = false;
return; return default;
} }
if (!Version.TryParse(_buildVer, out Version newVersion)) if (!Version.TryParse(_buildVer, out Version newVersion))
@ -174,9 +163,27 @@ namespace Ryujinx.Ava
_running = false; _running = false;
return default;
}
return (currentVersion, newVersion);
}
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
{
if (_running)
{
return; return;
} }
_running = true;
Optional<(Version, Version)> versionTuple = await CheckVersionAsync(showVersionUpToDate);
if (_running is false || !versionTuple.HasValue) return;
(Version currentVersion, Version newVersion) = versionTuple.Value;
if (newVersion <= currentVersion) if (newVersion <= currentVersion)
{ {
if (showVersionUpToDate) if (showVersionUpToDate)

View File

@ -1,3 +1,4 @@
using Gommon;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
@ -14,6 +15,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary namespace Ryujinx.Ava.Utilities.AppLibrary
@ -21,7 +23,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public class ApplicationData public class ApplicationData
{ {
public bool Favorite { get; set; } public bool Favorite { get; set; }
public bool UserConfig { get; set; } public bool HasIndependentConfiguration { get; set; }
public byte[] Icon { get; set; } public byte[] Icon { get; set; }
public string Name { get; set; } = "Unknown"; public string Name { get; set; } = "Unknown";
@ -33,33 +35,12 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
set set
{ {
_id = value; _id = value;
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
Compatibility = CompatibilityCsv.Find(Id);
} }
} }
public string Developer { get; set; } = "Unknown"; public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0"; public string Version { get; set; } = "0";
public bool HasPlayabilityInfo => PlayabilityStatus != null;
public string LocalizedStatus =>
PlayabilityStatus.HasValue
? LocaleManager.Instance[PlayabilityStatus!.Value]
: string.Empty;
public LocaleKeys? PlayabilityStatus { get; set; }
public 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 PlayerCount { get; set; }
public int GameCount { get; set; } public int GameCount { get; set; }
@ -79,12 +60,40 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public bool HasPlayedPreviously => TimePlayedString != string.Empty; public bool HasPlayedPreviously => TimePlayed.TotalSeconds > 1;
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n"); public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
public Optional<CompatibilityEntry> Compatibility { get; private set; }
public bool HasPlayabilityInfo => Compatibility.HasValue;
public string LocalizedStatus => Compatibility.Convert(x => x.LocalizedStatus);
public bool HasCompatibilityLabels => !FormattedCompatibilityLabels.Equals(string.Empty);
public string FormattedCompatibilityLabels
=> Compatibility.Convert(x => x.FormattedIssueLabels).OrElse(string.Empty);
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
public string LocalizedStatusTooltip =>
Compatibility.Convert(x =>
#pragma warning disable CS8509 // It is exhaustive for all possible values this can contain.
LocaleManager.Instance[x.Status 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,
}]
).OrElse(string.Empty);
[JsonIgnore] public string IdString => Id.ToString("x16"); [JsonIgnore] public string IdString => Id.ToString("x16");
[JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL; [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
@ -93,17 +102,17 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{ {
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
if (!System.IO.Path.Exists(titleFilePath)) if (!System.IO.Path.Exists(titleFilePath))
{ {
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist."); Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
return string.Empty; return string.Empty;
} }
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower(); string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();
if (extension is ".nsp" or ".xci") if (extension is ".nsp" or ".xci")

View File

@ -533,7 +533,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
data.Favorite = appMetadata.Favorite; data.Favorite = appMetadata.Favorite;
data.TimePlayed = appMetadata.TimePlayed; data.TimePlayed = appMetadata.TimePlayed;
data.LastPlayed = appMetadata.LastPlayed; data.LastPlayed = appMetadata.LastPlayed;
data.UserConfig = File.Exists(Program.GetDirGameUserConfig(data.IdBaseString, false, false)); // Just check user config data.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(data.IdBaseString, false, false)); // Just check user config
} }
data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();

View File

@ -45,7 +45,7 @@ namespace Ryujinx.Ava.Utilities
if (string.IsNullOrEmpty(contentPath)) if (string.IsNullOrEmpty(contentPath))
goto BadData; goto BadData;
appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) }; appData = new() { Name = Name, Id = ProgramId, Path = contentPath };
appControl = StructHelpers.CreateCustomNacpData(Name, Version); appControl = StructHelpers.CreateCustomNacpData(Name, Version);
return true; return true;

View File

@ -11,6 +11,7 @@ namespace Ryujinx.Ava.Utilities
public static bool? OverrideHardwareAcceleration { get; private set; } public static bool? OverrideHardwareAcceleration { get; private set; }
public static string OverrideGraphicsBackend { get; private set; } public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideBackendThreading { get; private set; } public static string OverrideBackendThreading { get; private set; }
public static string OverrideBackendThreadingAfterReboot { get; private set; }
public static string OverridePPTC { get; private set; } public static string OverridePPTC { get; private set; }
public static string OverrideMemoryManagerMode { get; private set; } public static string OverrideMemoryManagerMode { get; private set; }
public static string OverrideSystemRegion { get; private set; } public static string OverrideSystemRegion { get; private set; }
@ -23,10 +24,6 @@ namespace Ryujinx.Ava.Utilities
public static bool StartFullscreenArg { get; private set; } public static bool StartFullscreenArg { get; private set; }
public static bool HideAvailableUpdates { get; private set; } public static bool HideAvailableUpdates { get; private set; }
public static void ArgumentsClean()
{
}
public static void ParseArguments(string[] args) public static void ParseArguments(string[] args)
{ {
@ -99,6 +96,16 @@ namespace Ryujinx.Ava.Utilities
OverrideBackendThreading = args[++i]; OverrideBackendThreading = args[++i];
break; break;
case "--bt":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideBackendThreadingAfterReboot = args[++i];
break;
case "--pptc": case "--pptc":
if (i + 1 >= args.Length) if (i + 1 >= args.Length)
{ {

View File

@ -60,10 +60,21 @@ namespace Ryujinx.Ava.Utilities.Compat
} }
} }
public static CompatibilityEntry Find(string titleId)
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId));
public static CompatibilityEntry Find(ulong titleId)
=> Find(titleId.ToString("X16"));
public static LocaleKeys? GetStatus(string titleId) public static LocaleKeys? GetStatus(string titleId)
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status; => Find(titleId)?.Status;
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16")); public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
public static string GetLabels(string titleId)
=> Find(titleId)?.FormattedIssueLabels;
public static string GetLabels(ulong titleId) => GetLabels(titleId.ToString("X16"));
} }
public class CompatibilityEntry public class CompatibilityEntry

View File

@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 63; public const int CurrentVersion = 66;
/// <summary> /// <summary>
/// Version of the configuration file format /// Version of the configuration file format
@ -112,6 +112,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary> /// </summary>
public bool LoggingEnableFsAccessLog { get; set; } public bool LoggingEnableFsAccessLog { get; set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public bool LoggingEnableAvalonia { get; set; }
/// <summary> /// <summary>
/// Controls which log messages are written to the log targets /// Controls which log messages are written to the log targets
/// </summary> /// </summary>
@ -158,10 +163,15 @@ namespace Ryujinx.Ava.Utilities.Configuration
public bool EnableDiscordIntegration { get; set; } public bool EnableDiscordIntegration { get; set; }
/// <summary> /// <summary>
/// Checks for updates when Ryujinx starts when enabled /// DEPRECATED: Checks for updates when Ryujinx starts when enabled
/// </summary> /// </summary>
public bool CheckUpdatesOnStart { get; set; } public bool CheckUpdatesOnStart { get; set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public UpdaterType UpdateCheckerType { get; set; }
/// <summary> /// <summary>
/// Show "Confirm Exit" Dialog /// Show "Confirm Exit" Dialog
/// </summary> /// </summary>
@ -374,6 +384,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary> /// </summary>
public bool EnableMouse { get; set; } public bool EnableMouse { get; set; }
/// <summary>
/// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
/// </summary>
public bool DisableInputWhenOutOfFocus { get; set; }
/// <summary> /// <summary>
/// Hotkey Keyboard Bindings /// Hotkey Keyboard Bindings
/// </summary> /// </summary>

View File

@ -18,14 +18,14 @@ namespace Ryujinx.Ava.Utilities.Configuration
{ {
public partial class ConfigurationState public partial class ConfigurationState
{ {
public void Load(ConfigurationFileFormat cff, string configurationFilePath, string gameId="") public void Load(ConfigurationFileFormat cff, string configurationFilePath, string titleId = "")
{ {
bool configurationFileUpdated = false; bool configurationFileUpdated = false;
bool LoadSetting = true; bool shouldLoadFromFile = true;
if (!string.IsNullOrEmpty(gameId)) if (!string.IsNullOrEmpty(titleId))
{ {
LoadSetting = false; shouldLoadFromFile = false;
} }
if (cff.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) if (cff.Version is < 0 or > ConfigurationFileFormat.CurrentVersion)
@ -49,13 +49,15 @@ namespace Ryujinx.Ava.Utilities.Configuration
configurationFileUpdated = true; configurationFileUpdated = true;
} }
EnableDiscordIntegration.Value = LoadSetting ? cff.EnableDiscordIntegration : EnableDiscordIntegration.Value; // Get from global config only
CheckUpdatesOnStart.Value = LoadSetting ? cff.CheckUpdatesOnStart : CheckUpdatesOnStart.Value; // Get from global config only EnableDiscordIntegration.Value = shouldLoadFromFile ? cff.EnableDiscordIntegration : EnableDiscordIntegration.Value; // Get from global config only
ShowConfirmExit.Value = LoadSetting ? cff.ShowConfirmExit : ShowConfirmExit.Value; // Get from global config only CheckUpdatesOnStart.Value = shouldLoadFromFile ? cff.CheckUpdatesOnStart : CheckUpdatesOnStart.Value; // Get from global config only
RememberWindowState.Value = LoadSetting ? cff.RememberWindowState : RememberWindowState.Value; // Get from global config only UpdateCheckerType.Value = shouldLoadFromFile ? cff.UpdateCheckerType : UpdateCheckerType.Value; // Get from global config only
ShowTitleBar.Value = LoadSetting ? cff.ShowTitleBar : ShowTitleBar.Value; // Get from global config only ShowConfirmExit.Value = shouldLoadFromFile ? cff.ShowConfirmExit : ShowConfirmExit.Value; // Get from global config only
EnableHardwareAcceleration.Value = LoadSetting ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only RememberWindowState.Value = shouldLoadFromFile ? cff.RememberWindowState : RememberWindowState.Value; // Get from global config only
HideCursor.Value = LoadSetting ? cff.HideCursor : HideCursor.Value; // Get from global config only ShowTitleBar.Value = shouldLoadFromFile ? cff.ShowTitleBar : ShowTitleBar.Value; // Get from global config only
EnableHardwareAcceleration.Value = shouldLoadFromFile ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only
HideCursor.Value = shouldLoadFromFile ? cff.HideCursor : HideCursor.Value; // Get from global config only
Logger.EnableFileLog.Value = cff.EnableFileLog; Logger.EnableFileLog.Value = cff.EnableFileLog;
Logger.EnableDebug.Value = cff.LoggingEnableDebug; Logger.EnableDebug.Value = cff.LoggingEnableDebug;
@ -91,7 +93,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.Language.Value = cff.SystemLanguage; System.Language.Value = cff.SystemLanguage;
System.Region.Value = cff.SystemRegion; System.Region.Value = cff.SystemRegion;
System.TimeZone.Value = cff.SystemTimeZone; System.TimeZone.Value = cff.SystemTimeZone;
System.SystemTimeOffset.Value = LoadSetting ? cff.SystemTimeOffset : System.SystemTimeOffset.Value; // Get from global config only System.SystemTimeOffset.Value = shouldLoadFromFile ? cff.SystemTimeOffset : System.SystemTimeOffset.Value; // Get from global config only
System.EnableDockedMode.Value = cff.DockedMode; System.EnableDockedMode.Value = cff.DockedMode;
System.EnablePtc.Value = cff.EnablePtc; System.EnablePtc.Value = cff.EnablePtc;
System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc; System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc;
@ -106,46 +108,47 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.IgnoreApplet.Value = cff.IgnoreApplet; System.IgnoreApplet.Value = cff.IgnoreApplet;
System.UseHypervisor.Value = cff.UseHypervisor; System.UseHypervisor.Value = cff.UseHypervisor;
UI.GuiColumns.FavColumn.Value = LoadSetting ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value; UI.GuiColumns.FavColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value;
UI.GuiColumns.IconColumn.Value = LoadSetting ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value; UI.GuiColumns.IconColumn.Value = shouldLoadFromFile ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value;
UI.GuiColumns.AppColumn.Value = LoadSetting ? cff.GuiColumns.AppColumn : UI.GuiColumns.AppColumn.Value; UI.GuiColumns.AppColumn.Value = shouldLoadFromFile ? cff.GuiColumns.AppColumn : UI.GuiColumns.AppColumn.Value;
UI.GuiColumns.DevColumn.Value = LoadSetting ? cff.GuiColumns.DevColumn : UI.GuiColumns.DevColumn.Value; UI.GuiColumns.DevColumn.Value = shouldLoadFromFile ? cff.GuiColumns.DevColumn : UI.GuiColumns.DevColumn.Value;
UI.GuiColumns.VersionColumn.Value = LoadSetting ? cff.GuiColumns.VersionColumn : UI.GuiColumns.VersionColumn.Value; UI.GuiColumns.VersionColumn.Value = shouldLoadFromFile ? cff.GuiColumns.VersionColumn : UI.GuiColumns.VersionColumn.Value;
UI.GuiColumns.TimePlayedColumn.Value = LoadSetting ? cff.GuiColumns.TimePlayedColumn : UI.GuiColumns.TimePlayedColumn.Value; UI.GuiColumns.TimePlayedColumn.Value = shouldLoadFromFile ? cff.GuiColumns.TimePlayedColumn : UI.GuiColumns.TimePlayedColumn.Value;
UI.GuiColumns.LastPlayedColumn.Value = LoadSetting ? cff.GuiColumns.LastPlayedColumn : UI.GuiColumns.LastPlayedColumn.Value; UI.GuiColumns.LastPlayedColumn.Value = shouldLoadFromFile ? cff.GuiColumns.LastPlayedColumn : UI.GuiColumns.LastPlayedColumn.Value;
UI.GuiColumns.FileExtColumn.Value = LoadSetting ? cff.GuiColumns.FileExtColumn : UI.GuiColumns.FileExtColumn.Value; UI.GuiColumns.FileExtColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FileExtColumn : UI.GuiColumns.FileExtColumn.Value;
UI.GuiColumns.FileSizeColumn.Value = LoadSetting ? cff.GuiColumns.FileSizeColumn : UI.GuiColumns.FileSizeColumn.Value; UI.GuiColumns.FileSizeColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FileSizeColumn : UI.GuiColumns.FileSizeColumn.Value;
UI.GuiColumns.PathColumn.Value = LoadSetting ? cff.GuiColumns.PathColumn : UI.GuiColumns.PathColumn.Value; UI.GuiColumns.PathColumn.Value = shouldLoadFromFile ? cff.GuiColumns.PathColumn : UI.GuiColumns.PathColumn.Value;
UI.ColumnSort.SortColumnId.Value = LoadSetting ? cff.ColumnSort.SortColumnId : UI.ColumnSort.SortColumnId.Value; UI.ColumnSort.SortColumnId.Value = shouldLoadFromFile ? cff.ColumnSort.SortColumnId : UI.ColumnSort.SortColumnId.Value;
UI.ColumnSort.SortAscending.Value = LoadSetting ? cff.ColumnSort.SortAscending : UI.ColumnSort.SortAscending.Value; UI.ColumnSort.SortAscending.Value = shouldLoadFromFile ? cff.ColumnSort.SortAscending : UI.ColumnSort.SortAscending.Value;
UI.GameDirs.Value = LoadSetting ? cff.GameDirs : UI.GameDirs.Value; UI.GameDirs.Value = shouldLoadFromFile ? cff.GameDirs : UI.GameDirs.Value;
UI.AutoloadDirs.Value = LoadSetting ? (cff.AutoloadDirs ?? []) : UI.AutoloadDirs.Value; UI.AutoloadDirs.Value = shouldLoadFromFile ? (cff.AutoloadDirs ?? []) : UI.AutoloadDirs.Value;
UI.ShownFileTypes.NSP.Value = LoadSetting ? cff.ShownFileTypes.NSP : UI.ShownFileTypes.NSP.Value; UI.ShownFileTypes.NSP.Value = shouldLoadFromFile ? cff.ShownFileTypes.NSP : UI.ShownFileTypes.NSP.Value;
UI.ShownFileTypes.PFS0.Value = LoadSetting ? cff.ShownFileTypes.PFS0 : UI.ShownFileTypes.PFS0.Value; UI.ShownFileTypes.PFS0.Value = shouldLoadFromFile ? cff.ShownFileTypes.PFS0 : UI.ShownFileTypes.PFS0.Value;
UI.ShownFileTypes.XCI.Value = LoadSetting ? cff.ShownFileTypes.XCI : UI.ShownFileTypes.XCI.Value; UI.ShownFileTypes.XCI.Value = shouldLoadFromFile ? cff.ShownFileTypes.XCI : UI.ShownFileTypes.XCI.Value;
UI.ShownFileTypes.NCA.Value = LoadSetting ? cff.ShownFileTypes.NCA : UI.ShownFileTypes.NCA.Value; UI.ShownFileTypes.NCA.Value = shouldLoadFromFile ? cff.ShownFileTypes.NCA : UI.ShownFileTypes.NCA.Value;
UI.ShownFileTypes.NRO.Value = LoadSetting ? cff.ShownFileTypes.NRO : UI.ShownFileTypes.NRO.Value; UI.ShownFileTypes.NRO.Value = shouldLoadFromFile ? cff.ShownFileTypes.NRO : UI.ShownFileTypes.NRO.Value;
UI.ShownFileTypes.NSO.Value = LoadSetting ? cff.ShownFileTypes.NSO : UI.ShownFileTypes.NSO.Value; UI.ShownFileTypes.NSO.Value = shouldLoadFromFile ? cff.ShownFileTypes.NSO : UI.ShownFileTypes.NSO.Value;
UI.LanguageCode.Value = LoadSetting ? cff.LanguageCode : UI.LanguageCode.Value; UI.LanguageCode.Value = shouldLoadFromFile ? cff.LanguageCode : UI.LanguageCode.Value;
UI.BaseStyle.Value = LoadSetting ? cff.BaseStyle : UI.BaseStyle.Value; UI.BaseStyle.Value = shouldLoadFromFile ? cff.BaseStyle : UI.BaseStyle.Value;
UI.GameListViewMode.Value = LoadSetting ? cff.GameListViewMode : UI.GameListViewMode.Value; UI.GameListViewMode.Value = shouldLoadFromFile ? cff.GameListViewMode : UI.GameListViewMode.Value;
UI.ShowNames.Value = LoadSetting ? cff.ShowNames : UI.ShowNames.Value; UI.ShowNames.Value = shouldLoadFromFile ? cff.ShowNames : UI.ShowNames.Value;
UI.IsAscendingOrder.Value = LoadSetting ? cff.IsAscendingOrder : UI.IsAscendingOrder.Value; UI.IsAscendingOrder.Value = shouldLoadFromFile ? cff.IsAscendingOrder : UI.IsAscendingOrder.Value;
UI.GridSize.Value = LoadSetting ? cff.GridSize : UI.GridSize.Value; UI.GridSize.Value = shouldLoadFromFile ? cff.GridSize : UI.GridSize.Value;
UI.ApplicationSort.Value = LoadSetting ? cff.ApplicationSort : UI.ApplicationSort.Value; UI.ApplicationSort.Value = shouldLoadFromFile ? cff.ApplicationSort : UI.ApplicationSort.Value;
UI.StartFullscreen.Value = LoadSetting ? cff.StartFullscreen : UI.StartFullscreen.Value; UI.StartFullscreen.Value = shouldLoadFromFile ? cff.StartFullscreen : UI.StartFullscreen.Value;
UI.StartNoUI.Value = LoadSetting ? cff.StartNoUI : UI.StartNoUI.Value; UI.StartNoUI.Value = shouldLoadFromFile ? cff.StartNoUI : UI.StartNoUI.Value;
UI.ShowConsole.Value = LoadSetting ? cff.ShowConsole : UI.ShowConsole.Value; UI.ShowConsole.Value = shouldLoadFromFile ? cff.ShowConsole : UI.ShowConsole.Value;
UI.WindowStartup.WindowSizeWidth.Value = LoadSetting ? cff.WindowStartup.WindowSizeWidth : UI.WindowStartup.WindowSizeWidth.Value; UI.WindowStartup.WindowSizeWidth.Value = shouldLoadFromFile ? cff.WindowStartup.WindowSizeWidth : UI.WindowStartup.WindowSizeWidth.Value;
UI.WindowStartup.WindowSizeHeight.Value = LoadSetting ? cff.WindowStartup.WindowSizeHeight : UI.WindowStartup.WindowSizeHeight.Value; UI.WindowStartup.WindowSizeHeight.Value = shouldLoadFromFile ? cff.WindowStartup.WindowSizeHeight : UI.WindowStartup.WindowSizeHeight.Value;
UI.WindowStartup.WindowPositionX.Value = LoadSetting ? cff.WindowStartup.WindowPositionX : UI.WindowStartup.WindowPositionX.Value; UI.WindowStartup.WindowPositionX.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionX : UI.WindowStartup.WindowPositionX.Value;
UI.WindowStartup.WindowPositionY.Value = LoadSetting ? cff.WindowStartup.WindowPositionY : UI.WindowStartup.WindowPositionY.Value; UI.WindowStartup.WindowPositionY.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionY : UI.WindowStartup.WindowPositionY.Value;
UI.WindowStartup.WindowMaximized.Value = LoadSetting ? cff.WindowStartup.WindowMaximized : UI.WindowStartup.WindowMaximized.Value; UI.WindowStartup.WindowMaximized.Value = shouldLoadFromFile ? cff.WindowStartup.WindowMaximized : UI.WindowStartup.WindowMaximized.Value;
Hid.EnableKeyboard.Value = cff.EnableKeyboard; Hid.EnableKeyboard.Value = cff.EnableKeyboard;
Hid.EnableMouse.Value = cff.EnableMouse; Hid.EnableMouse.Value = cff.EnableMouse;
Hid.Hotkeys.Value = LoadSetting ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only Hid.DisableInputWhenOutOfFocus.Value = shouldLoadFromFile ? cff.DisableInputWhenOutOfFocus: Hid.DisableInputWhenOutOfFocus.Value; // Get from global config only
Hid.Hotkeys.Value = shouldLoadFromFile ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only
Hid.InputConfig.Value = cff.InputConfig ?? []; Hid.InputConfig.Value = cff.InputConfig ?? [];
Hid.RainbowSpeed.Value = cff.RainbowSpeed; Hid.RainbowSpeed.Value = cff.RainbowSpeed;
@ -437,7 +440,10 @@ namespace Ryujinx.Ava.Utilities.Configuration
} }
}), }),
(62, static cff => cff.RainbowSpeed = 1f), (62, static cff => cff.RainbowSpeed = 1f),
(63, static cff => cff.MatchSystemTime = false) (63, static cff => cff.MatchSystemTime = false),
(64, static cff => cff.LoggingEnableAvalonia = false),
(65, static cff => cff.UpdateCheckerType = cff.CheckUpdatesOnStart ? UpdaterType.PromptAtStartup : UpdaterType.Off),
(66, static cff => cff.DisableInputWhenOutOfFocus = false)
); );
} }
} }

View File

@ -1,6 +1,7 @@
using ARMeilleure; using ARMeilleure;
using Gommon; using Gommon;
using Ryujinx.Ava.Utilities.Configuration.System; using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
@ -255,6 +256,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary> /// </summary>
public ReactiveObject<bool> EnableFsAccessLog { get; private set; } public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public ReactiveObject<bool> EnableAvaloniaLog { get; private set; }
/// <summary> /// <summary>
/// Controls which log messages are written to the log targets /// Controls which log messages are written to the log targets
/// </summary> /// </summary>
@ -281,6 +287,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableTrace = new ReactiveObject<bool>(); EnableTrace = new ReactiveObject<bool>();
EnableGuest = new ReactiveObject<bool>(); EnableGuest = new ReactiveObject<bool>();
EnableFsAccessLog = new ReactiveObject<bool>(); EnableFsAccessLog = new ReactiveObject<bool>();
EnableAvaloniaLog = new ReactiveObject<bool>();
FilteredClasses = new ReactiveObject<LogClass[]>(); FilteredClasses = new ReactiveObject<LogClass[]>();
EnableFileLog = new ReactiveObject<bool>(); EnableFileLog = new ReactiveObject<bool>();
EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
@ -441,6 +448,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary> /// </summary>
public ReactiveObject<bool> EnableMouse { get; private set; } public ReactiveObject<bool> EnableMouse { get; private set; }
/// <summary>
/// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
/// </summary>
public ReactiveObject<bool> DisableInputWhenOutOfFocus { get; private set; }
/// <summary> /// <summary>
/// Hotkey Keyboard Bindings /// Hotkey Keyboard Bindings
/// </summary> /// </summary>
@ -462,6 +474,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
{ {
EnableKeyboard = new ReactiveObject<bool>(); EnableKeyboard = new ReactiveObject<bool>();
EnableMouse = new ReactiveObject<bool>(); EnableMouse = new ReactiveObject<bool>();
DisableInputWhenOutOfFocus = new ReactiveObject<bool>();
Hotkeys = new ReactiveObject<KeyboardHotkeys>(); Hotkeys = new ReactiveObject<KeyboardHotkeys>();
InputConfig = new ReactiveObject<List<InputConfig>>(); InputConfig = new ReactiveObject<List<InputConfig>>();
RainbowSpeed = new ReactiveObject<float>(); RainbowSpeed = new ReactiveObject<float>();
@ -762,6 +775,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary> /// </summary>
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; } public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
/// <summary> /// <summary>
/// Show "Confirm Exit" Dialog /// Show "Confirm Exit" Dialog
/// </summary> /// </summary>
@ -798,6 +816,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hacks = new HacksSection(); Hacks = new HacksSection();
EnableDiscordIntegration = new ReactiveObject<bool>(); EnableDiscordIntegration = new ReactiveObject<bool>();
CheckUpdatesOnStart = new ReactiveObject<bool>(); CheckUpdatesOnStart = new ReactiveObject<bool>();
UpdateCheckerType = new ReactiveObject<UpdaterType>();
ShowConfirmExit = new ReactiveObject<bool>(); ShowConfirmExit = new ReactiveObject<bool>();
RememberWindowState = new ReactiveObject<bool>(); RememberWindowState = new ReactiveObject<bool>();
ShowTitleBar = new ReactiveObject<bool>(); ShowTitleBar = new ReactiveObject<bool>();

View File

@ -46,6 +46,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
LoggingEnableTrace = Logger.EnableTrace, LoggingEnableTrace = Logger.EnableTrace,
LoggingEnableGuest = Logger.EnableGuest, LoggingEnableGuest = Logger.EnableGuest,
LoggingEnableFsAccessLog = Logger.EnableFsAccessLog, LoggingEnableFsAccessLog = Logger.EnableFsAccessLog,
LoggingEnableAvalonia = Logger.EnableAvaloniaLog,
LoggingFilteredClasses = Logger.FilteredClasses, LoggingFilteredClasses = Logger.FilteredClasses,
LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel, LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel,
SystemLanguage = System.Language, SystemLanguage = System.Language,
@ -55,6 +56,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
DockedMode = System.EnableDockedMode, DockedMode = System.EnableDockedMode,
EnableDiscordIntegration = EnableDiscordIntegration, EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart, CheckUpdatesOnStart = CheckUpdatesOnStart,
UpdateCheckerType = UpdateCheckerType,
ShowConfirmExit = ShowConfirmExit, ShowConfirmExit = ShowConfirmExit,
RememberWindowState = RememberWindowState, RememberWindowState = RememberWindowState,
ShowTitleBar = ShowTitleBar, ShowTitleBar = ShowTitleBar,
@ -129,6 +131,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
ShowConsole = UI.ShowConsole, ShowConsole = UI.ShowConsole,
EnableKeyboard = Hid.EnableKeyboard, EnableKeyboard = Hid.EnableKeyboard,
EnableMouse = Hid.EnableMouse, EnableMouse = Hid.EnableMouse,
DisableInputWhenOutOfFocus = Hid.DisableInputWhenOutOfFocus,
Hotkeys = Hid.Hotkeys, Hotkeys = Hid.Hotkeys,
InputConfig = Hid.InputConfig, InputConfig = Hid.InputConfig,
RainbowSpeed = Hid.RainbowSpeed, RainbowSpeed = Hid.RainbowSpeed,
@ -165,6 +168,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Logger.EnableTrace.Value = false; Logger.EnableTrace.Value = false;
Logger.EnableGuest.Value = true; Logger.EnableGuest.Value = true;
Logger.EnableFsAccessLog.Value = false; Logger.EnableFsAccessLog.Value = false;
Logger.EnableAvaloniaLog.Value = false;
Logger.FilteredClasses.Value = []; Logger.FilteredClasses.Value = [];
Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None;
System.Language.Value = Language.AmericanEnglish; System.Language.Value = Language.AmericanEnglish;
@ -173,7 +177,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.SystemTimeOffset.Value = 0; System.SystemTimeOffset.Value = 0;
System.EnableDockedMode.Value = true; System.EnableDockedMode.Value = true;
EnableDiscordIntegration.Value = true; EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true; UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
ShowConfirmExit.Value = true; ShowConfirmExit.Value = true;
RememberWindowState.Value = true; RememberWindowState.Value = true;
ShowTitleBar.Value = !OperatingSystem.IsWindows(); ShowTitleBar.Value = !OperatingSystem.IsWindows();
@ -242,6 +246,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
UI.WindowStartup.WindowMaximized.Value = false; UI.WindowStartup.WindowMaximized.Value = false;
Hid.EnableKeyboard.Value = false; Hid.EnableKeyboard.Value = false;
Hid.EnableMouse.Value = false; Hid.EnableMouse.Value = false;
Hid.DisableInputWhenOutOfFocus.Value = false;
Hid.Hotkeys.Value = new KeyboardHotkeys Hid.Hotkeys.Value = new KeyboardHotkeys
{ {
ToggleVSyncMode = Key.F1, ToggleVSyncMode = Key.F1,

View File

@ -0,0 +1,13 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.Configuration.UI
{
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))]
public enum UpdaterType
{
Off,
PromptAtStartup,
CheckInBackground
}
}

View File

@ -1,5 +1,4 @@
using Gommon; using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,7 +1,6 @@
using MsgPack; using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport namespace Ryujinx.Ava.Utilities.PlayReport
{ {

View File

@ -1,4 +1,5 @@
using System; using Gommon;
using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -281,9 +282,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList(); players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList();
return players.Count > 4 return players.Count > 4
? $"{players.Count} Players - " + string.Join(", ", ? $"{players.Count} Players - {
players.Take(3).Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")) players.Take(3)
: string.Join(", ", players.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")); .Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
.JoinToString(", ")
}"
: players
.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
.JoinToString(", ");
string RankMedal(int? rank) => rank switch string RankMedal(int? rank) => rank switch
{ {

View File

@ -1,9 +1,4 @@
using System; namespace Ryujinx.Ava.Utilities.PlayReport
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{ {
public static partial class PlayReports public static partial class PlayReports
{ {

View File

@ -1,5 +1,4 @@
using FluentAvalonia.Core; using MsgPack;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -153,7 +152,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{ {
List<MessagePackObject> packedObjects = []; List<MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys) foreach (string reportKey in ReportKeys)
{ {
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
{ {
@ -177,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{ {
Dictionary<string, MessagePackObject> packedObjects = []; Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys) foreach (string reportKey in ReportKeys)
{ {
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue; continue;