## Usage
diff --git a/docs/compatibility.csv b/docs/compatibility.csv
index 53ad389b6..7653ced9e 100644
--- a/docs/compatibility.csv
+++ b/docs/compatibility.csv
@@ -332,6 +332,7 @@
0100E680149DC000,"Arcaea",,playable,2023-03-16 19:31:21
01003C2010C78000,"Archaica: The Path Of Light",crash,nothing,2020-10-16 13:22:26
01004DA012976000,"Area 86",,playable,2020-12-16 16:45:52
+01008d8006a6a000,"Arena of Valor",crash,boots,2025-02-03 22:19:34
0100691013C46000,"ARIA CHRONICLE",,playable,2022-11-16 13:50:55
0100D4A00B284000,"ARK: Survival Evolved",gpu;nvdec;online-broken;UE4;ldn-untested,ingame,2024-04-16 00:53:56
0100C56012C96000,"Arkanoid vs. Space Invaders",services,ingame,2021-01-21 12:50:30
@@ -426,6 +427,7 @@
0100E48013A34000,"Balan Wonderworld Demo",gpu;services;UE4;demo,ingame,2023-02-16 20:05:07
0100CD801CE5E000,"Balatro",,ingame,2024-04-21 02:01:53
010010A00DA48000,"Baldur's Gate and Baldur's Gate II: Enhanced Editions",32-bit,playable,2022-09-12 23:52:15
+0100fd1014726000,"Baldur's Gate: Dark Alliance",ldn-untested,ingame,2025-02-03 22:21:00
0100BC400FB64000,"Balthazar's Dream",,playable,2022-09-13 00:13:22
01008D30128E0000,"Bamerang",,playable,2022-10-26 00:29:39
010013C010C5C000,"Banner of the Maid",,playable,2021-06-14 15:23:37
@@ -528,6 +530,7 @@
01005950022EC000,"Blade Strangers",nvdec,playable,2022-07-17 19:02:43
0100DF0011A6A000,"Bladed Fury",,playable,2022-10-26 11:36:26
0100CFA00CC74000,"Blades of Time",deadlock;online,boots,2022-07-17 19:19:58
+01003d700dd8a000,"Blades",,boots,2025-02-03 22:22:00
01006CC01182C000,"Blair Witch",nvdec;UE4,playable,2022-10-01 14:06:16
010039501405E000,"Blanc",gpu;slow,ingame,2023-02-22 14:00:13
0100698009C6E000,"Blasphemous",nvdec,playable,2021-03-01 12:15:31
@@ -955,7 +958,7 @@
010012800EBAE000,"Disney TSUM TSUM FESTIVAL",crash,menus,2020-07-14 14:05:28
01009740120FE000,"DISTRAINT 2",,playable,2020-09-03 16:08:12
010075B004DD2000,"DISTRAINT: Deluxe Edition",,playable,2020-06-15 23:42:24
-010027400CDC6000,"Divinity: Original Sin 2 - Definitive Edition",services;crash;online-broken;regression,menus,2023-08-13 17:20:03
+010027400CDC6000,"Divinity: Original Sin 2 - Definitive Edition",services;crash;online-broken;regression,ingame,2025-02-03 22:12:30
01001770115C8000,"Dodo Peak",nvdec;UE4,playable,2022-10-04 16:13:05
010077B0100DA000,"Dogurai",,playable,2020-10-04 02:40:16
010048100D51A000,"Dokapon Up! Mugen no Roulette",gpu;Needs Update,menus,2022-12-08 19:39:10
@@ -1246,7 +1249,7 @@
0100A6B00D4EC000,"Furwind",,playable,2021-02-19 19:44:08
0100ECE00C0C4000,"Fury Unleashed",crash;services,ingame,2020-10-18 11:52:40
010070000ED9E000,"Fury Unleashed Demo",,playable,2020-10-08 20:09:21
-0100E1F013674000,"FUSER™",nvdec;UE4,playable,2022-10-17 20:58:32
+0100E1F013674000,"FUSER™",nvdec;UE4;slow;gpu,ingame,2025-02-12 16:03:00
0100A7A015E4C000,"Fushigi no Gensokyo Lotus Labyrinth",Needs Update;audio;gpu;nvdec,ingame,2021-01-20 15:30:02
01003C300B274000,"Futari de! Nyanko Daisensou",,playable,2024-01-05 22:26:52
010055801134E000,"FUZE Player",online-broken;vulkan-backend-bug,ingame,2022-10-18 12:23:53
@@ -1654,7 +1657,7 @@
0100A73006E74000,"Legendary Eleven",,playable,2021-06-08 12:09:03
0100A7700B46C000,"Legendary Fishing",online,playable,2021-04-14 15:08:46
0100739018020000,"LEGO® 2K Drive",gpu;ldn-works,ingame,2024-04-09 02:05:12
-01003A30012C0000,"LEGO® CITY Undercover",nvdec,playable,2024-09-30 08:44:27
+010085500130a000,"LEGO® CITY Undercover",nvdec,playable,2024-09-30 08:44:27
010070D009FEC000,"LEGO® DC Super-Villains",,playable,2021-05-27 18:10:37
010052A00B5D2000,"LEGO® Harry Potter™ Collection",crash,ingame,2024-01-31 10:28:07
010073C01AF34000,"LEGO® Horizon Adventures™",vulkan-backend-bug;opengl-backend-bug;UE4,ingame,2025-01-07 04:24:56
@@ -1913,6 +1916,7 @@
010073E008E6E000,"Mugsters",,playable,2021-01-28 17:57:17
0100A8400471A000,"MUJO",,playable,2020-05-08 16:31:04
0100211005E94000,"Mulaka",,playable,2021-01-28 18:07:20
+01008e2013fb4000,"Multi Quiz",ldn-untested,ingame,2025-02-03 22:26:00
010038B00B9AE000,"Mummy Pinball",,playable,2022-08-05 16:08:11
01008E200C5C2000,"Muse Dash",,playable,2020-06-06 14:41:29
010035901046C000,"Mushroom Quest",,playable,2020-05-17 13:07:08
@@ -2028,6 +2032,7 @@
010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
+0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00
0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06
0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07
01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11
@@ -2058,7 +2063,7 @@
010002700C34C000,"Numbala",,playable,2020-05-11 12:01:07
010020500C8C8000,"Number Place 10000",gpu,menus,2021-11-24 09:14:23
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
010049F00EC30000,"Nyan Cat: Lost in Space",online,playable,2021-06-12 13:22:03
01002E6014FC4000,"O---O",,playable,2022-10-29 12:12:14
@@ -2466,7 +2471,7 @@
0100AFE00DDAC000,"Royal Roads",,playable,2020-11-17 12:54:38
0100E2C00B414000,"RPG Maker MV",nvdec,playable,2021-01-05 20:12:01
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
01006EC00F2CC000,"RUINER",UE4,playable,2022-10-03 14:11:33
010074F00DE4A000,"Run the Fan",,playable,2021-02-27 13:36:28
@@ -2475,6 +2480,7 @@
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
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
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
@@ -2532,7 +2538,7 @@
0100C3E00B700000,"SEGA AGES Space Harrier",,playable,2021-01-11 12:57:40
010054400D2E6000,"SEGA AGES Virtua Racing",online-broken,playable,2023-01-29 17:08:39
01001E700AC60000,"SEGA AGES Wonder Boy: Monster Land",online,playable,2021-05-05 16:28:25
-0100B3C014BDA000,"SEGA Genesis™ – Nintendo Switch Online",crash;regression,nothing,2022-04-11 07:27:21
+0100B3C014BDA000,"SEGA Genesis™ – Nintendo Switch Online",crash;regression,ingame,2025-02-03 22:13:30
0100F7300B24E000,"SEGA Mega Drive Classics",online,playable,2021-01-05 11:08:00
01009840046BC000,"Semispheres",,playable,2021-01-06 23:08:31
0100D1800D902000,"SENRAN KAGURA Peach Ball",,playable,2021-06-03 15:12:10
@@ -2668,10 +2674,10 @@
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
010031D00A604000,"Songbringer",,playable,2020-06-22 10:42:02
-0000000000000000,"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
-0000000000000000,"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 1 (2013)",crash;homebrew,ingame,2024-04-06 18:31:20
+,"Sonic 2 (2013)",crash;homebrew,ingame,2024-04-01 16:25:30
+,"Sonic A.I.R",homebrew,ingame,2024-04-01 16:25:32
+,"Sonic CD",crash;homebrew,ingame,2024-04-01 16:25:31
010040E0116B8000,"Sonic Colors: Ultimate",,playable,2022-11-12 21:24:26
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
@@ -2688,7 +2694,7 @@
0100707011722000,"Space Elite Force",,playable,2020-11-27 15:21:05
010047B010260000,"Space Pioneer",,playable,2022-10-20 12:24:37
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
010034800FB60000,"Spaceland",,playable,2020-11-01 14:31:56
010028D0045CE000,"Sparkle 2",,playable,2020-10-19 11:51:39
@@ -2833,7 +2839,7 @@
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
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
010028600EBDA000,"Super Mario™ 3D World + Bowser’s Fury",ldn-works,playable,2024-07-31 10:45:37
01004F8006A78000,"Super Meat Boy",services,playable,2020-04-02 23:10:07
@@ -2964,6 +2970,7 @@
0100C38004DCC000,"The Flame In The Flood: Complete Edition",gpu;nvdec;UE4,ingame,2022-08-22 16:23:49
010007700D4AC000,"The Forbidden Arts",,playable,2021-01-26 16:26:24
010030700CBBC000,"The friends of Ringo Ishikawa",,playable,2022-08-22 16:33:17
+0100b620139d8000,"The Game of Life 2",ldn-untested,ingame,2025-02-03 22:30:00
01006350148DA000,"The Gardener and the Wild Vines",gpu,ingame,2024-04-29 16:32:10
0100B13007A6A000,"The Gardens Between",,playable,2021-01-29 16:16:53
010036E00FB20000,"The Great Ace Attorney Chronicles",,playable,2023-06-22 21:26:29
@@ -2981,6 +2988,8 @@
010015D003EE4000,"The Jackbox Party Pack 2",online-working,playable,2022-08-22 18:23:40
0100CC80013D6000,"The Jackbox Party Pack 3",slow;online-working,playable,2022-08-22 18:41:06
0100E1F003EE8000,"The Jackbox Party Pack 4",online-working,playable,2022-08-22 18:56:34
+01006fe0096ac000,"The Jackbox Party Pack 5",slow;online-working,ingame,2025-02-14 05:32:00
+01005a400db52000,"The Jackbox Party Pack 6",slow;online-working,ingame,2025-02-14 05:26:00
010052C00B184000,"The Journey Down: Chapter One",nvdec,playable,2021-02-24 13:32:41
01006BC00B188000,"The Journey Down: Chapter Three",nvdec,playable,2021-02-24 13:45:27
01009AB00B186000,"The Journey Down: Chapter Two",nvdec,playable,2021-02-24 13:32:13
@@ -3159,6 +3168,7 @@
010055E00CA68000,"Trine 4: The Nightmare Prince",gpu,nothing,2025-01-07 05:47:46
0100D9000A930000,"Trine Enchanted Edition",ldn-untested;nvdec,playable,2021-06-03 11:28:15
01002D7010A54000,"Trinity Trigger",crash,ingame,2023-03-03 03:09:09
+010020700a5e0000,"TRIVIAL PURSUIT Live!",ldn-untested,ingame,2025-02-03 22:35:00
0100868013FFC000,"TRIVIAL PURSUIT Live! 2",,boots,2022-12-19 00:04:33
0100F78002040000,"Troll and I™",gpu;nvdec,ingame,2021-06-04 16:58:50
0100145011008000,"Trollhunters: Defenders of Arcadia",gpu;nvdec,ingame,2020-11-30 13:27:09
@@ -3208,6 +3218,7 @@
0100AB2010B4C000,"Unlock The King",,playable,2020-09-01 13:58:27
0100A3E011CB0000,"Unlock the King 2",,playable,2021-06-15 20:43:55
01005AA00372A000,"UNO® for Nintendo Switch",nvdec;ldn-untested,playable,2022-07-28 14:49:47
+0100b6e012ebe000,"UNO",ldn-untested,ingame,2025-02-03 22:40:00
0100E5D00CC0C000,"Unravel Two",nvdec,playable,2024-05-23 15:45:05
010001300CC4A000,"Unruly Heroes",,playable,2021-01-07 18:09:31
0100B410138C0000,"Unspottable",,playable,2022-10-25 19:28:49
@@ -3372,6 +3383,7 @@
0100F47016F26000,"Yomawari 3",,playable,2022-05-10 08:26:51
010012F00B6F2000,"Yomawari: The Long Night Collection",,playable,2022-09-03 14:36:59
0100CC600ABB2000,"Yonder: The Cloud Catcher Chronicles (Retail Only)",,playable,2021-01-28 14:06:25
+0100534009ff2000,"Yonder: The Cloud Catcher Chronicles",,playable,2025-02-03 22:19:13
0100BE50042F6000,"Yono and the Celestial Elephants",,playable,2021-01-28 18:23:58
0100F110029C8000,"Yooka-Laylee",,playable,2021-01-28 14:21:45
010022F00DA66000,"Yooka-Laylee and the Impossible Lair",,playable,2021-03-05 17:32:21
diff --git a/src/ARMeilleure/Memory/ReservedRegion.cs b/src/ARMeilleure/Memory/ReservedRegion.cs
index a3ebd610d..dfe17c933 100644
--- a/src/ARMeilleure/Memory/ReservedRegion.cs
+++ b/src/ARMeilleure/Memory/ReservedRegion.cs
@@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
public IJitMemoryBlock Block { get; }
+ public IJitMemoryAllocator Allocator { get; }
public nint Pointer => Block.Pointer;
@@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity;
}
+ Allocator = allocator;
Block = allocator.Reserve(maxSize);
_maxSize = maxSize;
_sizeGranularity = granularity;
diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs
index d7e8201d8..bc75e7d3f 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -2,6 +2,8 @@ using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory;
using ARMeilleure.Native;
+using Humanizer;
+using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
@@ -18,9 +20,8 @@ namespace ARMeilleure.Translation.Cache
private static readonly int _pageMask = _pageSize - 1;
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 CacheMemoryAllocator _cacheAllocator;
@@ -30,6 +31,9 @@ namespace ARMeilleure.Translation.Cache
private static readonly Lock _lock = new();
private static bool _initialized;
+ private static readonly List _jitRegions = new();
+ private static int _activeRegionIndex = 0;
+
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
@@ -48,7 +52,9 @@ namespace ARMeilleure.Translation.Cache
return;
}
- _jitRegion = new ReservedRegion(allocator, CacheSize);
+ ReservedRegion firstRegion = new(allocator, CacheSize);
+ _jitRegions.Add(firstRegion);
+ _activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
@@ -59,7 +65,9 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows())
{
- JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
+ JitUnwindWindows.InstallFunctionTableHandler(
+ firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
+ );
}
_initialized = true;
@@ -75,8 +83,8 @@ namespace ARMeilleure.Translation.Cache
Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length);
-
- nint funcPtr = _jitRegion.Pointer + funcOffset;
+ ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
+ nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -90,9 +98,9 @@ namespace ARMeilleure.Translation.Cache
}
else
{
- ReprotectAsWritable(funcOffset, code.Length);
+ ReprotectAsWritable(targetRegion, funcOffset, 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)
{
@@ -116,52 +124,83 @@ namespace ARMeilleure.Translation.Cache
{
Debug.Assert(_initialized);
- int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
-
- if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
+ foreach (ReservedRegion region in _jitRegions)
{
- _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
- _cacheEntries.RemoveAt(entryIndex);
+ if (pointer.ToInt64() < region.Pointer.ToInt64() ||
+ 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 regionStart = offset & ~_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 regionStart = offset & ~_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)
{
codeSize = AlignCodeSize(codeSize);
- int allocOffset = _cacheAllocator.Allocate(codeSize);
-
- if (allocOffset < 0)
+ for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
{
- 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;
+
+ int newRegionNumber = _activeRegionIndex;
- return allocOffset;
+ 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)
{
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
@@ -185,18 +224,21 @@ namespace ARMeilleure.Translation.Cache
{
lock (_lock)
{
- int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
-
- if (index < 0)
+ foreach (ReservedRegion _ in _jitRegions)
{
- index = ~index - 1;
- }
+ int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
- if (index >= 0)
- {
- entry = _cacheEntries[index];
- entryIndex = index;
- return true;
+ if (index < 0)
+ {
+ index = ~index - 1;
+ }
+
+ if (index >= 0)
+ {
+ entry = _cacheEntries[index];
+ entryIndex = index;
+ return true;
+ }
}
}
diff --git a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs
index de0b78dbe..f3aaa58ff 100644
--- a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs
+++ b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs
@@ -144,17 +144,15 @@ namespace ARMeilleure.Translation.PTC
public List GetBlacklistedFunctions()
{
- List funcs = new List();
+ List funcs = [];
- foreach (var profiledFunc in ProfiledFuncs)
+ foreach ((ulong ptr, FuncProfile funcProfile) in ProfiledFuncs)
{
- if (profiledFunc.Value.Blacklist)
- {
- if (!funcs.Contains(profiledFunc.Key))
- {
- funcs.Add(profiledFunc.Key);
- }
- }
+ if (!funcProfile.Blacklist)
+ continue;
+
+ if (!funcs.Contains(ptr))
+ funcs.Add(ptr);
}
return funcs;
diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs
index 42322c8a2..76d873f60 100644
--- a/src/Ryujinx.Common/TitleIDs.cs
+++ b/src/Ryujinx.Common/TitleIDs.cs
@@ -164,15 +164,16 @@ namespace Ryujinx.Common
"0100ba0018500000", // Splatoon 3: Splatfest World Premiere
//NSO Membership games
- "0100ccf019c8c000", // F-ZERO 99
"0100c62011050000", // GB - Nintendo Switch Online
"010012f017576000", // GBA - Nintendo Switch Online
"0100c9a00ece6000", // N64 - Nintendo Switch Online
"0100e0601c632000", // N64 - Nintendo Switch Online 18+
"0100d870045b6000", // NES - Nintendo Switch Online
+ "0100b3c014bda000", // SEGA Genesis - Nintendo Switch Online
+ "01008d300c50c000", // SNES - Nintendo Switch Online
+ "0100ccf019c8c000", // F-ZERO 99
"0100ad9012510000", // PAC-MAN 99
"010040600c5ce000", // Tetris 99
- "01008d300c50c000", // SNES - Nintendo Switch Online
"0100277011f1a000", // Super Mario Bros. 35
//Misc Nintendo 1st party games
@@ -218,6 +219,7 @@ namespace Ryujinx.Common
//Misc Games
"010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance
+ "01008c2019598000", // Bluey: The Video Game
"0100c6800b934000", // Brawlhalla
"0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
index 5849401ab..ccf8ad964 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
@@ -1,4 +1,6 @@
using ARMeilleure.Memory;
+using Humanizer;
+using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
@@ -15,9 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly int _pageMask = _pageSize - 1;
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 CacheMemoryAllocator _cacheAllocator;
@@ -26,6 +27,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly Lock _lock = new();
private static bool _initialized;
+ private static readonly List _jitRegions = new();
+ private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
@@ -45,7 +48,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return;
}
- _jitRegion = new ReservedRegion(allocator, CacheSize);
+ ReservedRegion firstRegion = new(allocator, CacheSize);
+ _jitRegions.Add(firstRegion);
+ _activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
@@ -65,8 +70,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length);
-
- nint funcPtr = _jitRegion.Pointer + funcOffset;
+ ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
+ nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -80,18 +85,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
else
{
- ReprotectAsWritable(funcOffset, code.Length);
- code.CopyTo(new Span((void*)funcPtr, code.Length));
- ReprotectAsExecutable(funcOffset, code.Length);
+ ReprotectAsWritable(targetRegion, funcOffset, code.Length);
+ Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
+ ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
- if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
- {
- FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
- }
- else
- {
- _jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
- }
+ _jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
Add(funcOffset, code.Length);
@@ -106,50 +104,80 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
Debug.Assert(_initialized);
- int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
-
- if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
+ foreach (ReservedRegion region in _jitRegions)
{
- _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
- _cacheEntries.RemoveAt(entryIndex);
+ if (pointer.ToInt64() < region.Pointer.ToInt64() ||
+ 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 regionStart = offset & ~_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 regionStart = offset & ~_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)
{
codeSize = AlignCodeSize(codeSize);
- int allocOffset = _cacheAllocator.Allocate(codeSize);
-
- if (allocOffset < 0)
+ for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
{
- 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;
+
+ int newRegionNumber = _activeRegionIndex;
- return allocOffset;
+ 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)
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
index 1bbf70182..65d297c28 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
@@ -12,7 +12,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
private const int CodeAlignment = 4; // Bytes.
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
// and allow the guest to take the fast path.
diff --git a/src/Ryujinx.Graphics.Vulkan/Vendor.cs b/src/Ryujinx.Graphics.Vulkan/Vendor.cs
index 87c6407cd..550ba0221 100644
--- a/src/Ryujinx.Graphics.Vulkan/Vendor.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Vendor.cs
@@ -1,5 +1,4 @@
using Silk.NET.Vulkan;
-using System.Text.RegularExpressions;
namespace Ryujinx.Graphics.Vulkan
{
diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs
index b41bc60b1..47bfadc4c 100644
--- a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs
+++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs
@@ -15,7 +15,6 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
-using System.Text.RegularExpressions;
namespace Ryujinx.HLE.HOS.Applets.Error
{
@@ -159,13 +158,15 @@ namespace Ryujinx.HLE.HOS.Applets.Error
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)
{
message = GetMessageText(module, description, "FlvMsg");
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);
}
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs
index 3db2712f3..018bb8f14 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/WinSockHelper.cs
@@ -150,6 +150,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{ BsdSocketOption.SoLinger, SocketOptionName.Linger },
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
+ { BsdSocketOption.SoNoSigpipe, SocketOptionName.DontLinger },
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },
diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs
index 8ccb5cf89..3748eef39 100644
--- a/src/Ryujinx.HLE/UI/IHostUIHandler.cs
+++ b/src/Ryujinx.HLE/UI/IHostUIHandler.cs
@@ -45,10 +45,12 @@ namespace Ryujinx.HLE.UI
/// The value associated to the .
void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value);
+ ///
/// Displays a Message Dialog box specific to Error Applet and blocks until it is closed.
///
/// False when OK is pressed, True when another button (Details) is pressed.
- 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);
///
/// Creates a handler to process keyboard inputs into text strings.
diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs
index 15689f0c8..a936a5a2d 100644
--- a/src/Ryujinx.Horizon/HorizonStatic.cs
+++ b/src/Ryujinx.Horizon/HorizonStatic.cs
@@ -1,5 +1,5 @@
-using MsgPack;
using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Memory;
using System;
using System.Threading;
@@ -8,7 +8,7 @@ namespace Ryujinx.Horizon
{
public static class HorizonStatic
{
- internal static void HandlePlayReport(MessagePackObject report) =>
+ internal static void HandlePlayReport(PlayReport report) =>
new Thread(() => PlayReport?.Invoke(report))
{
Name = "HLE.PlayReportEvent",
@@ -16,7 +16,7 @@ namespace Ryujinx.Horizon
Priority = ThreadPriority.AboveNormal
}.Start();
- public static event Action PlayReport;
+ public static event Action PlayReport;
[field: ThreadStatic]
public static HorizonOptions Options { get; private set; }
diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
index 2f8657e0b..0ca851e6e 100644
--- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
+++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
@@ -1,4 +1,3 @@
-using Gommon;
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
@@ -12,19 +11,12 @@ using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;
using System.Text;
-using System.Threading;
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
namespace Ryujinx.Horizon.Prepo.Ipc
{
partial class PrepoService : IPrepoService
{
- enum PlayReportKind
- {
- Normal,
- System,
- }
-
private readonly ArpApi _arp;
private readonly PrepoServicePermissionLevel _permissionLevel;
private ulong _systemSessionId;
@@ -196,10 +188,17 @@ namespace Ryujinx.Horizon.Prepo.Ipc
{
return PrepoResult.InvalidBufferSize;
}
-
+
StringBuilder builder = new();
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray());
+ PlayReport playReport = new()
+ {
+ Kind = playReportKind,
+ Room = gameRoom,
+ ReportData = deserializedReport
+ };
+
builder.AppendLine();
builder.AppendLine("PlayReport log:");
builder.AppendLine($" Kind: {playReportKind}");
@@ -209,10 +208,12 @@ namespace Ryujinx.Horizon.Prepo.Ipc
if (pid != 0)
{
builder.AppendLine($" Pid: {pid}");
+ playReport.Pid = pid;
}
else
{
builder.AppendLine($" ApplicationId: {applicationId}");
+ playReport.AppId = applicationId;
}
Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid);
@@ -223,17 +224,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
_arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure();
+ playReport.Version = applicationLaunchProperty.Version;
+
builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}");
if (!userId.IsNull)
{
builder.AppendLine($" UserId: {userId}");
+ playReport.UserId = userId;
}
builder.AppendLine($" Room: {gameRoom}");
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
- HorizonStatic.HandlePlayReport(deserializedReport);
+ HorizonStatic.HandlePlayReport(playReport);
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
diff --git a/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs b/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs
new file mode 100644
index 000000000..e896219d5
--- /dev/null
+++ b/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs
@@ -0,0 +1,24 @@
+using MsgPack;
+using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.Ncm;
+
+namespace Ryujinx.Horizon.Prepo.Types
+{
+ public struct PlayReport
+ {
+ public PlayReportKind Kind { get; init; }
+ public string Room { get; init; }
+ public MessagePackObject ReportData { get; init; }
+
+ public ApplicationId? AppId;
+ public ulong? Pid;
+ public uint Version;
+ public Uid? UserId;
+ }
+
+ public enum PlayReportKind
+ {
+ Normal,
+ System,
+ }
+}
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index 4a54b7ead..21219d91b 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -185,6 +185,15 @@ namespace Ryujinx.Input.HLE
}
}
+ public bool InputUpdatesBlocked
+ {
+ get
+ {
+ lock (_lock)
+ return _blockInputUpdates;
+ }
+ }
+
public void BlockInputUpdates()
{
lock (_lock)
diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs
index 25f451858..b26921e6a 100644
--- a/src/Ryujinx/AppHost.cs
+++ b/src/Ryujinx/AppHost.cs
@@ -517,7 +517,7 @@ namespace Ryujinx.Ava
Device?.System.ChangeDockedModeState(e.NewValue);
}
- private void UpdateAudioVolumeState(object sender, ReactiveEventArgs e)
+ public void UpdateAudioVolumeState(object sender, ReactiveEventArgs e)
{
Device?.SetVolume(e.NewValue);
@@ -1041,6 +1041,7 @@ namespace Ryujinx.Ava
if (_viewModel.StartGamesInFullscreen)
{
_viewModel.WindowState = WindowState.FullScreen;
+ _viewModel.Window.TitleBar.ExtendsContentIntoTitleBar = true;
}
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI)
diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json
index 9f9053ae0..684051368 100644
--- a/src/Ryujinx/Assets/locales.json
+++ b/src/Ryujinx/Assets/locales.json
@@ -76,7 +76,7 @@
"ID": "MenuBarFileOpenAppletOpenMiiApplet",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Mii-Bearbeitungsapplet",
"el_GR": "",
"en_US": "Mii Edit Applet",
"es_ES": "Applet Editor Mii",
@@ -87,7 +87,7 @@
"ko_KR": "Mii 편집 애플릿",
"no_NO": "Mii-redigeringsapplet",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Applet Editor de Mii",
"ru_RU": "Апплет Mii Editor",
"sv_SE": "Redigera Mii-applet",
"th_TH": "",
@@ -176,7 +176,7 @@
"ID": "SettingsTabSystemMemoryManagerModeSoftware",
"Translations": {
"ar_SA": "البرنامج",
- "de_DE": "",
+ "de_DE": "Programme",
"el_GR": "Λογισμικό",
"en_US": "Software",
"es_ES": "",
@@ -326,7 +326,7 @@
"ID": "MenuBarFileOpenFromFileError",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Keine Anwendungen im ausgewählten Datei gefunden.",
"el_GR": "",
"en_US": "No applications found in selected file.",
"es_ES": "No se encontraron aplicaciones en el archivo seleccionado.",
@@ -376,7 +376,7 @@
"ID": "MenuBarFileLoadDlcFromFolder",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "DLC aus Ordner laden",
"el_GR": "",
"en_US": "Load DLC From Folder",
"es_ES": "Cargar DLC Desde Carpeta",
@@ -401,7 +401,7 @@
"ID": "MenuBarFileLoadTitleUpdatesFromFolder",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Titel-Updates aus Ordner laden",
"el_GR": "",
"en_US": "Load Title Updates From Folder",
"es_ES": "Cargar Actualizaciones de Títulos Desde Carpeta",
@@ -576,7 +576,7 @@
"ID": "MenuBarOptionsStartGamesWithoutUI",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Spiele ohne Benutzeroberfläche starten",
"el_GR": "",
"en_US": "Start Games with UI Hidden",
"es_ES": "",
@@ -584,12 +584,12 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "UI를 숨긴 상태에서 게임 시작",
+ "no_NO": "Start Spillet med UI Gjemt",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Iniciar jogos ocultando a interface",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "Starta spel med dolt användargränssnitt",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
@@ -751,7 +751,7 @@
"ID": "MenuBarActionsScanAmiiboBin",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Amiibo scannen (aus Bin-Datei)",
"el_GR": "",
"en_US": "Scan An Amiibo (From Bin)",
"es_ES": "",
@@ -762,7 +762,7 @@
"ko_KR": "Amiibo 스캔(빈에서)",
"no_NO": "Skann en Amiibo (fra bin fil)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Escaneie um Amiibo (de um .bin)",
"ru_RU": "Сканировать Amiibo (из папки Bin)",
"sv_SE": "Skanna en Amiibo (från bin-fil)",
"th_TH": "",
@@ -851,7 +851,7 @@
"ID": "MenuBarActionsInstallKeys",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Schlüssel installieren",
"el_GR": "",
"en_US": "Install Keys",
"es_ES": "",
@@ -862,7 +862,7 @@
"ko_KR": "설치 키",
"no_NO": "Installere nøkler",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Instalar Chaves",
"ru_RU": "Установить ключи",
"sv_SE": "Installera nycklar",
"th_TH": "",
@@ -876,7 +876,7 @@
"ID": "MenuBarFileActionsInstallKeysFromFile",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Schlüssel aus KEYS oder ZIP installieren",
"el_GR": "",
"en_US": "Install keys from KEYS or ZIP",
"es_ES": "Instalar keys de KEYS o ZIP",
@@ -887,7 +887,7 @@
"ko_KR": "키나 ZIP에서 키 설치",
"no_NO": "Installer nøkler fra KEYS eller ZIP",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Instalar chaves de CHAVES ou ZIP",
"ru_RU": "Установить ключи из файла KEYS или ZIP",
"sv_SE": "Installera nycklar från KEYS eller ZIP",
"th_TH": "",
@@ -901,7 +901,7 @@
"ID": "MenuBarFileActionsInstallKeysFromFolder",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Schlüssel aus einem Verzeichnis installieren",
"el_GR": "",
"en_US": "Install keys from a directory",
"es_ES": "Instalar keys de un directorio",
@@ -912,7 +912,7 @@
"ko_KR": "디렉터리에서 키 설치",
"no_NO": "Installer nøkler fra en mappe",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Instalar chaves de um diretório",
"ru_RU": "Установить ключи из папки",
"sv_SE": "Installera nycklar från en katalog",
"th_TH": "",
@@ -1001,7 +1001,7 @@
"ID": "MenuBarActionsXCITrimmer",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "XCI-Dateien trimmen",
"el_GR": "",
"en_US": "Trim XCI Files",
"es_ES": "Recortar archivos XCI",
@@ -1012,7 +1012,7 @@
"ko_KR": "XCI 파일 트리머",
"no_NO": "Trim XCI-filer",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Reduzir arquivos XCI",
"ru_RU": "Уменьшить размер XCI файлов",
"sv_SE": "Optimera XCI-filer",
"th_TH": "",
@@ -1037,7 +1037,7 @@
"ko_KR": "보기(_V)",
"no_NO": "_Vis",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Ver",
"ru_RU": "_Вид",
"sv_SE": "_Visa",
"th_TH": "_มุมมอง",
@@ -1062,7 +1062,7 @@
"ko_KR": "윈도 창",
"no_NO": "Vindu størrelse",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Tamanho da janela",
"ru_RU": "Размер окна",
"sv_SE": "Fönsterstorlek",
"th_TH": "ขนาดหน้าต่าง",
@@ -1226,7 +1226,7 @@
"ID": "MenuBarHelpFaqAndGuides",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "FAQ & Anleitungen",
"el_GR": "",
"en_US": "FAQ & Guides",
"es_ES": "",
@@ -1237,7 +1237,7 @@
"ko_KR": "자주 묻는 질문(FAQ) 및 안내",
"no_NO": "Vanlige spørsmål og veiledninger",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "FAQ & Guias",
"ru_RU": "FAQ и Руководства",
"sv_SE": "Frågor, svar och guider",
"th_TH": "",
@@ -1251,7 +1251,7 @@
"ID": "MenuBarHelpFaq",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "FAQ & Fehlerbehebung Seite",
"el_GR": "",
"en_US": "FAQ & Troubleshooting Page",
"es_ES": "",
@@ -1262,7 +1262,7 @@
"ko_KR": "자주 묻는 질문(FAQ) 및 문제해결 페이지",
"no_NO": "FAQ- og feilsøkingsside",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "FAQ e solução de problemas",
"ru_RU": "FAQ & Устранение неполадок",
"sv_SE": "Frågor, svar och felsökningssida",
"th_TH": "",
@@ -1276,7 +1276,7 @@
"ID": "MenuBarHelpFaqTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Öffnet die FAQ- und Fehlerbehebungsseite im offiziellen Ryujinx-Wiki",
"el_GR": "",
"en_US": "Opens the FAQ and Troubleshooting page on the official Ryujinx wiki",
"es_ES": "",
@@ -1287,7 +1287,7 @@
"ko_KR": "공식 Ryujinx 위키에서 자주 묻는 질문(FAQ) 및 문제 해결 페이지 열기",
"no_NO": "Åpner FAQ- og feilsøkingssiden på den offisielle Ryujinx-wikien",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Abre a página de FAQ e solução de problemas no wiki oficial do Ryujinx",
"ru_RU": "Открывает страницы с FAQ и Устранением неполадок на официальной странице вики Ryujinx",
"sv_SE": "Öppnar Frågor, svar och felsökningssidan på den officiella Ryujinx-wikin",
"th_TH": "",
@@ -1301,7 +1301,7 @@
"ID": "MenuBarHelpSetup",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Setup- und Konfigurationsanleitung",
"el_GR": "",
"en_US": "Setup & Configuration Guide",
"es_ES": "",
@@ -1312,7 +1312,7 @@
"ko_KR": "설치 및 구성 안내",
"no_NO": "Oppsett- og konfigurasjonsveiledning",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Guia de instalação e configuração",
"ru_RU": "Руководство по установке и настройке",
"sv_SE": "Konfigurationsguide",
"th_TH": "",
@@ -1326,7 +1326,7 @@
"ID": "MenuBarHelpSetupTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Öffnet die Setup- und Konfigurationsanleitung im offiziellen Ryujinx-Wiki",
"el_GR": "",
"en_US": "Opens the Setup & Configuration guide on the official Ryujinx wiki",
"es_ES": "",
@@ -1337,7 +1337,7 @@
"ko_KR": "공식 Ryujinx 위키에서 설정 및 구성 안내 열기",
"no_NO": "Åpner oppsett- og konfigurasjonsveiledningen på den offisielle Ryujinx-wikien",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Abre o guia de instalação e configuração no wiki oficial do Ryujinx",
"ru_RU": "Открывает страницу Руководство по установке и настройке на официальной странице вики Ryujinx",
"sv_SE": "Öppnar konfigurationsguiden på den officiella Ryujinx-wikin",
"th_TH": "",
@@ -1351,7 +1351,7 @@
"ID": "MenuBarHelpMultiplayer",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Multiplayer (LDN/LAN) Anleitung",
"el_GR": "",
"en_US": "Multiplayer (LDN/LAN) Guide",
"es_ES": "",
@@ -1362,7 +1362,7 @@
"ko_KR": "멀티플레이어(LDN/LAN) 안내",
"no_NO": "Flerspillerveiledning (LDN/LAN)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Guia multijogador (LDN/LAN)",
"ru_RU": "Гайд по мультиплееру (LDN/LAN)",
"sv_SE": "Flerspelarguide (LDN/LAN)",
"th_TH": "",
@@ -1376,7 +1376,7 @@
"ID": "MenuBarHelpMultiplayerTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Öffnet die Multiplayer-Anleitung im offiziellen Ryujinx-Wiki",
"el_GR": "",
"en_US": "Opens the Multiplayer guide on the official Ryujinx wiki",
"es_ES": "",
@@ -1387,7 +1387,7 @@
"ko_KR": "공식 Ryujinx 위키에서 멀티플레이어 안내 열기",
"no_NO": "Åpner flerspillerveiledningen på den offisielle Ryujinx-wikien",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Abre o guia multijogador no wiki oficial do Ryujinx",
"ru_RU": "Открывает гайд по мультиплееру на официальной странице вики Ryujinx",
"sv_SE": "Öppnar flerspelarguiden på den officiella Ryujinx-wikin",
"th_TH": "",
@@ -1524,6 +1524,156 @@
},
{
"ID": "GameListHeaderDeveloper",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Entwickelt von {0}",
+ "el_GR": "",
+ "en_US": "Developed by {0}",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Utviklet av {0}",
+ "pl_PL": "",
+ "pt_BR": "Desenvolvido por {0}",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "由 {0} 开发",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameListHeaderVersion",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "Έκδοση: {0}",
+ "en_US": "Version: {0}",
+ "es_ES": "Versión: {0}",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "Versione: {0}",
+ "ja_JP": "バージョン: {0}",
+ "ko_KR": "버전: {0}",
+ "no_NO": "Versjon: {0}",
+ "pl_PL": "Wersja: {0}",
+ "pt_BR": "Versão: {0}",
+ "ru_RU": "Версия: {0}",
+ "sv_SE": "",
+ "th_TH": "เวอร์ชั่น: {0}",
+ "tr_TR": "Sürüm: {0}",
+ "uk_UA": "Версія: {0}",
+ "zh_CN": "版本: {0}",
+ "zh_TW": "版本: {0}"
+ }
+ },
+ {
+ "ID": "GameListHeaderTimePlayed",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Spielzeit:",
+ "el_GR": "Χρόνος:",
+ "en_US": "Play Time:",
+ "es_ES": "Tiempo jugado:",
+ "fr_FR": "Temps de jeu:",
+ "he_IL": "",
+ "it_IT": "Tempo di gioco:",
+ "ja_JP": "プレイ時間:",
+ "ko_KR": "플레이 타임:",
+ "no_NO": "Spilletid:",
+ "pl_PL": "Czas w grze:",
+ "pt_BR": "Tempo de jogo:",
+ "ru_RU": "Время в игре:",
+ "sv_SE": "Speltid:",
+ "th_TH": "เล่นไปแล้ว:",
+ "tr_TR": "Oynama Süresi:",
+ "uk_UA": "Зіграно часу:",
+ "zh_CN": "游玩时长:",
+ "zh_TW": "遊玩時數:"
+ }
+ },
+ {
+ "ID": "GameListHeaderLastPlayed",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Zuletzt gespielt: ",
+ "el_GR": "Παίχτηκε: ",
+ "en_US": "Last Played:",
+ "es_ES": "Jugado por última vez:",
+ "fr_FR": "Dernière partie jouée:",
+ "he_IL": "",
+ "it_IT": "Ultima partita:",
+ "ja_JP": "最終プレイ日時:",
+ "ko_KR": "마지막 플레이:",
+ "no_NO": "Sist Spilt:",
+ "pl_PL": "Ostatnio grane:",
+ "pt_BR": "Último jogo:",
+ "ru_RU": "Последний запуск:",
+ "sv_SE": "Senast spelad:",
+ "th_TH": "เล่นล่าสุด:",
+ "tr_TR": "Son Oynama Tarihi:",
+ "uk_UA": "Востаннє зіграно:",
+ "zh_CN": "最近游玩:",
+ "zh_TW": "最近遊玩:"
+ }
+ },
+ {
+ "ID": "GameListHeaderFileExtension",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Dateiformat: {0}",
+ "el_GR": "Κατάληξη: {0}",
+ "en_US": "Extension: {0}",
+ "es_ES": "Extensión: {0}",
+ "fr_FR": "Extension du Fichier: {0}",
+ "he_IL": "",
+ "it_IT": "Estensione: {0}",
+ "ja_JP": "ファイル拡張子: {0}",
+ "ko_KR": "파일 확장자: {0}",
+ "no_NO": "Fil Eks.: {0}",
+ "pl_PL": "Rozszerzenie pliku: {0}",
+ "pt_BR": "Extensão: {0}",
+ "ru_RU": "Расширение файла: {0}",
+ "sv_SE": "Filänd: {0}",
+ "th_TH": "นามสกุลไฟล์: {0}",
+ "tr_TR": "Dosya Uzantısı: {0}",
+ "uk_UA": "Розширення файлу: {0}",
+ "zh_CN": "扩展名: {0}",
+ "zh_TW": "副檔名: {0}"
+ }
+ },
+ {
+ "ID": "GameListHeaderFileSize",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Dateigröße: {0}",
+ "el_GR": "Μέγεθος Αρχείου: {0}",
+ "en_US": "File Size: {0}",
+ "es_ES": "Tamaño del archivo: {0}",
+ "fr_FR": "Taille du Fichier: {0}",
+ "he_IL": "",
+ "it_IT": "Dimensione file: {0}",
+ "ja_JP": "ファイルサイズ: {0}",
+ "ko_KR": "파일 크기: {0}",
+ "no_NO": "Fil Størrelse: {0}",
+ "pl_PL": "Rozmiar pliku: {0}",
+ "pt_BR": "Tamanho: {0}",
+ "ru_RU": "Размер файла: {0}",
+ "sv_SE": "Filstorlek: {0}",
+ "th_TH": "ขนาดไฟล์: {0}",
+ "tr_TR": "Dosya Boyutu: {0}",
+ "uk_UA": "Розмір файлу: {0}",
+ "zh_CN": "大小: {0}",
+ "zh_TW": "檔案大小: {0}"
+ }
+ },
+ {
+ "ID": "GameListSortDeveloper",
"Translations": {
"ar_SA": "المطور",
"de_DE": "Entwickler",
@@ -1548,32 +1698,7 @@
}
},
{
- "ID": "GameListHeaderVersion",
- "Translations": {
- "ar_SA": "الإصدار",
- "de_DE": "",
- "el_GR": "Έκδοση",
- "en_US": "Version",
- "es_ES": "Versión",
- "fr_FR": "",
- "he_IL": "גרסה",
- "it_IT": "Versione",
- "ja_JP": "バージョン",
- "ko_KR": "버전",
- "no_NO": "Versjon",
- "pl_PL": "Wersja",
- "pt_BR": "Versão",
- "ru_RU": "Версия",
- "sv_SE": "",
- "th_TH": "เวอร์ชั่น",
- "tr_TR": "Sürüm",
- "uk_UA": "Версія",
- "zh_CN": "版本",
- "zh_TW": "版本"
- }
- },
- {
- "ID": "GameListHeaderTimePlayed",
+ "ID": "GameListSortTimePlayed",
"Translations": {
"ar_SA": "وقت اللعب",
"de_DE": "Spielzeit",
@@ -1598,7 +1723,7 @@
}
},
{
- "ID": "GameListHeaderLastPlayed",
+ "ID": "GameListSortLastPlayed",
"Translations": {
"ar_SA": "آخر مرة لُعبت",
"de_DE": "Zuletzt gespielt",
@@ -1623,7 +1748,7 @@
}
},
{
- "ID": "GameListHeaderFileExtension",
+ "ID": "GameListSortFileExtension",
"Translations": {
"ar_SA": "صيغة الملف",
"de_DE": "Dateiformat",
@@ -1648,7 +1773,7 @@
}
},
{
- "ID": "GameListHeaderFileSize",
+ "ID": "GameListSortFileSize",
"Translations": {
"ar_SA": "حجم الملف",
"de_DE": "Dateigröße",
@@ -1673,7 +1798,7 @@
}
},
{
- "ID": "GameListHeaderPath",
+ "ID": "GameListSortPath",
"Translations": {
"ar_SA": "المسار",
"de_DE": "Pfad",
@@ -1697,6 +1822,106 @@
"zh_TW": "路徑"
}
},
+ {
+ "ID": "GameListHeaderCompatibilityStatus",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Kompatibilität:",
+ "el_GR": "",
+ "en_US": "Compatibility:",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Kompatibilitet",
+ "pl_PL": "",
+ "pt_BR": "Compatibilidade:",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "兼容性:",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameListHeaderTitleId",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Title ID:",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Tittel ID:",
+ "pl_PL": "",
+ "pt_BR": "ID do título:",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "标题 ID:",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameListHeaderHostedGames",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Hosted Games: {0}",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Spill som Arrangeres: {0}",
+ "pl_PL": "",
+ "pt_BR": "Jogos hospedados: {0}",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "服务的游戏: {0}",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameListHeaderPlayerCount",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Online Players: {0}",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Online-spillere: {0}",
+ "pl_PL": "",
+ "pt_BR": "Jogadores Online: {0}",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "在线玩家: {0}",
+ "zh_TW": ""
+ }
+ },
{
"ID": "GameListContextMenuOpenUserSaveDirectory",
"Translations": {
@@ -2034,16 +2259,16 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "PPTC 캐시 제거",
+ "no_NO": "Tøm PPTC-bufferen",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Limpar cache PPTC",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "清理 PPTC 缓存",
"zh_TW": ""
}
},
@@ -2059,16 +2284,16 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "앱의 모든 PPTC 캐시 파일 삭제",
+ "no_NO": "Sletter alle PPTC-cache-filer for applikasjonen",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Apagar os arquivos de cache PPTC do aplicativo",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "删除应用程序的所有 PPTC 缓存",
"zh_TW": ""
}
},
@@ -2384,12 +2609,12 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
+ "ko_KR": "선택한 DLC 파일에서 RomFS 추출",
"no_NO": "Pakk ut RomFS filene fra valgt DLC fil",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Extraia o RomFS de um arquivo DLC selecionado",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "Extrahera RomFS från en vald DLC-fil",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
@@ -2412,7 +2637,7 @@
"ko_KR": "로고",
"no_NO": "",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Logotipo",
"ru_RU": "Лого",
"sv_SE": "Logotyp",
"th_TH": "โลโก้",
@@ -2534,16 +2759,16 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "호환성 항목 표시",
+ "no_NO": "Vis kompatibilitetsoppføring",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Mostrar entrada de compatibilidade",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "显示兼容性项目",
"zh_TW": ""
}
},
@@ -2559,16 +2784,16 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "일반적으로 도움말 메뉴를 통해 접근할 수 있는 호환성 목록에 선택한 게임을 표시합니다.",
+ "no_NO": "Vis det valgte spillet i kompatibilitetslisten, som du vanligvis får tilgang til via Hjelp-menyen.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Exibe o jogo selecionado na Lista de Compatibilidade, que normalmente pode ser acessada pelo menu Ajuda.",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "在兼容性列表中显示选定的游戏,您通常可以通过帮助菜单访问。",
"zh_TW": ""
}
},
@@ -2578,22 +2803,22 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
- "en_US": "Show Game Stats",
+ "en_US": "Show Game Info",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "게임 통계 표시",
+ "no_NO": "Vis Spill Info",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Mostrar informações do jogo",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "显示游戏信息",
"zh_TW": ""
}
},
@@ -2603,22 +2828,22 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
- "en_US": "Show the other various information about the currently selected game that is missing from the Grid view layout.",
+ "en_US": "Show stats & details about the currently selected game.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "그리드 보기 레이아웃에서 누락된 현재 선택된 게임에 대한 다양한 정보를 표시합니다.",
+ "no_NO": "Vis statistikk og detaljer om det valgte spillet.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Mostrar estatísticas e detalhes sobre o jogo selecionado no momento.",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "显示当前选定游戏的状态与详细信息。",
"zh_TW": ""
}
},
@@ -2712,7 +2937,7 @@
"ko_KR": "해당 게임의 모드가 포함된 대체 SD 카드 Atmosphere 디렉터리를 엽니다. 실제 하드웨어용으로 패키징된 모드에 유용합니다.",
"no_NO": "Åpner den alternative SD-kortets Atmosfære-mappe som inneholder programmoduser. Nyttig for modifikasjoner som er pakket for ekte maskinvare.",
"pl_PL": "Otwiera alternatywny katalog Atmosphere na karcie SD, który zawiera mody danej aplikacji. Przydatne dla modów przygotowanych pod prawdziwy sprzęt.",
- "pt_BR": "",
+ "pt_BR": "Abre o diretório Atmosphere do cartão SD alternativo que contém os Mods do aplicativo. Útil para mods que são empacotados para hardware real.",
"ru_RU": "Открывает папку Atmosphere на альтернативной SD-карте, которая содержит моды для приложений и игр. Полезно для модов, сделанных для реальной консоли.",
"sv_SE": "Öppnar den alternativa Atmosphere-katalogen på SD-kort som innehåller applikationens Mods. Användbart för Mods som är paketerade för riktig hårdvara.",
"th_TH": "เปิดไดเร็กทอรี่ Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน ซึ่งมีประโยชน์สำหรับ Mods ที่บรรจุมากับฮาร์ดแวร์จริง",
@@ -2737,7 +2962,7 @@
"ko_KR": "XCI 파일 확인 및 트림",
"no_NO": "Kontroller og trim XCI-filen",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Verificar e reduzir o arquivo XCI",
"ru_RU": "Проверить и обрезать XCI файл",
"sv_SE": "Kontrollera och optimera XCI-fil",
"th_TH": "",
@@ -2762,7 +2987,7 @@
"ko_KR": "디스크 공간을 절약하기 위해 XCI 파일 확인 및 트림",
"no_NO": "Kontroller og trimm XCI-filen for å spare diskplass",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Verifique e reduza o arquivo XCI para economizar espaço em disco",
"ru_RU": "Проверить и обрезать XCI файл для уменьшения его размера",
"sv_SE": "Kontrollera och optimera XCI-fil för att spara diskutrymme",
"th_TH": "",
@@ -2837,7 +3062,7 @@
"ko_KR": "XCI 파일 '{0}' 트리밍",
"no_NO": "Trimming av XCI-filen '{0}'",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Reduzindo arquivo XCI '{0}'",
"ru_RU": "Обрезается XCI файл '{0}'",
"sv_SE": "Optimerar XCI-filen '{0}'",
"th_TH": "",
@@ -3125,26 +3350,251 @@
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunch",
"Translations": {
- "ar_SA": "التحقق من وجود تحديثات عند التشغيل",
- "de_DE": "Beim Start nach Updates suchen",
- "el_GR": "Έλεγχος για Ενημερώσεις στην Εκκίνηση",
- "en_US": "Check for Updates on Launch",
- "es_ES": "Buscar actualizaciones al iniciar",
- "fr_FR": "Vérifier les mises à jour au démarrage",
- "he_IL": "בדוק אם קיימים עדכונים בהפעלה",
- "it_IT": "Controlla aggiornamenti all'avvio",
- "ja_JP": "起動時にアップデートを確認する",
- "ko_KR": "시작 시, 업데이트 확인",
- "no_NO": "Se etter oppdateringer ved oppstart",
- "pl_PL": "Sprawdzaj aktualizacje przy uruchomieniu",
- "pt_BR": "Verificar se há atualizações ao iniciar",
- "ru_RU": "Проверять наличие обновлений при запуске",
- "sv_SE": "Leta efter uppdatering vid uppstart",
- "th_TH": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม",
- "tr_TR": "Her Açılışta Güncellemeleri Denetle",
- "uk_UA": "Перевіряти наявність оновлень під час запуску",
- "zh_CN": "启动时检查更新",
- "zh_TW": "啟動時檢查更新"
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Check for Updates:",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Se etter Oppdateringer:",
+ "pl_PL": "",
+ "pt_BR": "Verificar atualizações:",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "检查更新",
+ "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": "Av",
+ "pl_PL": "",
+ "pt_BR": "Desligado",
+ "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": "Spør",
+ "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": "Bakgrunn",
+ "pl_PL": "",
+ "pt_BR": "Fundo",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "背景",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsTabGeneralFocusLossType",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "On Emulator Focus Lost:",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "På Emulator Fokus Tapt:",
+ "pl_PL": "",
+ "pt_BR": "Ao perder o Foco do emulador:",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "当模拟器在后台时:",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsTabGeneralFocusLossTypeDoNothing",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Do Nothing",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Gjør Ingenting",
+ "pl_PL": "",
+ "pt_BR": "Não fazer nada",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "什么事情也不做",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsTabGeneralFocusLossTypeBlockInput",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Block Input",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Blokkinngang",
+ "pl_PL": "",
+ "pt_BR": "Bloquear entrada",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "禁用输入",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsTabGeneralFocusLossTypeMuteAudio",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Mute Volume",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Demp Lyd",
+ "pl_PL": "",
+ "pt_BR": "Ficar mudo",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "静音",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsTabGeneralFocusLossTypeBlockInputAndMuteAudio",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Block Input & Mute Volume",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Blokker Inputs og demp Volumet",
+ "pl_PL": "",
+ "pt_BR": "Bloquear entrada & Ficar mudo",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "阻止输入且静音",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsTabGeneralFocusLossTypePauseEmulation",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Pause Emulation",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Pause Emulatoren",
+ "pl_PL": "",
+ "pt_BR": "Pausar a emulação",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "暂停模拟",
+ "zh_TW": ""
}
},
{
@@ -3197,6 +3647,31 @@
"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": "Desativar entrada quando estiver fora de foco",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "在后台时禁用输入",
+ "zh_TW": ""
+ }
+ },
{
"ID": "SettingsTabGeneralShowTitleBar",
"Translations": {
@@ -3212,7 +3687,7 @@
"ko_KR": "제목 표시줄 표시(다시 시작해야 함)",
"no_NO": "Vis tittellinje (krever omstart)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Mostrar barra de título (requer reinicialização)",
"ru_RU": "Показать строку заголовка (требуется перезапуск)",
"sv_SE": "Visa titelrad (kräver omstart)",
"th_TH": "",
@@ -3384,7 +3859,7 @@
"he_IL": "",
"it_IT": "Aggiornamenti e DLC che fanno riferimento a file mancanti verranno disabilitati automaticamente",
"ja_JP": "",
- "ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 언로드",
+ "ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 불러오기 취소",
"no_NO": "DLC og oppdateringer som henviser til manglende filer, vil bli lastet ned automatisk",
"pl_PL": "",
"pt_BR": "DLCs e Atualizações que se referem a arquivos ausentes serão descarregadas automaticamente",
@@ -4146,7 +4621,7 @@
"zh_CN": "繁体中文(推荐)",
"zh_TW": "正體中文 (建議)"
}
- },
+ },
{
"ID": "SettingsTabSystemSystemLanguageSwedish",
"Translations": {
@@ -4159,8 +4634,8 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "스웨덴어",
+ "no_NO": "Svensk",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
@@ -4168,10 +4643,10 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "瑞典语",
"zh_TW": ""
}
- },
+ },
{
"ID": "SettingsTabSystemSystemLanguageNorwegian",
"Translations": {
@@ -4184,7 +4659,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
+ "ko_KR": "노르웨이어",
"no_NO": "Norsk",
"pl_PL": "",
"pt_BR": "",
@@ -4193,7 +4668,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "挪威语",
"zh_TW": ""
}
},
@@ -4259,16 +4734,16 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "매치 시스템 시간",
+ "no_NO": "Match systemtid",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Sincronizar data e hora com o sistema PC",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "与系统时间同步",
"zh_TW": ""
}
},
@@ -4287,7 +4762,7 @@
"ko_KR": "PPTC(프로파일된 영구 번역 캐시)",
"no_NO": "PPTC (Profilert Vedvarende Oversettelseshurtigbuffer)",
"pl_PL": "PPTC (Profilowana pamięć podręczna trwałych łłumaczeń)",
- "pt_BR": "Habilitar PPTC (Profiled Persistent Translation Cache)",
+ "pt_BR": "PPTC (Cache de Tradução Persistente de Perfil)",
"ru_RU": "Использовать PPTC (Profiled Persistent Translation Cache)",
"sv_SE": "PPTC (Profilerad bestående översättningscache)",
"th_TH": "PPTC (แคชโปรไฟล์การแปลแบบถาวร)",
@@ -4312,7 +4787,7 @@
"ko_KR": "저전력 PPTC 캐시",
"no_NO": "PPTC med lavt strømforbruk",
"pl_PL": "Low-power PPTC",
- "pt_BR": "Low-power PPTC",
+ "pt_BR": "Cache PPTC com baixo consumo de energia",
"ru_RU": "PPTC с низким электропотреблением",
"sv_SE": "PPTC med låg strömförbrukning",
"th_TH": "PPTC แบบพลังงานตํ่า",
@@ -4562,7 +5037,7 @@
"ko_KR": "4GB",
"no_NO": "4GB",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "4GB",
"ru_RU": "4ГиБ",
"sv_SE": "",
"th_TH": "",
@@ -4587,7 +5062,7 @@
"ko_KR": "6GB",
"no_NO": "6GB",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "6GB",
"ru_RU": "6ГиБ",
"sv_SE": "",
"th_TH": "",
@@ -4612,7 +5087,7 @@
"ko_KR": "8GB",
"no_NO": "8GB",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "8GB",
"ru_RU": "8ГиБ",
"sv_SE": "",
"th_TH": "",
@@ -4637,7 +5112,7 @@
"ko_KR": "12GB",
"no_NO": "12GB",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "12GB",
"ru_RU": "12ГиБ",
"sv_SE": "",
"th_TH": "",
@@ -5522,6 +5997,31 @@
"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": "Aktivere UI-logger",
+ "pl_PL": "",
+ "pt_BR": "Habilitar logs da IU",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "启用 UI 日志",
+ "zh_TW": ""
+ }
+ },
{
"ID": "SettingsTabLoggingEnableFsAccessLogs",
"Translations": {
@@ -5897,6 +6397,56 @@
"zh_TW": "關閉"
}
},
+ {
+ "ID": "SettingsButtonReset",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Reset Settings",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Tilbakestill innstillinger",
+ "pl_PL": "",
+ "pt_BR": "Redefinir configurações",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "重置设置",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "SettingsButtonResetConfirm",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "I want to reset my settings.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Jeg vil tilbakestille innstillingene mine.",
+ "pl_PL": "",
+ "pt_BR": "Quero redefinir minhas configurações.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "我要重置我的设置。",
+ "zh_TW": ""
+ }
+ },
{
"ID": "SettingsButtonOk",
"Translations": {
@@ -5914,7 +6464,7 @@
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "Ok",
"th_TH": "ตกลง",
"tr_TR": "Tamam",
"uk_UA": "",
@@ -7809,12 +8359,12 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "비활성화",
+ "no_NO": "Deaktiver",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "Inaktivera",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
@@ -7834,12 +8384,12 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "레인보우",
+ "no_NO": "Regnbue",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "Regnbåge",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
@@ -7859,8 +8409,8 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "레인보우 속도",
+ "no_NO": "Regnbue Hastighet",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
@@ -7868,7 +8418,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "彩虹滚动速度",
"zh_TW": ""
}
},
@@ -7884,12 +8434,12 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "색상",
+ "no_NO": "Farge",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "Färg",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
@@ -7962,7 +8512,7 @@
"ko_KR": "알 수 없음",
"no_NO": "Ukjent",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Desconhecido",
"ru_RU": "Неизвестно",
"sv_SE": "Okänd",
"th_TH": "ไม่รู้จัก",
@@ -10912,7 +11462,7 @@
"ko_KR": "닫기",
"no_NO": "Lukk",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Fechar",
"ru_RU": "Закрыть",
"sv_SE": "Stäng",
"th_TH": "",
@@ -11112,7 +11662,7 @@
"ko_KR": "프로필 보기",
"no_NO": "Se Profil",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Ver perfil",
"ru_RU": "Показать профиль",
"sv_SE": "Visa profil",
"th_TH": "",
@@ -12687,7 +13237,7 @@
"ko_KR": "XCI 트리머 창",
"no_NO": "XCI Trimmervindu",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Janela de redução XCI",
"ru_RU": "Окно триммера XCI",
"sv_SE": "XCI-optimerare",
"th_TH": "",
@@ -13134,16 +13684,16 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "다음에서 모든 PPTC 데이터를 제거하려고 합니다:\n\n{0}\n\n계속하시겠습니까?",
+ "no_NO": "Du er i ferd med å slette alle PPTC-data fra:\n\n{0}\n\n\nEr du sikker på at du vil fortsette?",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Você está prestes a limpar todos os dados PPTC de:\n\n{0}\n\nTem certeza de que deseja continuar?",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "",
+ "zh_CN": "您正要清理 PPTC 数据:\n\n{0}\n\n您确实要继续吗?",
"zh_TW": ""
}
},
@@ -13437,7 +13987,7 @@
"ko_KR": "{0}에서 잘못된 키 파일이 발견",
"no_NO": "En ugyldig Keys-fil ble funnet i {0}.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Um arquivo Chaves inválido foi encontrado em {0}",
"ru_RU": "В {0} были найдены некорректные ключи",
"sv_SE": "En ogiltig nyckelfil hittades i {0}",
"th_TH": "",
@@ -13462,7 +14012,7 @@
"ko_KR": "설치 키",
"no_NO": "Installere nøkler",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Instalar Chaves",
"ru_RU": "Установить ключи",
"sv_SE": "Installera nycklar",
"th_TH": "",
@@ -13487,7 +14037,7 @@
"ko_KR": "새로운 키 파일이 설치됩니다.",
"no_NO": "Ny Keys-fil vil bli installert.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O novo arquivo Keys será instalado",
"ru_RU": "Будут установлены новые ключи.",
"sv_SE": "Ny nyckelfil kommer att installeras.",
"th_TH": "",
@@ -13512,7 +14062,7 @@
"ko_KR": "\n\n이로 인해 현재 설치된 키 중 일부가 대체될 수 있습니다.",
"no_NO": "\n\nDette kan erstatte noen av de nåværende installerte nøklene.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "\n\nIsso pode substituir algumas das chaves instaladas atualmente.",
"ru_RU": "\n\nЭто действие может перезаписать установленные ключи.",
"sv_SE": "\n\nDetta kan ersätta några av de redan installerade nycklarna.",
"th_TH": "",
@@ -13537,7 +14087,7 @@
"ko_KR": "\n\n계속하시겠습니까?",
"no_NO": "\n\nVil du fortsette?",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "\n\nVocê quer continuar?",
"ru_RU": "\n\nХотите продолжить?",
"sv_SE": "\n\nVill du fortsätta?",
"th_TH": "",
@@ -13562,7 +14112,7 @@
"ko_KR": "키 설치 중...",
"no_NO": "Installere nøkler...",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Instalando Chaves...",
"ru_RU": "Установка ключей...",
"sv_SE": "Installerar nycklar...",
"th_TH": "",
@@ -13587,7 +14137,7 @@
"ko_KR": "새로운 키 파일이 성공적으로 설치되었습니다.",
"no_NO": "Ny Keys -fil installert.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Novo arquivo de chaves instalado com sucesso.",
"ru_RU": "Новые ключи были успешно установлены.",
"sv_SE": "Ny nyckelfil installerades.",
"th_TH": "",
@@ -14587,7 +15137,7 @@
"ko_KR": "Ryujinx는 Nintendo Switch™용 에뮬레이터입니다.\n모든 최신 소식을 Discord에서 확인하세요.\n기여에 관심이 있는 개발자는 GitHub 또는 Discord에서 자세한 내용을 확인할 수 있습니다.",
"no_NO": "Ryujinx er en emulator for Nintendo SwitchTM.\nVennligst støtt oss på Patreon.\nFå alle de siste nyhetene på vår Twitter eller Discord.\nUtviklere som er interessert i å bidra kan finne ut mer på GitHub eller Discord.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Ryujinx é um emulador de Nintendo Switch™.\nReceba todas as últimas notícias em nosso Discord.\nDesenvolvedores interessados em contribuir podem descobrir mais em nosso GitHub ou Discord.",
"ru_RU": "Ryujinx - это эмулятор для Nintendo Switch™.\nПолучайте все последние новости разработки в нашем Discord.\nРазработчики, заинтересованные в участии, могут узнать больше на нашем GitHub или Discord.",
"sv_SE": "Ryujinx är en emulator för Nintendo Switch™.\nFå de senaste nyheterna via vår Discord.\nUtvecklare som är intresserade att bidra kan hitta mer info på vår GitHub eller Discord.",
"th_TH": "",
@@ -14637,7 +15187,7 @@
"ko_KR": "이전 관리자 :",
"no_NO": "Tidligere vedlikeholdt av:",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Anteriormente mantido por:",
"ru_RU": "Поддержка:",
"sv_SE": "Underhölls tidigare av:",
"th_TH": "",
@@ -14689,7 +15239,7 @@
"pl_PL": "Seria Amiibo",
"pt_BR": "Franquia Amiibo",
"ru_RU": "Серия Amiibo",
- "sv_SE": "",
+ "sv_SE": "Amiibo-serie",
"th_TH": "",
"tr_TR": "Amiibo Serisi",
"uk_UA": "Серія Amiibo",
@@ -15237,7 +15787,7 @@
"ko_KR": "\"현재 진행 중인\" 디스코드 활동에 Ryujinx를 표시할지 여부를 선택",
"no_NO": "Velg om Ryujinx skal vises på din \"spillende\" Discord aktivitet eller ikke",
"pl_PL": "Wybierz, czy chcesz wyświetlać Ryujinx w swojej \"aktualnie grane\" aktywności Discord",
- "pt_BR": "Habilita ou desabilita Discord Rich Presence",
+ "pt_BR": "Escolha se deseja mostrar Ryujinx ou não na sua atividade do Discord quando estiver usando-o",
"ru_RU": "Включает или отключает отображение статуса \"Играет в игру\" в Discord",
"sv_SE": "Välj huruvida Ryujinx ska visas på din \"spelar för tillfället\" Discord-aktivitet",
"th_TH": "เลือกว่าจะแสดง Ryujinx ในกิจกรรม Discord \"ที่กำลังเล่นอยู่\" ของคุณหรือไม่?",
@@ -15662,7 +16212,7 @@
"ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.",
"no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Sincroniza a data e hora do emulador com seu sistema PC",
"ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.",
"sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.",
"th_TH": "",
@@ -15712,7 +16262,7 @@
"ko_KR": "번역된 JIT 함수를 저장하여 게임을 불러올 때마다 번역할 필요가 없도록 합니다.\n\n게임을 처음 부팅한 후 끊김 현상을 줄이고 부팅 시간을 크게 단축합니다.\n\n모르면 켬으로 두세요.",
"no_NO": "Lagrer oversatte JIT funksjoner så de ikke trenger og bli oversatt hver gang spillet laster.\n\nKan redusere hakkete spilling og gjør at spillet starter opp raskere ved første oppstart.\n\nLa være PÅ om usikker.",
"pl_PL": "Zapisuje przetłumaczone funkcje JIT, dzięki czemu nie muszą być tłumaczone za każdym razem, gdy gra się ładuje.\n\nZmniejsza zacinanie się i znacznie przyspiesza uruchamianie po pierwszym uruchomieniu gry.\n\nJeśli nie masz pewności, pozostaw WŁĄCZONE",
- "pt_BR": "Habilita ou desabilita PPTC",
+ "pt_BR": "Salva funções JIT traduzidas para que elas não precisem ser traduzidas toda vez que o jogo for carregado.\n\nReduz a trepidação e acelera significativamente os tempos de inicialização após a primeira inicialização de um jogo.\n\nDeixe LIGADO se não tiver certeza.",
"ru_RU": "Сохраняет скомпилированные JIT-функции для того, чтобы не преобразовывать их по новой каждый раз при запуске игры.\n\nУменьшает статтеры и значительно ускоряет последующую загрузку игр.\n\nРекомендуется оставить включенным.",
"sv_SE": "Sparar översatta JIT-funktioner så att de inte behöver översättas varje gång som spelet läses in.\n\nMinskar stuttering och snabbare på uppstartstiden väsentligt efter första uppstarten av ett spel.\n\nLämna PÅ om du är osäker.",
"th_TH": "บันทึกฟังก์ชั่น JIT ที่แปลแล้ว ดังนั้นจึงไม่จำเป็นต้องแปลทุกครั้งที่โหลดเกม\n\nลดอาการกระตุกและเร่งความเร็วการบูตได้อย่างมากหลังจากการบูตครั้งแรกของเกม\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ",
@@ -15737,7 +16287,7 @@
"ko_KR": "코어의 3분의 1을 사용하여 PPTC를 불러옵니다.",
"no_NO": "Last inn PPTC med en tredjedel av antall kjerner.",
"pl_PL": "",
- "pt_BR": "Carregar o PPTC usando um terço da quantidade de núcleos.",
+ "pt_BR": "Carrega o PPTC usando um terço da quantidade de núcleos.",
"ru_RU": "Загрузить PPTC, используя треть от количества ядер.",
"sv_SE": "Läs in PPTC med en tredjedel av mängden kärnor.",
"th_TH": "โหลด PPTC โดยใช้หนึ่งในสามของจำนวนคอร์",
@@ -16062,7 +16612,7 @@
"ko_KR": "후속 실행 시 끊김 현상을 줄이는 디스크 셰이더 캐시를 저장합니다.\n\n모르면 켬으로 두세요.",
"no_NO": "Lagrer en disk shader cache som reduserer hakking jo flere ganger du spiller.\n\nLa være PÅ om usikker.",
"pl_PL": "Zapisuje pamięć podręczną shaderów na dysku, co zmniejsza zacinanie się w kolejnych uruchomieniach.\n\nPozostaw WŁĄCZONE, jeśli nie masz pewności.",
- "pt_BR": "Habilita ou desabilita o cache de shader",
+ "pt_BR": "Salva um cache de shader de disco que reduz a trepidação em execuções subsequentes.\n\nDeixe LIGADO se não tiver certeza.",
"ru_RU": "Сохраняет кэш шейдеров на диске, для уменьшения статтеров при последующих запусках.\n\nРекомендуется оставить включенным.",
"sv_SE": "Sparar en disk shader cache som minskar stuttering i efterföljande körningar.\n\nLämna PÅ om du är osäker.",
"th_TH": "บันทึกแคชแสงเงาของดิสก์ซึ่งช่วยลดการกระตุกในการรันครั้งต่อๆ ไป\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ",
@@ -16447,6 +16997,31 @@
"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": "Skriver ut Avalonia (UI)-loggmeldinger i konsollen.",
+ "pl_PL": "",
+ "pt_BR": "Imprimir mensagens de log do Avalonia (IU) no console.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "在控制台显示 Avalonia (UI) 的日志信息",
+ "zh_TW": ""
+ }
+ },
{
"ID": "OpenGlLogLevel",
"Translations": {
@@ -16562,7 +17137,7 @@
"ko_KR": "파일 탐색기를 열어 DLC를 일괄 불러오기할 폴더를 하나 이상 선택",
"no_NO": "Åpne en filutforsker for å velge en eller flere mapper å laste inn DLC fra",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Abrir um explorador de arquivos para escolher uma ou mais pastas para carregar DLC em massa",
"ru_RU": "Открывает проводник, для выбора одной или нескольких папок для массовой загрузки DLC",
"sv_SE": "Öppna en filutforskare för att välja en eller flera mappar att läsa in alla DLC från",
"th_TH": "เปิดตัวสำรวจไฟล์เพื่อเลือกหนึ่งโฟลเดอร์ขึ้นไปเพื่อโหลด DLC จำนวนมาก",
@@ -16587,7 +17162,7 @@
"ko_KR": "파일 탐색기를 열어 하나 이상의 폴더를 선택하여 대량으로 타이틀 업데이트 불러오기",
"no_NO": "Åpne en filutforsker for å velge en eller flere mapper som du vil laste inn titteloppdateringer fra",
"pl_PL": "",
- "pt_BR": "Abra o explorador de arquivos para selecionar uma ou mais pastas e carregar atualizações de jogo em massa.",
+ "pt_BR": "Abrir o explorador de arquivos para selecionar uma ou mais pastas e carregar atualizações de jogo em massa.",
"ru_RU": "Открывает проводник, чтобы выбрать одну или несколько папок для массовой загрузки обновлений приложений",
"sv_SE": "Öppna en filutforskare för att välja en eller flera mappar att läsa in alla titeluppdateringar från",
"th_TH": "เปิดตัวสำรวจไฟล์เพื่อเลือกหนึ่งโฟลเดอร์ขึ้นไปเพื่อโหลดไฟล์อัปเดตจำนวนมาก",
@@ -17347,6 +17922,31 @@
"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": "Oppdatering tilgjengelig!",
+ "pl_PL": "",
+ "pt_BR": "Atualização disponível!",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "有可用的更新!",
+ "zh_TW": ""
+ }
+ },
{
"ID": "ControllerSettingsRotate90",
"Translations": {
@@ -17912,7 +18512,7 @@
"ko_KR": "일부",
"no_NO": "Delvis",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Parcial",
"ru_RU": "Частично",
"sv_SE": "Delvis",
"th_TH": "",
@@ -17937,7 +18537,7 @@
"ko_KR": "트리밍되지 않음",
"no_NO": "Ikke trimmet",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Não reduzido",
"ru_RU": "Не обрезан",
"sv_SE": "Inte optimerad",
"th_TH": "",
@@ -17962,7 +18562,7 @@
"ko_KR": "트리밍됨",
"no_NO": "Trimmet",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Reduzido",
"ru_RU": "Обрезан",
"sv_SE": "Optimerad",
"th_TH": "",
@@ -17987,7 +18587,7 @@
"ko_KR": "(실패)",
"no_NO": "(Mislyktes)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Falhou",
"ru_RU": "(Ошибка)",
"sv_SE": "(misslyckades)",
"th_TH": "",
@@ -18012,7 +18612,7 @@
"ko_KR": "{0:n0} Mb 저장",
"no_NO": "Spare {0:n0} Mb",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Salvo {0:n0} Mb",
"ru_RU": "Сохранить {0:n0} Мб",
"sv_SE": "Spara {0:n0} Mb",
"th_TH": "",
@@ -18037,7 +18637,7 @@
"ko_KR": "{0:n0}Mb 저장됨",
"no_NO": "Spart {0:n0} Mb",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Salvo {0:n0} Mb",
"ru_RU": "Сохранено {0:n0} Мб",
"sv_SE": "Sparade {0:n0} Mb",
"th_TH": "",
@@ -18064,7 +18664,7 @@
"pl_PL": "",
"pt_BR": "{0} - Informação",
"ru_RU": "{0} - Информация",
- "sv_SE": "",
+ "sv_SE": "{0} - Information",
"th_TH": "{0} – ข้อมูล",
"tr_TR": "{0} - Bilgi",
"uk_UA": "{0} - Інформація",
@@ -18212,7 +18812,7 @@
"ko_KR": "캐비닛 대화 상자",
"no_NO": "Dialogboks for kabinett",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Diálogo do Gabinete",
"ru_RU": "Сообщение кабинета",
"sv_SE": "Cabinet-dialog",
"th_TH": "",
@@ -18237,7 +18837,7 @@
"ko_KR": "Amiibo의 새 이름 입력하기",
"no_NO": "Skriv inn Amiiboens nye navn",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Digite o novo nome do seu Amiibo",
"ru_RU": "Введите новое имя вашего Amiibo",
"sv_SE": "Ange nya namnet för din Amiibo",
"th_TH": "",
@@ -18262,7 +18862,7 @@
"ko_KR": "지금 Amiibo를 스캔하세요.",
"no_NO": "Vennligst skann Amiiboene dine nå.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Por favor, escaneie seu Amiibo agora.",
"ru_RU": "Пожалуйста, отсканируйте свой Amiibo.",
"sv_SE": "Skanna din Amiibo nu.",
"th_TH": "",
@@ -18387,7 +18987,7 @@
"ko_KR": "지원되는 컨트롤러 :",
"no_NO": "Støttede kontrollere:",
"pl_PL": "Obsługiwane Kontrolery:",
- "pt_BR": "",
+ "pt_BR": "Controladores suportados:",
"ru_RU": "Поддерживаемые геймпады:",
"sv_SE": "Handkontroller som stöds:",
"th_TH": "คอนโทรลเลอร์ที่รองรับ:",
@@ -18437,7 +19037,7 @@
"ko_KR": "현재 구성이 유효하지 않습니다. 설정을 열고 입력을 다시 구성하십시오.",
"no_NO": "Din nåværende konfigurasjon er ugyldig. Åpne innstillinger og konfigurer inndata.",
"pl_PL": "Twoja aktualna konfiguracja jest nieprawidłowa. Otwórz ustawienia i skonfiguruj swoje wejścia.",
- "pt_BR": "",
+ "pt_BR": "Sua configuração atual é inválida. Abra as configurações e reconfigure suas entradas.",
"ru_RU": "Текущая конфигурация некорректна. Откройте параметры и перенастройте управление.",
"sv_SE": "Din aktuella konfiguration är ogiltig. Öppna inställningarna och konfigurera om din inmatning.",
"th_TH": "การกำหนดค่าปัจจุบันของคุณไม่ถูกต้อง กรุณาเปิดการตั้งค่าและกำหนดค่าอินพุตของคุณใหม่",
@@ -18462,7 +19062,7 @@
"ko_KR": "도킹 모드가 설정되었습니다. 휴대용 제어 기능을 비활성화해야 합니다.",
"no_NO": "Docked modus. Håndholdt kontroll skal være deaktivert.",
"pl_PL": "Ustawiony tryb zadokowany. Sterowanie przenośne powinno być wyłączone.",
- "pt_BR": "",
+ "pt_BR": "Modo ancorado definido. O controle portátil deve ser desabilitado.",
"ru_RU": "Используется стационарный режим. Управление в портативном режиме должно быть отключено.",
"sv_SE": "Dockat läge angivet. Handhållna kontroller bör inaktiveras.",
"th_TH": "ตั้งค่าด็อกโหมด ควรปิดใช้งานการควบคุมแบบแฮนด์เฮลด์",
@@ -19134,12 +19734,12 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
+ "ko_KR": "LED 설정",
+ "no_NO": "LED-innstillinger",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Configurações de LED",
"ru_RU": "",
- "sv_SE": "",
+ "sv_SE": "LED-inställningar",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
@@ -19387,7 +19987,7 @@
"ko_KR": "모드 디렉터리 선택",
"no_NO": "Velg modifikasjons mappe",
"pl_PL": "Wybierz katalog modów",
- "pt_BR": "",
+ "pt_BR": "Selecione o diretório do mod",
"ru_RU": "Выбрать папку с модами",
"sv_SE": "Välj moddkatalog",
"th_TH": "เลือกไดเรกทอรี Mods",
@@ -19412,7 +20012,7 @@
"ko_KR": "XCI 파일 확인 및 정리",
"no_NO": "Kontroller og trim XCI-filen",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Verifique e reduza o arquivo XCI",
"ru_RU": "Проверить и обрезать XCI файл",
"sv_SE": "Kontrollera och optimera XCI-filer",
"th_TH": "",
@@ -19437,7 +20037,7 @@
"ko_KR": "이 기능은 먼저 충분한 공간을 확보한 다음 XCI 파일을 트리밍하여 디스크 공간을 절약합니다.",
"no_NO": "Denne funksjonen kontrollerer først hvor mye plass som er ledig, og trimmer deretter XCI-filen for å spare diskplass.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Esta função primeiro verificará o espaço vazio e depois reduzirá o arquivo XCI para economizar espaço em disco.",
"ru_RU": "Эта функция сначала проверит наличие пустого пространства, а затем обрежет файл XCI, чтобы сэкономить место на диске.",
"sv_SE": "Denna funktion kommer först att kontrollera ledigt utrymme och sedan optimera XCI-filen för att spara diskutrymme.",
"th_TH": "",
@@ -19462,7 +20062,7 @@
"ko_KR": "현재 파일 크기 : {0:n}MB\n게임 데이터 크기 : {1:n}MB\n디스크 공간 절약 : {2:n}MB",
"no_NO": "Nåværende filstørrelse: 0:n MB\nSpilldatastørrelse: {1:n} MB\nDiskplassbesparelse: {2:n} MB",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Tamanho atual do arquivo: {0:n} MB\nTamanho dos dados do jogo: {1:n} MB\nEconomia de espaço em disco: {2:n} MB",
"ru_RU": "Размер текущего файла: {0:n} Мб\nРазмер игровых данных: {1:n} MB\nЭкономия дискового пространства: {2:n} Мб",
"sv_SE": "Aktuell filstorlek: {0:n} MB\nStorlek för speldata: {1:n} MB\nSparat diskutrymme: {2:n} MB",
"th_TH": "",
@@ -19487,7 +20087,7 @@
"ko_KR": "XCI 파일은 트리밍할 필요가 없습니다. 자세한 내용은 로그를 확인",
"no_NO": "XCI-filen trenger ikke å trimmes. Sjekk loggene for mer informasjon",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI não precisa ser reduzido. Verifique os logs para mais detalhes",
"ru_RU": "Файл XCI не нуждается в обрезке. Проверьте логи для получения более подробной информации",
"sv_SE": "XCI-filen behöver inte optimeras. Kontrollera loggen för mer information",
"th_TH": "",
@@ -19512,7 +20112,7 @@
"ko_KR": "XCI 파일은 트리밍을 해제할 수 없습니다. 자세한 내용은 로그를 확인",
"no_NO": "XCI-filen kan ikke trimmes. Sjekk loggene for mer informasjon",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI não pode ser desfeito. Verifique os logs para mais detalhes",
"ru_RU": "XCI файл не может быть обрезан. Проверьте логи для получения более подробной информации",
"sv_SE": "XCI-filen kan inte avoptimeras. Kontrollera loggen för mer information",
"th_TH": "",
@@ -19537,7 +20137,7 @@
"ko_KR": "XCI 파일은 읽기 전용이므로 쓰기 가능하게 만들 수 없습니다. 자세한 내용은 로그를 확인",
"no_NO": "XCI-filen er skrivebeskyttet og kunne ikke gjøres skrivbar. Sjekk loggene for mer informasjon",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI é somente leitura e não pôde ser tornado gravável. Verifique os logs para mais detalhes",
"ru_RU": "Файл XCI доступен только для чтения и его невозможно сделать доступным для записи. Проверьте логи для получения более подробной информации",
"sv_SE": "XCI-filen är skrivskyddad och kunde inte göras skrivbar. Kontrollera loggen för mer information",
"th_TH": "",
@@ -19562,7 +20162,7 @@
"ko_KR": "XCI 파일이 스캔된 후 크기가 변경되었습니다. 파일이 쓰여지고 있지 않은지 확인하고 다시 시도하세요.",
"no_NO": "XCI File har endret størrelse siden den ble skannet. Kontroller at det ikke skrives til filen, og prøv på nytt.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI mudou de tamanho desde que foi escaneado. Verifique se o arquivo não está sendo gravado e tente novamente.",
"ru_RU": "Файл XCI изменился в размере после сканирования. Проверьте, не производится ли запись в этот файл, и повторите попытку.",
"sv_SE": "XCI-filen har ändrats i storlek sedan den lästes av. Kontrollera att filen inte skrivs till och försök igen.",
"th_TH": "",
@@ -19587,7 +20187,7 @@
"ko_KR": "XCI 파일에 여유 공간 영역에 데이터가 있으므로 트리밍하는 것이 안전하지 않음",
"no_NO": "XCI-filen har data i ledig plass, og det er ikke trygt å trimme den",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI tem dados na área de espaço livre, não é seguro reduzi-lo",
"ru_RU": "XCI файл содержит данные в пустой зоне, обрезать его небезопасно",
"sv_SE": "XCI-filen har data i det lediga utrymmet. Den är inte säker att optimera",
"th_TH": "",
@@ -19612,7 +20212,7 @@
"ko_KR": "XCI 파일에 유효하지 않은 데이터가 포함되어 있습니다. 자세한 내용은 로그를 확인",
"no_NO": "XCI-filen inneholder ugyldige data. Sjekk loggene for ytterligere detaljer",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI contém dados inválidos. Verifique os logs para obter mais detalhes",
"ru_RU": "Файл XCI содержит недопустимые данные. Проверьте логи для получения дополнительной информации",
"sv_SE": "XCI-filen innehåller ogiltig data. Kontrollera loggen för mer information",
"th_TH": "",
@@ -19637,7 +20237,7 @@
"ko_KR": "XCI 파일을 쓰기 위해 열 수 없습니다. 자세한 내용은 로그를 확인",
"no_NO": "XCI-filen kunne ikke åpnes for skriving. Sjekk loggene for ytterligere detaljer",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O arquivo XCI não pôde ser aberto para gravação. Verifique os logs para mais detalhes",
"ru_RU": "XCI файл не удалось открыть для записи. Проверьте логи для получения дополнительной информации",
"sv_SE": "XCI-filen kunde inte öppnas för skrivning. Kontrollera loggen för mer information",
"th_TH": "",
@@ -19662,7 +20262,7 @@
"ko_KR": "XCI 파일 트리밍에 실패",
"no_NO": "Trimming av XCI-filen mislyktes",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "A redução do arquivo XCI falhou",
"ru_RU": "Обрезка файла XCI не удалась",
"sv_SE": "Optimering av XCI-filen misslyckades",
"th_TH": "",
@@ -19687,7 +20287,7 @@
"ko_KR": "작업이 취소됨",
"no_NO": "Operasjonen ble avlyst",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "A operação foi cancelada",
"ru_RU": "Операция была отменена",
"sv_SE": "Åtgärden avbröts",
"th_TH": "",
@@ -19712,7 +20312,7 @@
"ko_KR": "작업이 수행되지 않음",
"no_NO": "Ingen operasjon ble utført",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Nenhuma operação foi realizada",
"ru_RU": "Операция не была проведена",
"sv_SE": "Ingen åtgärd genomfördes",
"th_TH": "",
@@ -19862,7 +20462,7 @@
"ko_KR": "XCI 파일 트리머",
"no_NO": "",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Redutor de arquivo XCI",
"ru_RU": "Уменьшение размера XCI файлов",
"sv_SE": "Optimera XCI-filer",
"th_TH": "",
@@ -19887,7 +20487,7 @@
"ko_KR": "{1}개 타이틀 중 {0}개 선택됨",
"no_NO": "{0} av {1} Valgte tittel(er)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "{0} de {1} Título(s) Selecionado(s)",
"ru_RU": "{0} из {1} файла(ов) выбрано",
"sv_SE": "{0} av {1} spel markerade",
"th_TH": "",
@@ -19912,7 +20512,7 @@
"ko_KR": "{1}개 타이틀 중 {0}개 선택됨({2}개 표시됨)",
"no_NO": "{0} av {1} Tittel(er) valgt ({2} vises)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "{0} de {1} Título(s) Selecionado(s) ({2} exibidos)",
"ru_RU": "{0} из {1} файла(ов) выбрано ({2} показано)",
"sv_SE": "{0} av {1} spel markerade ({2} visade)",
"th_TH": "",
@@ -19937,7 +20537,7 @@
"ko_KR": "{0}개의 타이틀을 트리밍 중...",
"no_NO": "Trimming av {0} tittel(er)...",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Reduzindo {0} Título(s)...",
"ru_RU": "Обрезка {0} файла(ов)...",
"sv_SE": "Optimerar {0} spel...",
"th_TH": "",
@@ -19962,7 +20562,7 @@
"ko_KR": "{0}개의 타이틀을 트리밍 해제 중...",
"no_NO": "Untrimming {0} Tittel(er)...",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Desfazendo {0} Título(s)...",
"ru_RU": "Отмена обрезки {0} файла(ов)...",
"sv_SE": "Avoptimerar {0} spel...",
"th_TH": "",
@@ -19987,7 +20587,7 @@
"ko_KR": "실패",
"no_NO": "Mislyktes",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Falhou",
"ru_RU": "Ошибка",
"sv_SE": "Misslyckades",
"th_TH": "",
@@ -20012,7 +20612,7 @@
"ko_KR": "잠재적 비용 절감",
"no_NO": "Potensielle besparelser",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Economia potencial",
"ru_RU": "Потенциально освобождено места",
"sv_SE": "Möjlig besparning",
"th_TH": "",
@@ -20037,7 +20637,7 @@
"ko_KR": "실제 비용 절감",
"no_NO": "Faktiske besparelser",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Economia real",
"ru_RU": "Реально освобождено места",
"sv_SE": "Faktisk besparning",
"th_TH": "",
@@ -20087,7 +20687,7 @@
"ko_KR": "표시됨 선택",
"no_NO": "Velg vist",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Selecionar mostrado(s)",
"ru_RU": "Выбрать то что показано",
"sv_SE": "Markera visade",
"th_TH": "",
@@ -20112,7 +20712,7 @@
"ko_KR": "표시됨 선택 취소",
"no_NO": "Opphev valg av Vist",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Desmarcar mostrado(s)",
"ru_RU": "Отменить выбор показанного",
"sv_SE": "Avmarkera visade",
"th_TH": "",
@@ -20137,7 +20737,7 @@
"ko_KR": "타이틀",
"no_NO": "Tittel",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Título",
"ru_RU": "Приложение",
"sv_SE": "Titel",
"th_TH": "",
@@ -20162,7 +20762,7 @@
"ko_KR": "공간 절약s",
"no_NO": "Plassbesparelser",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Economia de espaço",
"ru_RU": "Сохранение места на диске",
"sv_SE": "Utrymmesbesparning",
"th_TH": "",
@@ -20187,7 +20787,7 @@
"ko_KR": "트림",
"no_NO": "",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Reduzir",
"ru_RU": "Обрезать",
"sv_SE": "Optimera",
"th_TH": "",
@@ -20212,7 +20812,7 @@
"ko_KR": "언트림",
"no_NO": "Utrim",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Desfazer",
"ru_RU": "Отмена обрезки",
"sv_SE": "Avoptimera",
"th_TH": "",
@@ -20362,7 +20962,7 @@
"ko_KR": "{0} DLC 사용 가능",
"no_NO": "{0} Nedlastbare innhold(er)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "{0} DLC(s) disponíveis",
"ru_RU": "{0} доступных DLC",
"sv_SE": "{0} DLC(er) tillgängliga",
"th_TH": "",
@@ -20562,7 +21162,7 @@
"ko_KR": "계속",
"no_NO": "Fortsett",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Continuar",
"ru_RU": "Продолжить",
"sv_SE": "Fortsätt",
"th_TH": "",
@@ -20912,7 +21512,7 @@
"ko_KR": "에뮬레이터에서 사용할 그래픽 후단부를 선택합니다.\n\nVulkan은 드라이버가 최신 상태인 한 모든 최신 그래픽 카드에 전반적으로 더 좋습니다. Vulkan은 또한 모든 GPU 공급업체에서 더 빠른 셰이더 컴파일(덜 끊김)을 제공합니다.\n\nOpenGL은 오래된 Nvidia GPU, Linux의 오래된 AMD GPU 또는 VRAM이 낮은 GPU에서 더 나은 결과를 얻을 수 있지만 셰이더 컴파일 끊김이 더 큽니다.\n\n모르면 Vulkan으로 설정합니다. 최신 그래픽 드라이버를 사용해도 GPU가 Vulkan을 지원하지 않는 경우 OpenGL로 설정하세요..",
"no_NO": "Velg grafikkbackend som skal brukes i emulatoren.\n\nVulkan er generelt bedre for alle moderne grafikkort, så lenge driverne er oppdatert. Vulkan har også en raskere sammenstilling av Shader (mindre hakkete) på alle GPU-leverandører.\n\nOpenGL kan oppnå bedre resultater for eldre Nvidia GPU-er, på eldre AMD GPU-er på Linux, eller på GPU-er med lavere VRAM, selv om skyggekompileringsutløser vil være større.\n\nSett til Vulkan hvis du er usikker. Sett til OpenGL hvis ikke GPU-en støtter Vulkan selv med de nyeste grafikkdriverne.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Selecione o backend gráfico que será usado no emulador.\n\nO Vulkan é melhor no geral para todas as placas de vídeo modernas, desde que seus drivers estejam atualizados. O Vulkan também apresenta compilação de shader mais rápida (menos travamentos) em todos os fornecedores de GPU.\n\nO OpenGL pode obter melhores resultados em GPUs Nvidia antigas, em GPUs AMD antigas no Linux ou em GPUs com VRAM menor, embora os travamentos de compilação de shader sejam maiores.\n\nDefina como Vulkan se não tiver certeza. Defina como OpenGL se sua GPU não suportar Vulkan, mesmo com os drivers gráficos mais recentes.",
"ru_RU": "Выбирает бэкенд, который будет использован в эмуляторе.\n\nVulkan является лучшим выбором для всех современных графических карт с актуальными драйверами. В Vulkan также включена более быстрая компиляция шейдеров (меньше статтеров) для всех видеоадаптеров.\n\nПри использовании OpenGL можно достичь лучших результатов на старых видеоадаптерах Nvidia и AMD в Linux или на видеоадаптерах с небольшим количеством видеопамяти, хотя статтеров при компиляции шейдеров будет больше.\n\nРекомендуется использовать Vulkan. Используйте OpenGL, если ваш видеоадаптер не поддерживает Vulkan даже с актуальными драйверами.",
"sv_SE": "Väljer den grafikbakände som ska användas i emulatorn.\n\nVulkan är oftast bättre för alla moderna grafikkort, så länge som deras drivrutiner är uppdaterade. Vulkan har också funktioner för snabbare shader compilation (mindre stuttering) för alla GPU-tillverkare.\n\nOpenGL kan nå bättre resultat på gamla Nvidia GPU:er, på äldre AMD GPU:er på Linux, eller på GPU:er med lägre VRAM, även om shader compilation stuttering kommer att vara större.\n\nStäll in till Vulkan om du är osäker. Ställ in till OpenGL om du GPU inte har stöd för Vulkan även med de senaste grafikdrivrutinerna.",
"th_TH": "เลือกกราฟิกเบื้องหลังที่จะใช้ในโปรแกรมจำลอง\n\nโดยรวมแล้ว Vulkan นั้นดีกว่าสำหรับการ์ดจอรุ่นใหม่ทั้งหมด ตราบใดที่ไดรเวอร์ยังอัพเดทอยู่เสมอ Vulkan ยังมีคุณสมบัติการคอมไพล์เชเดอร์ที่เร็วขึ้น(และลดอาการกระตุก) สำหรับ GPU อื่นๆทุกอัน\n\nOpenGL อาจได้รับผลลัพธ์ที่ดีกว่าบน Nvidia GPU รุ่นเก่า, AMD GPU รุ่นเก่าบน Linux หรือบน GPU ที่มี VRAM น้อย แม้ว่าการคอมไพล์เชเดอร์ จะทำให้อาการกระตุกมากขึ้นก็ตาม\n\nตั้งค่าเป็น Vulkan หากไม่แน่ใจ ตั้งค่าเป็น OpenGL หาก GPU ของคุณไม่รองรับ Vulkan แม้จะมีไดรเวอร์กราฟิกล่าสุดก็ตาม",
@@ -20962,7 +21562,7 @@
"ko_KR": "Vulkan을 사용합니다.\nARM 맥에서 해당 플랫폼에서 잘 실행되는 게임을 플레이하는 경우 Metal 후단부를 사용합니다.",
"no_NO": "Bruker Vulkan \nPå en ARM Mac, og når du spiller et spill som kjører bra under den, bruker du Metal-backend.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Usa Vulkan.\nEm um Mac ARM, e ao jogar um jogo que roda bem nele, usa o backend Metal.",
"ru_RU": "Использует Vulkan.\nНа Mac с ARM процессорами используется Metal, если игра с ним совместима и хорошо работает.",
"sv_SE": "Använder Vulkan.\nPå en ARM Mac och vid spel som körs bra på den så används Metal-bakänden.",
"th_TH": "",
@@ -21012,7 +21612,7 @@
"ko_KR": "VRAM 사용량을 줄이기 위해 ASTC 텍스처를 압축합니다.\n\n이 텍스처 형식을 사용하는 게임에는 Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder, The Legend of Zelda: Tears of the Kingdom이 있습니다.\n\n4GiB VRAM 이하의 그래픽 카드는 이러한 게임을 실행하는 동안 어느 시점에서 충돌할 가능성이 있습니다.\n\n위에서 언급한 게임에서 VRAM이 부족한 경우에만 활성화합니다. 모르면 끔으로 두세요.",
"no_NO": "Kompresser ASTC-teksturer for å redusere VRAM-bruk.\n\nSpill som bruker dette teksturformatet, inkluderer Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGrafikkkort med 4GiB VRAM eller mindre, vil sannsynligvis krasje på et tidspunkt når spillene kjører.\n\nAktiver bare hvis du går tom for VRAM på nevnte spill. La AV om du er usikker.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Compacta texturas ASTC para reduzir o uso de VRAM.\n\nJogos que usam esse formato de textura incluem Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder e The Legend of Zelda: Tears of the Kingdom.\n\nPlacas gráficas com 4GB VRAM ou menos provavelmente travarão em algum momento durante a execução desses jogos.\n\nHabilite somente se estiver ficando sem VRAM nos jogos mencionados acima. Deixe DESLIGADO se não tiver certeza.",
"ru_RU": "Сжатие ASTC текстур для уменьшения использования VRAM. \n\nИгры, использующие этот формат текстур: Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder и The Legend of Zelda: Tears of the Kingdom. \nНа видеоадаптерах с 4GiB видеопамяти или менее возможны вылеты при запуске этих игр. \n\nВключите, только если у вас заканчивается видеопамять в вышеупомянутых играх. \n\nРекомендуется оставить выключенным.",
"sv_SE": "Komprimerar ASTC-texturer för att minska VRAM-användning.\n\nSpel som använder detta texturformat inkluderar Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder och The Legend of Zelda: Tears of the Kingdom.\n\nGrafikkort med 4GiB VRAM eller mindre kommer sannolikt krascha någon gång när du kör dessa spel.\n\nAktivera endast om du har slut på VRAM på ovan nämnda spel. Lämna AV om du är osäker.",
"th_TH": "บีบอัดพื้นผิว ASTC เพื่อลดการใช้งาน VRAM\n\nเกมที่ใช้รูปแบบพื้นผิวนี้ ได้แก่ Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder และ The Legend of Zelda: Tears of the Kingdom\n\nการ์ดจอที่มี 4GiB VRAM หรือน้อยกว่ามีแนวโน้มที่จะพังในบางจุดขณะเล่นเกมเหล่านี้\n\nเปิดใช้งานเฉพาะในกรณีที่ VRAM ของคุณใกล้หมดในเกมที่กล่าวมาข้างต้น ปล่อยให้ปิดหากไม่แน่ใจ",
@@ -21339,7 +21939,7 @@
"pl_PL": "Głoś",
"pt_BR": "",
"ru_RU": "Громкость",
- "sv_SE": "",
+ "sv_SE": "Volym",
"th_TH": "ระดับเสียง",
"tr_TR": "Ses",
"uk_UA": "Гуч.",
@@ -22059,7 +22659,7 @@
"he_IL": "ממשק רשת",
"it_IT": "Interfaccia di rete:",
"ja_JP": "ネットワークインタフェース:",
- "ko_KR": "네트워크 인터페이스:",
+ "ko_KR": "네트워크 인터페이스 :",
"no_NO": "Nettverksgrensesnitt",
"pl_PL": "Interfejs sieci:",
"pt_BR": "Interface de rede:",
@@ -22212,7 +22812,7 @@
"ko_KR": "멀티플레이어",
"no_NO": "Flerspiller",
"pl_PL": "Gra Wieloosobowa",
- "pt_BR": "",
+ "pt_BR": "Multijogador",
"ru_RU": "Мультиплеер",
"sv_SE": "Flerspelare",
"th_TH": "ผู้เล่นหลายคน",
@@ -22362,7 +22962,7 @@
"ko_KR": "P2P 네트워크 호스팅 비활성화(대기 시간이 늘어날 수 있음)",
"no_NO": "Deaktiver P2P-nettverkshosting (kan øke ventetiden)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Desabilitar hospedagem de rede P2P (pode aumentar a latência)",
"ru_RU": "Отключить хостинг P2P-сетей (может увеличить задержку)",
"sv_SE": "Inaktivera P2P-nätverkshosting (kan öka latens)",
"th_TH": "",
@@ -22387,7 +22987,7 @@
"ko_KR": "P2P 네트워크 호스팅을 비활성화하면 피어가 직접 연결하지 않고 마스터 서버를 통해 프록시합니다.",
"no_NO": "Deaktiver P2P-nettverkshosting, så vil andre brukere gå via hovedserveren i stedet for å koble seg direkte til deg.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Desabilite a hospedagem de rede P2P, os pares farão proxy através do servidor mestre em vez de se conectarem a você diretamente.",
"ru_RU": "Отключая хостинг P2P-сетей, пользователи будут проксироваться через главный сервер, а не подключаться к вам напрямую.",
"sv_SE": "Inaktivera P2P-nätverkshosting, motparter kommer skickas genom masterservern isället för att ansluta direkt till dig.",
"th_TH": "",
@@ -22412,7 +23012,7 @@
"ko_KR": "네트워크 암호 문구 :",
"no_NO": "Nettverkspassord:",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Senha de rede:",
"ru_RU": "Cетевой пароль:",
"sv_SE": "Lösenfras för nätverk:",
"th_TH": "",
@@ -22437,7 +23037,7 @@
"ko_KR": "귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.",
"no_NO": "Du vil bare kunne se spill som er arrangert med samme passordfrase som deg.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Você só poderá ver jogos hospedados com a mesma senha que você.",
"ru_RU": "Вы сможете видеть только те игры, в которых используется тот же пароль, что и у вас.",
"sv_SE": "Du kommer endast kunna se hostade spel med samma lösenfras som du.",
"th_TH": "",
@@ -22462,7 +23062,7 @@
"ko_KR": "Ryujinx-<8 hex chars> 형식으로 암호를 입력하세요. 귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.",
"no_NO": "Skriv inn en passordfrase i formatet Ryujinx-<8 heks tegn>. Du vil bare kunne se spill som er arrangert med samme passordfrase som deg.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Insira uma frase-senha no formato Ryujinx-<8 hex chars>. Você só poderá ver jogos hospedados com a mesma frase-senha que você.",
"ru_RU": "Введите пароль в формате Ryujinx-<8 шестнадцатеричных символов>. Вы сможете видеть только те игры, в которых используется тот же пароль, что и у вас.",
"sv_SE": "Ange en lösenfras i formatet Ryujinx-<8 hextecken>. Du kommer endast kunna se hostade spel med samma lösenfras som du.",
"th_TH": "",
@@ -22487,7 +23087,7 @@
"ko_KR": "(일반)",
"no_NO": "(offentlig)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "(público)",
"ru_RU": "(публичный)",
"sv_SE": "(publik)",
"th_TH": "",
@@ -22512,7 +23112,7 @@
"ko_KR": "무작위 생성",
"no_NO": "Generer tilfeldig",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Gerar Aleatório",
"ru_RU": "Сгенерировать рандомно",
"sv_SE": "Generera slumpmässigt",
"th_TH": "",
@@ -22537,7 +23137,7 @@
"ko_KR": "다른 플레이어와 공유할 수 있는 새로운 암호 문구를 생성합니다.",
"no_NO": "Genererer en ny passordfrase, som kan deles med andre spillere.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Gera uma nova senha, que pode ser compartilhada com outros jogadores.",
"ru_RU": "Генерирует новый пароль, который можно передать другим игрокам.",
"sv_SE": "Genererar en ny lösenfras som kan delas med andra spelare.",
"th_TH": "",
@@ -22562,7 +23162,7 @@
"ko_KR": "지우기",
"no_NO": "Slett",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Limpar",
"ru_RU": "Очистить",
"sv_SE": "Töm",
"th_TH": "",
@@ -22587,7 +23187,7 @@
"ko_KR": "현재 암호를 지우고 공용 네트워크로 돌아갑니다.",
"no_NO": "Sletter den gjeldende passordfrasen og går tilbake til det offentlige nettverket.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Limpa a senha atual, retornando à rede pública.",
"ru_RU": "Очищает текущий пароль, возвращаясь в публичную сеть.",
"sv_SE": "Tömmer aktuell lösenfras och återgår till det publika nätverket.",
"th_TH": "",
@@ -22612,7 +23212,7 @@
"ko_KR": "유효하지 않은 암호입니다! \"Ryujinx-<8 hex chars>\" 형식이어야 합니다.",
"no_NO": "Ugyldig passordfrase! Må være i formatet \"Ryujinx-<8 hex tegn>\"",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Frase-senha inválida! Deve estar no formato \"Ryujinx-<8 hex chars>\"",
"ru_RU": "Неверный пароль! Пароль должен быть в формате \"Ryujinx-<8 шестнадцатеричных символов>\"",
"sv_SE": "Ogiltig lösenfras! Måste vara i formatet \"Ryujinx-<8 hextecken>\"",
"th_TH": "",
@@ -22662,7 +23262,7 @@
"ko_KR": "사용자 정의 주사율 활성화(실험적)",
"no_NO": "Aktiver egendefinert oppdateringsfrekvens (eksperimentell)",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Habilitar taxa de atualização personalizada (Experimental)",
"ru_RU": "Включить пользовательскую частоту кадров (Экспериментально)",
"sv_SE": "Aktivera anpassad uppdateringsfrekvens (experimentell)",
"th_TH": "",
@@ -22712,7 +23312,7 @@
"ko_KR": "무제한",
"no_NO": "Ubegrenset",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Ilimitado",
"ru_RU": "Без ограничений",
"sv_SE": "Obunden",
"th_TH": "",
@@ -22737,7 +23337,7 @@
"ko_KR": "사용자 정의 주사율",
"no_NO": "Egendefinert oppdateringsfrekvens",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Taxa de atualização personalizada",
"ru_RU": "Пользовательская частота кадров",
"sv_SE": "Anpassad uppdateringsfrekvens",
"th_TH": "",
@@ -22751,7 +23351,7 @@
"ID": "SettingsTabSystemVSyncModeTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Emulierte vertikale Synchronisation. \"Switch\" emuliert die 60Hz-Bildwiederholfrequenz der Switch. \"Unbounded\" ist eine unbegrenzte Bildwiederholfrequenz.",
"el_GR": "",
"en_US": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.",
"es_ES": "",
@@ -22762,7 +23362,7 @@
"ko_KR": "에뮬레이트된 수직 동기화. '스위치'는 스위치의 60Hz 주사율을 에뮬레이트합니다. '무한'은 무제한 주사율입니다.",
"no_NO": "Emulert vertikal synkronisering. «Switch» emulerer Switchs oppdateringsfrekvens på 60 Hz. «Ubegrenset» er en ubegrenset oppdateringsfrekvens.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Sincronização vertical emulada. 'Switch' emula a taxa de atualização de 60 Hz do Switch. 'Ilimitada' é uma taxa de atualização sem limite.",
"ru_RU": "Эмулированная вертикальная синхронизация. 'Консоль' эмулирует частоту обновления консоли, равную 60 Гц. 'Без ограничений' - неограниченная частота кадров.",
"sv_SE": "Emulerad vertikal synk. 'Switch' emulerar Switchens uppdateringsfrekvens på 60Hz. 'Obunden' är en obegränsad uppdateringsfrekvens.",
"th_TH": "",
@@ -22776,7 +23376,7 @@
"ID": "SettingsTabSystemVSyncModeTooltipCustom",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Emulierte vertikale Synchronisation. \"Switch\" emuliert die 60Hz-Bildwiederholfrequenz der Switch. „Unbounded“ ist eine unbegrenzte Bildwiederholfrequenz. „Benutzerdefinierte Bildwiederholfrequenz“ emuliert die angegebene benutzerdefinierte Bildwiederholfrequenz.",
"el_GR": "",
"en_US": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom Refresh Rate' emulates the specified custom refresh rate.",
"es_ES": "",
@@ -22787,7 +23387,7 @@
"ko_KR": "에뮬레이트된 수직 동기화. '스위치'는 스위치의 60Hz 주사율을 에뮬레이트합니다. '무한'은 무제한 주사율입니다. '사용자 지정'은 지정된 사용자 지정 주사율을 에뮬레이트합니다.",
"no_NO": "Emulert vertikal synkronisering. «Switch» emulerer Switchs oppdateringsfrekvens på 60 Hz. «Ubegrenset» er en ubegrenset oppdateringsfrekvens. «Egendefinert» emulerer den angitte egendefinerte oppdateringsfrekvensen.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Sincronização Vertical Emulada. 'Switch' emula a taxa de atualização de 60 Hz do Switch. 'Ilimitada' é uma taxa de atualização sem limite. 'Taxa de atualização personalizada' emula a taxa de atualização personalizada especificada.",
"ru_RU": "Эмулированная вертикальная синхронизация. 'Консоль' эмулирует частоту обновления консоли, равную 60 Гц. 'Без ограничений' - неограниченная частота кадров. 'Пользовательска частота кадров' эмулирует выбранную пользователем частоту кадров.",
"sv_SE": "Emulerad vertikal synk. 'Switch' emulerar Switchens uppdateringsfrekvens på 60Hz. 'Obunden' är en obegränsad uppdateringsfrekvens. 'Anpassad uppdateringsfrekvens' emulerar den angivna anpassade uppdateringsfrekvensen.",
"th_TH": "",
@@ -22801,7 +23401,7 @@
"ID": "SettingsTabSystemEnableCustomVSyncIntervalTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Ermöglicht es dem Benutzer, eine emulierte Bildwiederholfrequenz festzulegen. In einigen Titeln kann dies die Geschwindigkeit der Spiel-Logik erhöhen oder verringern. In anderen Titeln kann dies dazu führen, dass die FPS auf ein Vielfaches der Bildwiederholfrequenz begrenzt werden oder zu unvorhersehbarem Verhalten führen. Dies ist eine experimentelle Funktion, ohne Garantien dafür, wie sich das Gameplay auswirkt. \n\nLassen Sie diese Option deaktiviert, wenn Sie sich nicht sicher sind.",
"el_GR": "",
"en_US": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.",
"es_ES": "",
@@ -22812,7 +23412,7 @@
"ko_KR": "사용자가 에뮬레이트된 화면 주사율을 지정할 수 있습니다. 일부 타이틀에서는 게임플레이 로직 속도가 빨라지거나 느려질 수 있습니다. 다른 타이틀에서는 주사율의 배수로 FPS를 제한하거나 예측할 수 없는 동작으로 이어질 수 있습니다. 이는 실험적 기능으로 게임 플레이에 어떤 영향을 미칠지 보장할 수 없습니다. \n\n모르면 끔으로 두세요.",
"no_NO": "Gjør det mulig for brukeren å angi en emulert oppdateringsfrekvens. I noen titler kan dette øke eller senke hastigheten på spillogikken. I andre titler kan det gjøre det mulig å begrense FPS til et multiplum av oppdateringsfrekvensen, eller føre til uforutsigbar oppførsel. Dette er en eksperimentell funksjon, og det gis ingen garantier for hvordan spillingen påvirkes. \n\nLa AV stå hvis du er usikker.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Permite que o usuário especifique uma taxa de atualização emulada. Em alguns títulos, isso pode acelerar ou desacelerar a taxa de lógica do jogo. Em outros títulos, pode permitir limitar o FPS em algum múltiplo da taxa de atualização ou levar a um comportamento imprevisível. Este é um recurso experimental, sem garantias de como o jogo será afetado. \n\nDeixe OFF se não tiver certeza.",
"ru_RU": "Позволяет пользователю указать эмулируемую частоту кадров. В некоторых играх это может ускорить или замедлить скорость логики игрового процесса. В других играх это может позволить ограничить FPS на уровне, кратном частоте обновления, или привести к непредсказуемому поведению. Это экспериментальная функция, и нет никаких гарантий того, как она повлияет на игровой процесс. \n\nОставьте выключенным, если не уверены.",
"sv_SE": "Låter användaren ange en emulerad uppdateringsfrekvens. För vissa spel så kan detta snabba upp eller ner frekvensen för spellogiken. I andra spel så kan detta tillåta att bildfrekvensen kapas för delar av uppdateringsfrekvensen eller leda till oväntat beteende. Detta är en experimentell funktion utan några garantier för hur spelet påverkas. \n\nLämna AV om du är osäker.",
"th_TH": "",
@@ -22826,7 +23426,7 @@
"ID": "SettingsTabSystemCustomVSyncIntervalValueTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Der Zielwert für die benutzerdefinierte Bildwiederholfrequenz.",
"el_GR": "",
"en_US": "The custom refresh rate target value.",
"es_ES": "",
@@ -22837,7 +23437,7 @@
"ko_KR": "사용자 정의 주사율 목표 값입니다.",
"no_NO": "Den egendefinerte målverdien for oppdateringsfrekvens.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "O valor alvo da taxa de atualização personalizada.",
"ru_RU": "Заданное значение частоты кадров",
"sv_SE": "Målvärde för anpassad uppdateringsfrekvens.",
"th_TH": "",
@@ -22851,7 +23451,7 @@
"ID": "SettingsTabSystemCustomVSyncIntervalSliderTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Die benutzerdefinierte Bildwiederholfrequenz als Prozentsatz der normalen Switch-Bildwiederholfrequenz.",
"el_GR": "",
"en_US": "The custom refresh rate, as a percentage of the normal Switch refresh rate.",
"es_ES": "",
@@ -22862,7 +23462,7 @@
"ko_KR": "일반 스위치 주사율의 백분율로 나타낸 사용자 지정 주사율입니다.",
"no_NO": "Den egendefinerte oppdateringsfrekvensen, i prosent av den normale oppdateringsfrekvensen for Switch-konsollen.",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "A taxa de atualização personalizada, como uma porcentagem da taxa de atualização normal do Switch.",
"ru_RU": "Пользовательская частота кадров в процентах от обычной частоты обновления на консоли.",
"sv_SE": "Anpassad uppdateringsfrekvens, som en procentdel av den normala uppdateringsfrekvensen för Switch.",
"th_TH": "",
@@ -22876,7 +23476,7 @@
"ID": "SettingsTabSystemCustomVSyncIntervalPercentage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Benutzerdefinierte Bildwiederholfrequenz %:",
"el_GR": "",
"en_US": "Custom Refresh Rate %:",
"es_ES": "",
@@ -22887,7 +23487,7 @@
"ko_KR": "사용자 정의 주사율 % :",
"no_NO": "Egendefinert oppdateringsfrekvens %:",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Taxa de atualização personalizada %:",
"ru_RU": "Пользовательская частота кадров %:",
"sv_SE": "Anpassad uppdateringsfrekvens %:",
"th_TH": "",
@@ -22901,7 +23501,7 @@
"ID": "SettingsTabSystemCustomVSyncIntervalValue",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Wert für benutzerdefinierte Bildwiederholfrequenz:",
"el_GR": "",
"en_US": "Custom Refresh Rate Value:",
"es_ES": "",
@@ -22912,7 +23512,7 @@
"ko_KR": "사용자 정의 주사율 값 :",
"no_NO": "Egendefinert verdi for oppdateringsfrekvens:",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Valor da taxa de atualização personalizada:",
"ru_RU": "Значение пользовательской частоты кадров:",
"sv_SE": "Värde för anpassad uppdateringsfrekvens:",
"th_TH": "",
@@ -22937,7 +23537,7 @@
"ko_KR": "간격",
"no_NO": "Intervall",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Intervalo",
"ru_RU": "Интервал",
"sv_SE": "Intervall",
"th_TH": "",
@@ -22951,7 +23551,7 @@
"ID": "SettingsTabHotkeysToggleVSyncModeHotkey",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "VSync-Modus umschalten:",
"el_GR": "",
"en_US": "Toggle VSync mode:",
"es_ES": "",
@@ -22962,7 +23562,7 @@
"ko_KR": "수직 동기화 모드 전환 :",
"no_NO": "Veksle mellom VSync-modus:",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Alternar modo VSync:",
"ru_RU": "Выбрать режим вертикальной синхронизации:",
"sv_SE": "Växla VSync-läge:",
"th_TH": "",
@@ -22976,7 +23576,7 @@
"ID": "SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Benutzerdefinierte Bildwiederholfrequenz erhöhen:",
"el_GR": "",
"en_US": "Raise custom refresh rate",
"es_ES": "",
@@ -22987,7 +23587,7 @@
"ko_KR": "사용자 정의 주사율 증가",
"no_NO": "Øk den egendefinerte oppdateringsfrekvensen",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Aumentar taxa de atualização personalizada",
"ru_RU": "Повышение пользовательской частоты кадров",
"sv_SE": "Höj anpassad uppdateringsfrekvens",
"th_TH": "",
@@ -23001,7 +23601,7 @@
"ID": "SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Benutzerdefinierte Bildwiederholfrequenz senken:",
"el_GR": "",
"en_US": "Lower custom refresh rate:",
"es_ES": "",
@@ -23012,7 +23612,7 @@
"ko_KR": "사용자 정의 주사율 감소",
"no_NO": "Lavere tilpasset oppdateringsfrekvens",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Reduzir Taxa de atualização personalizada:",
"ru_RU": "Понижение пользовательской частоты кадров",
"sv_SE": "Sänk anpassad uppdateringsfrekvens",
"th_TH": "",
@@ -23026,7 +23626,7 @@
"ID": "CompatibilityListLastUpdated",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Zuletzt aktualisiert: {0}",
"el_GR": "",
"en_US": "Last updated: {0}",
"es_ES": "",
@@ -23034,10 +23634,10 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
+ "ko_KR": "최종 업데이트 : {0}",
"no_NO": "Sist oppdatert: {0}",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Última atualização: {0}",
"ru_RU": "",
"sv_SE": "Senast uppdaterad: {0}",
"th_TH": "",
@@ -23051,7 +23651,7 @@
"ID": "CompatibilityListWarning",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Diese Kompatibilitätsliste könnte veraltete Einträge enthalten. Teste dennoch Spiele im \"Ingame\"-Status.",
"el_GR": "",
"en_US": "This compatibility list might contain out of date entries.\nDo not be opposed to testing games in the \"Ingame\" status.",
"es_ES": "",
@@ -23062,7 +23662,7 @@
"ko_KR": "이 호환성 목록에는 오래된 항목이 포함되어 있을 수 있습니다.\n\"게임 내\" 상태에서 게임을 테스트하는 것을 반대하지 마십시오.",
"no_NO": "Denne kompatibilitetslisten kan inneholde oppføringer som er tomme for data.\nVær ikke imot å teste spill i statusen «Ingame».",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Esta lista de compatibilidade pode estar desatualizada.\nNão se oponha a testar os jogos",
"ru_RU": "",
"sv_SE": "Denna kompatibilitetslista kan innehålla utdaterade poster.\nTesta gärna spelen som listas med \"Spelproblem\"-status.",
"th_TH": "",
@@ -23076,7 +23676,7 @@
"ID": "CompatibilityListSearchBoxWatermark",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Kompatibilitätseinträge durchsuchen...",
"el_GR": "",
"en_US": "Search compatibility entries...",
"es_ES": "",
@@ -23087,7 +23687,7 @@
"ko_KR": "호환성 항목 검색...",
"no_NO": "Søk i kompatibilitetsoppføringer...",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Pesquisa de compatibilidade",
"ru_RU": "",
"sv_SE": "Sök i kompatibilitetsposter...",
"th_TH": "",
@@ -23112,7 +23712,7 @@
"ko_KR": "호환성 목록 열기",
"no_NO": "Åpne kompatibilitetslisten",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Lista de Compatibilidade",
"ru_RU": "",
"sv_SE": "Öppna kompatibilitetslistan",
"th_TH": "",
@@ -23126,7 +23726,7 @@
"ID": "CompatibilityListOnlyShowOwnedGames",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Nur eigene Spiele anzeigen",
"el_GR": "",
"en_US": "Only show owned games",
"es_ES": "",
@@ -23137,7 +23737,7 @@
"ko_KR": "보유 게임만 표시",
"no_NO": "Vis bare eide spill",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Mostrar apenas jogos disponíveis",
"ru_RU": "",
"sv_SE": "Visa endast ägda spel",
"th_TH": "",
@@ -23151,7 +23751,7 @@
"ID": "CompatibilityListPlayable",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Spielbar",
"el_GR": "",
"en_US": "Playable",
"es_ES": "",
@@ -23162,7 +23762,7 @@
"ko_KR": "플레이 가능",
"no_NO": "Spillbar",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Jogável",
"ru_RU": "",
"sv_SE": "Spelbart",
"th_TH": "",
@@ -23176,7 +23776,7 @@
"ID": "CompatibilityListIngame",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Im Spiel",
"el_GR": "",
"en_US": "Ingame",
"es_ES": "",
@@ -23187,7 +23787,7 @@
"ko_KR": "게임 내",
"no_NO": "",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "No jogo",
"ru_RU": "",
"sv_SE": "Spelproblem",
"th_TH": "",
@@ -23212,7 +23812,7 @@
"ko_KR": "메뉴",
"no_NO": "Menyer",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Menu",
"ru_RU": "",
"sv_SE": "Menyer",
"th_TH": "",
@@ -23237,7 +23837,7 @@
"ko_KR": "부츠",
"no_NO": "Starter",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Inicializa",
"ru_RU": "",
"sv_SE": "Startar",
"th_TH": "",
@@ -23262,7 +23862,7 @@
"ko_KR": "없음",
"no_NO": "Ingenting",
"pl_PL": "",
- "pt_BR": "",
+ "pt_BR": "Nada",
"ru_RU": "",
"sv_SE": "Ingenting",
"th_TH": "",
@@ -23273,29 +23873,204 @@
}
},
{
- "ID": "ExtractAocListHeader",
+ "ID": "CompatibilityListPlayableTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
+ "en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Starter opp og spiller uten krasj eller GPU-feil av noe slag, og med en hastighet som er rask nok til å ha rimelig glede av på en gjennomsnittlig PC.",
+ "pl_PL": "",
+ "pt_BR": "Inicializa e roda sem travamentos ou bugs de GPU de qualquer tipo, e em uma velocidade rápida o suficiente para ser aproveitado em um PC comum.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "启动和游戏时不会出现任何崩溃或任何类型的 GPU bug 且速度足够快可以在一般 PC 上尽情游玩。",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "CompatibilityListIngameTooltip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Starter og går i gang i spillet, men lider av ett eller flere av følgende: krasjer, fastlåser, GPU-feil, distraherende dårlig lyd eller er rett og slett for tregt. Spillet kan fortsatt spilles helt til ende, men ikke slik det er ment å spilles.",
+ "pl_PL": "",
+ "pt_BR": "Inicializa e entra no jogo, mas sofre de um ou mais dos seguintes: travamentos, deadlocks, bugs de GPU, áudio ruim que distrai ou é simplesmente muito lento. O jogo ainda pode ser jogado até o fim, mas não da forma como foi criado para ser jogado.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "可以成功启动并进入游戏但可能会遇到以下一种或多种问题: 崩溃、卡死、GPU bug、令人无法接受的音频,或者只是太慢。仍然可以继续进行游戏,但是可能无法达到预期。",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "CompatibilityListMenusTooltip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Boots and goes past the title screen but does not make it into main gameplay.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Starter opp og går forbi tittelskjermen, men kommer ikke inn i hovedspillet.",
+ "pl_PL": "",
+ "pt_BR": "Inicializa e passa da tela de título, mas não entra no jogo principal.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "可以启动并通过标题画面但是无法进入到主要的游戏流程。",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "CompatibilityListBootsTooltip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Startet, kommt aber nicht über den Titelbildschirm hinaus.",
+ "el_GR": "",
+ "en_US": "Boots but does not make it past the title screen.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Starter, men kommer ikke lenger enn til tittelskjermen.",
+ "pl_PL": "",
+ "pt_BR": "Inizializa, mas não passa da tela de título.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "可以启动但是无法通过标题画面。",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "CompatibilityListNothingTooltip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Startet nicht oder zeigt keine Anzeichen von Aktivität.",
+ "el_GR": "",
+ "en_US": "Does not boot or shows no signs of activity.",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Starter ikke opp eller viser ingen tegn til aktivitet.",
+ "pl_PL": "",
+ "pt_BR": "Não inicializa ou não mostra sinais de atividade.",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "无法启动或显示无任何动静。",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ExtractAocListHeader",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Wähle ein DLC zum Extrahieren aus",
+ "el_GR": "",
"en_US": "Select a DLC to Extract",
"es_ES": "",
"fr_FR": "Choisissez un DLC à extraire",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
- "ko_KR": "",
+ "ko_KR": "추출할 DLC 선택",
"no_NO": "Velg en DLC og hente ut",
"pl_PL": "",
+ "pt_BR": "Selecione um DLC para extrair",
+ "ru_RU": "",
+ "sv_SE": "Välj en DLC att extrahera",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "选择一个要解压的 DLC",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameInfoRpcImage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Rich Presence Image",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Rikt nærværsbilde",
+ "pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
- "zh_CN": "选择一个要解压的 DLC",
+ "zh_CN": "Rich Presence 图像",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameInfoRpcDynamic",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Dynamic Rich Presence",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "Dynamisk og rik tilstedeværelse",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "动态 Rich Presence",
"zh_TW": ""
}
}
]
-}
\ No newline at end of file
+}
diff --git a/src/Ryujinx/Common/ThemeManager.cs b/src/Ryujinx/Common/ThemeManager.cs
deleted file mode 100644
index 6da01bfa7..000000000
--- a/src/Ryujinx/Common/ThemeManager.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-
-namespace Ryujinx.Ava.Common
-{
- public static class ThemeManager
- {
- public static event Action ThemeChanged;
-
- public static void OnThemeChanged()
- {
- ThemeChanged?.Invoke();
- }
- }
-}
diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs
index 20b296511..47fc8ad69 100644
--- a/src/Ryujinx/DiscordIntegrationModule.cs
+++ b/src/Ryujinx/DiscordIntegrationModule.cs
@@ -1,19 +1,15 @@
using DiscordRPC;
using Gommon;
-using MsgPack;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
+using Ryujinx.Ava.Utilities.PlayReport;
using Ryujinx.Common;
-using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
+using Ryujinx.Horizon.Prepo.Types;
using System.Text;
namespace Ryujinx.Ava
@@ -41,6 +37,9 @@ namespace Ryujinx.Ava
private static RichPresence _discordPresencePlaying;
private static ApplicationMetadata _currentApp;
+ public static bool HasAssetImage(string titleId) => TitleIDs.DiscordGameAssetKeys.ContainsIgnoreCase(titleId);
+ public static bool HasAnalyzer(string titleId) => PlayReports.Analyzer.TitleIds.ContainsIgnoreCase(titleId);
+
public static void Initialize()
{
_discordPresenceMain = new RichPresence
@@ -124,20 +123,22 @@ namespace Ryujinx.Ava
_currentApp = null;
}
- private static void HandlePlayReport(MessagePackObject playReport)
+ private static void HandlePlayReport(PlayReport playReport)
{
if (_discordClient is null) return;
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
- PlayReportAnalyzer.FormattedValue formattedValue =
- PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
+ FormattedValue formattedValue =
+ PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return;
- _discordPresencePlaying.Details = formattedValue.Reset
- ? $"Playing {_currentApp.Title}"
- : formattedValue.FormattedString;
+ _discordPresencePlaying.Details = TruncateToByteLength(
+ formattedValue.Reset
+ ? $"Playing {_currentApp.Title}"
+ : formattedValue.FormattedString
+ );
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
return; //don't trigger an update if the set presence Details are identical to current
diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs
index 1b10145fe..a57863d5d 100644
--- a/src/Ryujinx/Headless/Options.cs
+++ b/src/Ryujinx/Headless/Options.cs
@@ -387,7 +387,7 @@ namespace Ryujinx.Headless
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
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; }
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs
index c9d672af4..081998a00 100644
--- a/src/Ryujinx/Headless/Windows/WindowBase.cs
+++ b/src/Ryujinx/Headless/Windows/WindowBase.cs
@@ -513,7 +513,7 @@ namespace Ryujinx.Headless
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()
{
@@ -521,7 +521,7 @@ namespace Ryujinx.Headless
message = message,
buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
numbuttons = buttonsText.Length,
- window = WindowHandle,
+ window = WindowHandle
};
for (int i = 0; i < buttonsText.Length; i++)
diff --git a/src/Ryujinx/RyujinxApp.axaml.cs b/src/Ryujinx/RyujinxApp.axaml.cs
index 32318776a..90552cd16 100644
--- a/src/Ryujinx/RyujinxApp.axaml.cs
+++ b/src/Ryujinx/RyujinxApp.axaml.cs
@@ -22,6 +22,8 @@ namespace Ryujinx.Ava
{
public class RyujinxApp : Application
{
+ public static event Action ThemeChanged;
+
internal static string FormatTitle(LocaleKeys? windowTitleKey = null, bool includeVersion = true)
=> windowTitleKey is null
? $"{FullAppName}{(includeVersion ? $" {Program.Version}" : string.Empty)}"
@@ -112,7 +114,7 @@ namespace Ryujinx.Ava
baseStyle = ConfigurationState.Instance.UI.BaseStyle;
}
- ThemeManager.OnThemeChanged();
+ ThemeChanged?.Invoke();
RequestedThemeVariant = baseStyle switch
{
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index c75e532ec..c03c4c45f 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -75,31 +75,32 @@ namespace Ryujinx.Ava.UI.Applet
bool opened = false;
UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent,
- title,
- message,
- string.Empty,
- LocaleManager.Instance[LocaleKeys.DialogOpenSettingsWindowLabel],
- string.Empty,
- LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
- (int)Symbol.Important,
- deferEvent,
- async window =>
- {
- if (opened)
- {
- return;
- }
+ title,
+ message,
+ string.Empty,
+ LocaleManager.Instance[LocaleKeys.DialogOpenSettingsWindowLabel],
+ string.Empty,
+ LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
+ (int)Symbol.Important,
+ deferEvent,
+ async window =>
+ {
+ if (opened)
+ {
+ 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)
{
@@ -110,7 +111,9 @@ namespace Ryujinx.Ava.UI.Applet
}
catch (Exception ex)
{
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex));
+ await ContentDialogHelper.CreateErrorDialog(
+ LocaleManager.Instance.UpdateAndGetDynamicValue(
+ LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex));
dialogCloseEvent.Set();
}
@@ -134,7 +137,9 @@ namespace Ryujinx.Ava.UI.Applet
try
{
_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)
{
@@ -146,7 +151,9 @@ namespace Ryujinx.Ava.UI.Applet
{
error = true;
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex));
+ await ContentDialogHelper.CreateErrorDialog(
+ LocaleManager.Instance.UpdateAndGetDynamicValue(
+ LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex));
}
finally
{
@@ -177,7 +184,8 @@ namespace Ryujinx.Ava.UI.Applet
args.InitialText = "Ryujinx";
args.StringLengthMin = 1;
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)
{
inputText = userInput;
@@ -201,11 +209,13 @@ namespace Ryujinx.Ava.UI.Applet
Dispatcher.UIThread.InvokeAsync(async () =>
{
dialogCloseEvent.Set();
- await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.CabinetScanDialog],
- string.Empty,
- LocaleManager.Instance[LocaleKeys.InputDialogOk],
- string.Empty,
- LocaleManager.Instance[LocaleKeys.CabinetTitle]);
+ await ContentDialogHelper.CreateInfoDialog(
+ LocaleManager.Instance[LocaleKeys.CabinetScanDialog],
+ string.Empty,
+ LocaleManager.Instance[LocaleKeys.InputDialogOk],
+ string.Empty,
+ LocaleManager.Instance[LocaleKeys.CabinetTitle]
+ );
});
dialogCloseEvent.WaitOne();
}
@@ -217,7 +227,8 @@ namespace Ryujinx.Ava.UI.Applet
_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);
@@ -229,9 +240,7 @@ namespace Ryujinx.Ava.UI.Applet
{
ErrorAppletWindow msgDialog = new(_parent, buttons, message)
{
- Title = title,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Width = 400
+ Title = title, WindowStartupLocation = WindowStartupLocation.CenterScreen, Width = 400
};
object response = await msgDialog.Run();
@@ -249,7 +258,9 @@ namespace Ryujinx.Ava.UI.Applet
{
dialogCloseEvent.Set();
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex));
+ await ContentDialogHelper.CreateErrorDialog(
+ LocaleManager.Instance.UpdateAndGetDynamicValue(
+ LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex));
}
});
@@ -259,38 +270,36 @@ namespace Ryujinx.Ava.UI.Applet
}
public IDynamicTextInputHandler CreateDynamicTextInputHandler() => new AvaloniaDynamicTextInputHandler(_parent);
-
+
public UserProfile ShowPlayerSelectDialog()
{
UserId selected = UserId.Null;
byte[] defaultGuestImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg");
UserProfile guest = new(new UserId("00000000000000000000000000000080"), "Guest", defaultGuestImage);
-
+
ManualResetEvent dialogCloseEvent = new(false);
-
+
Dispatcher.UIThread.InvokeAsync(async () =>
{
ObservableCollection profiles = [];
NavigationDialogHost nav = new();
-
+
_parent.AccountManager.GetAllUsers()
.OrderBy(x => x.Name)
.ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav)));
-
+
profiles.Add(new Models.UserProfile(guest, nav));
- UserSelectorDialogViewModel viewModel = new()
+ ProfileSelectorDialogViewModel viewModel = new()
{
- Profiles = profiles,
- SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
+ Profiles = profiles, SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
};
- UserSelectorDialog content = new(viewModel);
- (selected, _) = await UserSelectorDialog.ShowInputDialog(content);
-
+ (selected, _) = await ProfileSelectorDialog.ShowInputDialog(viewModel);
+
dialogCloseEvent.Set();
});
-
+
dialogCloseEvent.WaitOne();
-
+
UserProfile profile = _parent.AccountManager.LastOpenedUser;
if (selected == guest.UserId)
{
@@ -311,6 +320,7 @@ namespace Ryujinx.Ava.UI.Applet
}
}
}
+
return profile;
}
}
diff --git a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
similarity index 96%
rename from src/Ryujinx/UI/Applet/UserSelectorDialog.axaml
rename to src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
index 2816afbce..d929cc501 100644
--- a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml
+++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
@@ -1,5 +1,5 @@
+ x:DataType="viewModels:ProfileSelectorDialogViewModel">
-
+
diff --git a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs
similarity index 85%
rename from src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs
rename to src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs
index 95081913e..b2c396b69 100644
--- a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs
+++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs
@@ -16,15 +16,15 @@ using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
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();
- ViewModel = viewModel;
- DataContext = ViewModel;
}
private void Grid_PointerEntered(object sender, PointerEventArgs e)
@@ -54,7 +54,7 @@ namespace Ryujinx.Ava.UI.Applet
if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
{
ViewModel.SelectedUserId = userProfile.UserId;
- Logger.Info?.Print(LogClass.UI, $"Selected user: {userProfile.UserId}");
+ Logger.Info?.Print(LogClass.UI, $"Selected: {userProfile.UserId}", "ProfileSelector");
ObservableCollection 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()
{
@@ -87,22 +87,25 @@ namespace Ryujinx.Ava.UI.Applet
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
- Content = content,
+ Content = new ProfileSelectorDialog(viewModel),
Padding = new Thickness(0)
};
UserId result = UserId.Null;
bool input = false;
+
+ contentDialog.Closed += Handler;
+ await ContentDialogHelper.ShowAsync(contentDialog);
+
+ return (result, input);
+
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
{
if (eventArgs.Result == ContentDialogResult.Primary)
{
- if (contentDialog.Content is UserSelectorDialog view)
- {
- result = view.ViewModel.SelectedUserId;
- input = true;
- }
+ result = viewModel.SelectedUserId;
+ input = true;
}
else
{
@@ -110,12 +113,6 @@ namespace Ryujinx.Ava.UI.Applet
input = false;
}
}
-
- contentDialog.Closed += Handler;
-
- await ContentDialogHelper.ShowAsync(contentDialog);
-
- return (result, input);
}
}
}
diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
index acade1df9..3e47a1910 100644
--- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
@@ -26,7 +26,6 @@
Icon="{ext:Icon mdi-gamepad}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
-
-
-
+
+
+
+
+
-
-
+ TextAlignment="Start"
+ TextWrapping="NoWrap" />
+
+
+
+
+
+
-
-
-
-
-
-
+
diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
index 0bd22a243..57dba3228 100644
--- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
+++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
@@ -1,12 +1,9 @@
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
-using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
@@ -23,10 +20,12 @@ namespace Ryujinx.Ava.UI.Controls
{
ContentDialog contentDialog = new()
{
+ Title = appData.Name,
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
- Content = new ApplicationDataView { DataContext = appData }
+ MinWidth = 256,
+ Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) }
};
Style closeButton = new(x => x.Name("CloseButton"));
diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
index 151bf5b32..c6b7268b9 100644
--- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
@@ -93,7 +93,19 @@
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0"
- Padding="0" >
+ Padding="0">
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs
index 7c6b0cf15..fb38fdb72 100644
--- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs
+++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs
@@ -7,7 +7,6 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System;
-using System.Globalization;
using System.Linq;
namespace Ryujinx.Ava.UI.Controls
diff --git a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs
index 47d0b94d0..7694e8883 100644
--- a/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs
+++ b/src/Ryujinx/UI/Helpers/Converters/MultiplayerInfoConverter.cs
@@ -1,27 +1,31 @@
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
+using Gommon;
+using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Globalization;
+using System.Text;
namespace Ryujinx.Ava.UI.Helpers
{
internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter
{
- private static readonly MultiplayerInfoConverter _instance = new();
+ public static readonly MultiplayerInfoConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- if (value is ApplicationData applicationData)
- {
- if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
- {
- return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
- }
- }
-
- return "";
+ if (value is not ApplicationData { HasLdnGames: true } applicationData)
+ return "";
+ return new StringBuilder()
+ .AppendLine(
+ LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames]
+ .Format(applicationData.GameCount))
+ .Append(
+ LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount]
+ .Format(applicationData.PlayerCount))
+ .ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
@@ -31,7 +35,7 @@ namespace Ryujinx.Ava.UI.Helpers
public override object ProvideValue(IServiceProvider serviceProvider)
{
- return _instance;
+ return Instance;
}
}
}
diff --git a/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs b/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs
index 99c6a0fce..c8082ec62 100644
--- a/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs
+++ b/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs
@@ -18,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or
LocaleKeys.CompatibilityListMenus => Brushes.Red,
- LocaleKeys.CompatibilityListIngame => Brushes.Yellow,
+ LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange,
_ => Brushes.ForestGreen
};
diff --git a/src/Ryujinx/UI/Helpers/LoggerAdapter.cs b/src/Ryujinx/UI/Helpers/LoggerAdapter.cs
index 928d1fc31..ba317e74a 100644
--- a/src/Ryujinx/UI/Helpers/LoggerAdapter.cs
+++ b/src/Ryujinx/UI/Helpers/LoggerAdapter.cs
@@ -1,6 +1,7 @@
using Avalonia.Logging;
using Avalonia.Utilities;
using Gommon;
+using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Text;
@@ -14,13 +15,19 @@ namespace Ryujinx.Ava.UI.Helpers
internal class LoggerAdapter : ILogSink
{
+ private static bool _avaloniaLogsEnabled = ConfigurationState.Instance.Logger.EnableAvaloniaLog;
+
public static void Register()
{
AvaLogger.Sink = new LoggerAdapter();
+ ConfigurationState.Instance.Logger.EnableAvaloniaLog.Event
+ += (_, e) => _avaloniaLogsEnabled = e.NewValue;
}
private static RyuLogger.Log? GetLog(AvaLogLevel level, string area)
{
+ if (!_avaloniaLogsEnabled) return null;
+
return level switch
{
AvaLogLevel.Verbose => RyuLogger.Debug,
diff --git a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs
index 10256babe..7a63c3391 100644
--- a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs
@@ -2,6 +2,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
+using Gommon;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.Configuration;
@@ -24,30 +25,35 @@ namespace Ryujinx.Ava.UI.ViewModels
Version = RyujinxApp.FullAppName + "\n" + Program.Version;
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value);
- ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
+ RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
}
- private void ThemeManager_ThemeChanged()
+ private void Ryujinx_ThemeChanged()
{
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)
{
bool isDarkTheme = theme == "Dark" || (theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
+
+ string themeName = isDarkTheme ? "Dark" : "Light";
- string basePath = "resm:Ryujinx.Assets.UIImages.";
- string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png";
-
- GithubLogo = LoadBitmap($"{basePath}Logo_GitHub_{themeSuffix}?assembly=Ryujinx");
- DiscordLogo = LoadBitmap($"{basePath}Logo_Discord_{themeSuffix}?assembly=Ryujinx");
+ GithubLogo = LoadBitmap(LogoPathFormat.Format("GitHub", themeName));
+ DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
public void Dispose()
{
- ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
+ RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
+
+ GithubLogo.Dispose();
+ DiscordLogo.Dispose();
+
GC.SuppressFinalize(this);
}
}
diff --git a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs
index e8869c475..e550c4ae0 100644
--- a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs
@@ -264,7 +264,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}");
// Neither local or remote files are valid JSON, close window.
- ShowInfoDialog();
+ await ShowInfoDialog();
Close();
}
else if (!remoteIsValid)
@@ -273,7 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels
// Only the local file is valid, the local one should be used
// but the user should be warned.
- ShowInfoDialog();
+ await ShowInfoDialog();
}
}
@@ -525,7 +525,7 @@ namespace Ryujinx.Ava.UI.ViewModels
AmiiboImage = bitmap;
}
- private static async void ShowInfoDialog()
+ private static async Task ShowInfoDialog()
{
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage],
diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
new file mode 100644
index 000000000..592d5f81a
--- /dev/null
+++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
@@ -0,0 +1,23 @@
+using Gommon;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Utilities.AppLibrary;
+
+namespace Ryujinx.Ava.UI.ViewModels
+{
+ public class ApplicationDataViewModel : BaseModel
+ {
+ public ApplicationData AppData { get; }
+
+ public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
+
+ public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
+ public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
+ public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
+ public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
+
+ public string FormattedLdnInfo =>
+ $"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" +
+ $"\n" +
+ $"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}";
+ }
+}
diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
index d7a09a0e3..bbfe80570 100644
--- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
@@ -7,6 +7,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
@@ -104,6 +105,13 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] private bool _isSubMenuOpen;
[ObservableProperty] private ApplicationContextMenu _listAppContextMenu;
[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 _isGameRunning;
@@ -633,15 +641,15 @@ namespace Ryujinx.Ava.UI.ViewModels
{
return SortMode switch
{
- ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
- ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper],
- ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed],
- ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed],
- ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension],
- ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize],
- ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath],
ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite],
ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel],
+ ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
+ ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper],
+ ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed],
+ ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed],
+ ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListSortFileExtension],
+ ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListSortFileSize],
+ ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListSortPath],
_ => string.Empty,
};
}
@@ -1147,10 +1155,10 @@ namespace Ryujinx.Ava.UI.ViewModels
List dirs = result.Select(it => it.Path.LocalPath).ToList();
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[localeMessageAddedKey], numAdded)
- });
+ );
await Dispatcher.UIThread.InvokeAsync(async () =>
{
diff --git a/src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs b/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs
similarity index 82%
rename from src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs
rename to src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs
index 094aed5cf..979e1616a 100644
--- a/src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs
@@ -4,7 +4,7 @@ using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels
{
- public partial class UserSelectorDialogViewModel : BaseModel
+ public partial class ProfileSelectorDialogViewModel : BaseModel
{
[ObservableProperty] private UserId _selectedUserId;
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index d54313e76..1b1f8a2a5 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -2,7 +2,7 @@ using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
-using Gommon;
+using CommunityToolkit.Mvvm.Input;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
@@ -13,6 +13,7 @@ using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.System;
+using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver;
@@ -28,8 +29,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.NetworkInformation;
-using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
@@ -123,9 +122,14 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool RememberWindowState { get; set; }
public bool ShowTitleBar { get; set; }
public int HideCursor { get; set; }
+ public int UpdateCheckerType { get; set; }
public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; }
+ public bool DisableInputWhenOutOfFocus { get; set; }
+
+ public int FocusLostActionType { get; set; }
+
public VSyncMode VSyncMode
{
get => _vSyncMode;
@@ -206,6 +210,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTrace { get; set; }
public bool EnableGuest { get; set; }
public bool EnableFsAccessLog { get; set; }
+ public bool EnableAvaloniaLog { get; set; }
public bool EnableDebug { get; set; }
public bool IsOpenAlEnabled { get; set; }
public bool IsSoundIoEnabled { get; set; }
@@ -477,6 +482,8 @@ namespace Ryujinx.Ava.UI.ViewModels
RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
+ UpdateCheckerType = (int)config.UpdateCheckerType.Value;
+ FocusLostActionType = (int)config.FocusLostActionType.Value;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@@ -496,6 +503,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDockedMode = config.System.EnableDockedMode;
EnableKeyboard = config.Hid.EnableKeyboard;
EnableMouse = config.Hid.EnableMouse;
+ DisableInputWhenOutOfFocus = config.Hid.DisableInputWhenOutOfFocus;
// Keyboard Hotkeys
KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value);
@@ -562,6 +570,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableGuest = config.Logger.EnableGuest;
EnableDebug = config.Logger.EnableDebug;
EnableFsAccessLog = config.Logger.EnableFsAccessLog;
+ EnableAvaloniaLog = config.Logger.EnableAvaloniaLog;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
@@ -582,6 +591,8 @@ namespace Ryujinx.Ava.UI.ViewModels
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
+ config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
+ config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
if (GameDirectoryChanged)
{
@@ -605,6 +616,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.EnableDockedMode.Value = EnableDockedMode;
config.Hid.EnableKeyboard.Value = EnableKeyboard;
config.Hid.EnableMouse.Value = EnableMouse;
+ config.Hid.DisableInputWhenOutOfFocus.Value = DisableInputWhenOutOfFocus;
// Keyboard Hotkeys
config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig();
@@ -681,6 +693,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
+ config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
@@ -722,6 +735,25 @@ namespace Ryujinx.Ava.UI.ViewModels
CloseWindow?.Invoke();
}
+ [ObservableProperty] private bool _wantsToReset;
+
+ public AsyncRelayCommand ResetButton => Commands.Create(async () =>
+ {
+ if (!WantsToReset) return;
+
+ CloseWindow?.Invoke();
+ ConfigurationState.Instance.LoadDefault();
+ ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+ RyujinxApp.MainWindow.LoadApplications();
+
+ await ContentDialogHelper.CreateInfoDialog(
+ $"Your {RyujinxApp.FullAppName} configuration has been reset.",
+ "",
+ string.Empty,
+ LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
+ "Configuration Reset");
+ });
+
public void CancelButton()
{
RevertIfNotSaved();
diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
index a0bcd1aa2..d1931ae2f 100644
--- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -51,12 +51,8 @@ namespace Ryujinx.Ava.UI.Views.Main
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show());
-
- UpdateMenuItem.Command = Commands.Create(async () =>
- {
- if (Updater.CanUpdate(true))
- await Updater.BeginUpdateAsync(true);
- });
+
+ UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
FaqMenuItem.Command =
SetupGuideMenuItem.Command =
diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml
index a0259c723..02cc1fc7b 100644
--- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml
+++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml
@@ -23,7 +23,7 @@
Background="{DynamicResource ThemeContentBackgroundColor}"
DockPanel.Dock="Bottom"
IsVisible="{Binding ShowMenuAndStatusBar}"
- ColumnDefinitions="Auto,Auto,*,Auto,Auto">
+ ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml
index cdc66a138..db557b417 100644
--- a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml
+++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml
@@ -113,37 +113,37 @@
Tag="TitleId" />
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml
index 7ab49c0ae..d28f0ffa7 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml
@@ -74,6 +74,10 @@
ToolTip.Tip="{ext:Locale DebugLogTooltip}">
+
+
+
@@ -30,18 +31,57 @@
ToolTip.Tip="{ext:Locale ToggleDiscordTooltip}"
Text="{ext:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true));
AddAutoloadDirButton.Command =
diff --git a/src/Ryujinx/UI/Windows/AboutWindow.axaml b/src/Ryujinx/UI/Windows/AboutWindow.axaml
index df880160f..e215cf27e 100644
--- a/src/Ryujinx/UI/Windows/AboutWindow.axaml
+++ b/src/Ryujinx/UI/Windows/AboutWindow.axaml
@@ -125,7 +125,7 @@
Background="Transparent"
Click="Button_OnClick"
CornerRadius="15"
- Tag="https://discord.gg/dHPrkBkkyA"
+ Tag="https://discord.gg/PEuzjrFXUA"
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
@@ -142,42 +142,40 @@
+ VerticalAlignment="Stretch" RowDefinitions="Auto,Auto,Auto">
+ x.Name("CloseButton"));
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml
index 9fa7bf046..7606d411b 100644
--- a/src/Ryujinx/UI/Windows/MainWindow.axaml
+++ b/src/Ryujinx/UI/Windows/MainWindow.axaml
@@ -21,7 +21,9 @@
x:DataType="viewModels:MainWindowViewModel"
mc:Ignorable="d"
WindowStartupLocation="Manual"
- Focusable="True">
+ Focusable="True"
+ GotFocus="InputElement_OnGotFocus"
+ LostFocus="InputElement_OnLostFocus">
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs
index f8b4b2beb..338e9de43 100644
--- a/src/Ryujinx/Updater.cs
+++ b/src/Ryujinx/Updater.cs
@@ -43,7 +43,18 @@ namespace Ryujinx.Ava
private const int ConnectionCount = 4;
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 long _buildSize;
private static bool _updateSuccessful;
@@ -51,30 +62,8 @@ namespace Ryujinx.Ava
private static readonly string[] _windowsDependencyDirs = [];
- public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
+ public static async Task> 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))
{
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
@@ -85,7 +74,7 @@ namespace Ryujinx.Ava
_running = false;
- return;
+ return default;
}
Logger.Info?.Print(LogClass.Application, "Checking for updates.");
@@ -123,7 +112,7 @@ namespace Ryujinx.Ava
_running = false;
- return;
+ return default;
}
break;
@@ -149,7 +138,7 @@ namespace Ryujinx.Ava
_running = false;
- return;
+ return default;
}
}
catch (Exception exception)
@@ -161,7 +150,7 @@ namespace Ryujinx.Ava
_running = false;
- return;
+ return default;
}
if (!Version.TryParse(_buildVer, out Version newVersion))
@@ -174,9 +163,27 @@ namespace Ryujinx.Ava
_running = false;
+ return default;
+ }
+
+ return (currentVersion, newVersion);
+ }
+
+ public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
+ {
+ if (_running)
+ {
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 (showVersionUpToDate)
diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs
index aef54819e..747ead8ca 100644
--- a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs
+++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs
@@ -1,3 +1,4 @@
+using Gommon;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
@@ -14,6 +15,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using System;
using System.IO;
+using System.Text;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary
@@ -32,23 +34,20 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
set
{
_id = value;
- PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
+
+ Compatibility = CompatibilityCsv.Find(Id);
}
}
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
-
- public bool HasPlayabilityInfo => PlayabilityStatus != null;
-
- public string LocalizedStatus =>
- PlayabilityStatus.HasValue
- ? LocaleManager.Instance[PlayabilityStatus!.Value]
- : string.Empty;
-
- public LocaleKeys? PlayabilityStatus { get; set; }
-
public int PlayerCount { get; set; }
public int GameCount { get; set; }
+
+ public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
+
+ public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
+ public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString);
+
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
@@ -60,11 +59,39 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
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 FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
+
+ public Optional 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");
@@ -74,16 +101,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
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))
{
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
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();
diff --git a/src/Ryujinx/Utilities/AppletMetadata.cs b/src/Ryujinx/Utilities/AppletMetadata.cs
index a165487a3..50f01b4c0 100644
--- a/src/Ryujinx/Utilities/AppletMetadata.cs
+++ b/src/Ryujinx/Utilities/AppletMetadata.cs
@@ -45,7 +45,7 @@ namespace Ryujinx.Ava.Utilities
if (string.IsNullOrEmpty(contentPath))
goto BadData;
- appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) };
+ appData = new() { Name = Name, Id = ProgramId, Path = contentPath };
appControl = StructHelpers.CreateCustomNacpData(Name, Version);
return true;
diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs
index c3fcf99ca..b58c05299 100644
--- a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs
+++ b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs
@@ -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)
- => 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 string GetLabels(string titleId)
+ => Find(titleId)?.FormattedIssueLabels;
+
+ public static string GetLabels(ulong titleId) => GetLabels(titleId.ToString("X16"));
}
public class CompatibilityEntry
@@ -100,12 +111,25 @@ namespace Ryujinx.Ava.Utilities.Compat
public Optional TitleId { get; }
public string[] Labels { get; }
public LocaleKeys? Status { get; }
+
+ public LocaleKeys? StatusDescription
+ => Status switch
+ {
+ LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
+ LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
+ LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
+ LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
+ LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
+ _ => null
+ };
+
public DateTime LastUpdated { get; }
public string LocalizedLastUpdated =>
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
-
+
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
+ public string LocalizedStatusDescription => LocaleManager.Instance[StatusDescription!.Value];
public string FormattedTitleId => TitleId
.OrElse(new string(' ', 16));
diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml
index 132b10e26..ef70a23d1 100644
--- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml
+++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml
@@ -64,6 +64,8 @@
VerticalAlignment="Center"
Text="{Binding LocalizedStatus}"
Width="85"
+ Background="Transparent"
+ ToolTip.Tip="{Binding LocalizedStatusDescription}"
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextWrapping="NoWrap" />
/// The current version of the file format
///
- public const int CurrentVersion = 63;
+ public const int CurrentVersion = 67;
///
/// Version of the configuration file format
@@ -111,6 +111,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages
///
public bool LoggingEnableFsAccessLog { get; set; }
+
+ ///
+ /// Enables log messages from Avalonia
+ ///
+ public bool LoggingEnableAvalonia { get; set; }
///
/// Controls which log messages are written to the log targets
@@ -158,9 +163,19 @@ namespace Ryujinx.Ava.Utilities.Configuration
public bool EnableDiscordIntegration { get; set; }
///
- /// Checks for updates when Ryujinx starts when enabled
+ /// DEPRECATED: Checks for updates when Ryujinx starts when enabled
///
public bool CheckUpdatesOnStart { get; set; }
+
+ ///
+ /// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
+ ///
+ public UpdaterType UpdateCheckerType { get; set; }
+
+ ///
+ /// How the emulator should behave when you click off/on the window.
+ ///
+ public FocusLostType FocusLostActionType { get; set; }
///
/// Show "Confirm Exit" Dialog
@@ -373,6 +388,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enable or disable mouse support (Independent from controllers binding)
///
public bool EnableMouse { get; set; }
+
+ ///
+ /// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
+ ///
+ public bool DisableInputWhenOutOfFocus { get; set; }
///
/// Hotkey Keyboard Bindings
diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs
index 0fb3fb754..6bca36340 100644
--- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs
+++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs
@@ -45,6 +45,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart;
+ UpdateCheckerType.Value = cff.UpdateCheckerType;
+ FocusLostActionType.Value = cff.FocusLostActionType;
ShowConfirmExit.Value = cff.ShowConfirmExit;
RememberWindowState.Value = cff.RememberWindowState;
ShowTitleBar.Value = cff.ShowTitleBar;
@@ -138,6 +140,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hid.EnableKeyboard.Value = cff.EnableKeyboard;
Hid.EnableMouse.Value = cff.EnableMouse;
+ Hid.DisableInputWhenOutOfFocus.Value = cff.DisableInputWhenOutOfFocus;
Hid.Hotkeys.Value = cff.Hotkeys;
Hid.InputConfig.Value = cff.InputConfig ?? [];
Hid.RainbowSpeed.Value = cff.RainbowSpeed;
@@ -430,7 +433,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
}
}),
(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),
+ (67, static cff => cff.FocusLostActionType = cff.DisableInputWhenOutOfFocus ? FocusLostType.BlockInput : FocusLostType.DoNothing)
);
}
}
diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs
index 2d6e2aa7f..51c40cc99 100644
--- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs
+++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs
@@ -1,6 +1,7 @@
using ARMeilleure;
using Gommon;
using Ryujinx.Ava.Utilities.Configuration.System;
+using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@@ -254,6 +255,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages
///
public ReactiveObject EnableFsAccessLog { get; private set; }
+
+ ///
+ /// Enables log messages from Avalonia
+ ///
+ public ReactiveObject EnableAvaloniaLog { get; private set; }
///
/// Controls which log messages are written to the log targets
@@ -281,6 +287,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableTrace = new ReactiveObject();
EnableGuest = new ReactiveObject();
EnableFsAccessLog = new ReactiveObject();
+ EnableAvaloniaLog = new ReactiveObject();
FilteredClasses = new ReactiveObject();
EnableFileLog = new ReactiveObject();
EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
@@ -440,6 +447,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enable or disable mouse support (Independent from controllers binding)
///
public ReactiveObject EnableMouse { get; private set; }
+
+ ///
+ /// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
+ ///
+ public ReactiveObject DisableInputWhenOutOfFocus { get; private set; }
///
/// Hotkey Keyboard Bindings
@@ -462,6 +474,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
{
EnableKeyboard = new ReactiveObject();
EnableMouse = new ReactiveObject();
+ DisableInputWhenOutOfFocus = new ReactiveObject();
Hotkeys = new ReactiveObject();
InputConfig = new ReactiveObject>();
RainbowSpeed = new ReactiveObject();
@@ -761,6 +774,16 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Checks for updates when Ryujinx starts when enabled
///
public ReactiveObject CheckUpdatesOnStart { get; private set; }
+
+ ///
+ /// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
+ ///
+ public ReactiveObject UpdateCheckerType { get; private set; }
+
+ ///
+ /// How the emulator should behave when you click off/on the window.
+ ///
+ public ReactiveObject FocusLostActionType { get; private set; }
///
/// Show "Confirm Exit" Dialog
@@ -798,6 +821,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hacks = new HacksSection();
EnableDiscordIntegration = new ReactiveObject();
CheckUpdatesOnStart = new ReactiveObject();
+ UpdateCheckerType = new ReactiveObject();
+ FocusLostActionType = new ReactiveObject();
ShowConfirmExit = new ReactiveObject();
RememberWindowState = new ReactiveObject();
ShowTitleBar = new ReactiveObject();
diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs
index 4ab77a60f..774b9217e 100644
--- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs
@@ -46,6 +46,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
LoggingEnableTrace = Logger.EnableTrace,
LoggingEnableGuest = Logger.EnableGuest,
LoggingEnableFsAccessLog = Logger.EnableFsAccessLog,
+ LoggingEnableAvalonia = Logger.EnableAvaloniaLog,
LoggingFilteredClasses = Logger.FilteredClasses,
LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel,
SystemLanguage = System.Language,
@@ -55,6 +56,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
DockedMode = System.EnableDockedMode,
EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart,
+ UpdateCheckerType = UpdateCheckerType,
+ FocusLostActionType = FocusLostActionType,
ShowConfirmExit = ShowConfirmExit,
RememberWindowState = RememberWindowState,
ShowTitleBar = ShowTitleBar,
@@ -129,6 +132,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
ShowConsole = UI.ShowConsole,
EnableKeyboard = Hid.EnableKeyboard,
EnableMouse = Hid.EnableMouse,
+ DisableInputWhenOutOfFocus = Hid.DisableInputWhenOutOfFocus,
Hotkeys = Hid.Hotkeys,
InputConfig = Hid.InputConfig,
RainbowSpeed = Hid.RainbowSpeed,
@@ -165,6 +169,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Logger.EnableTrace.Value = false;
Logger.EnableGuest.Value = true;
Logger.EnableFsAccessLog.Value = false;
+ Logger.EnableAvaloniaLog.Value = false;
Logger.FilteredClasses.Value = [];
Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None;
System.Language.Value = Language.AmericanEnglish;
@@ -173,7 +178,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.SystemTimeOffset.Value = 0;
System.EnableDockedMode.Value = true;
EnableDiscordIntegration.Value = true;
- CheckUpdatesOnStart.Value = true;
+ UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
+ FocusLostActionType.Value = FocusLostType.DoNothing;
ShowConfirmExit.Value = true;
RememberWindowState.Value = true;
ShowTitleBar.Value = !OperatingSystem.IsWindows();
@@ -242,6 +248,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
UI.WindowStartup.WindowMaximized.Value = false;
Hid.EnableKeyboard.Value = false;
Hid.EnableMouse.Value = false;
+ Hid.DisableInputWhenOutOfFocus.Value = false;
Hid.Hotkeys.Value = new KeyboardHotkeys
{
ToggleVSyncMode = Key.F1,
diff --git a/src/Ryujinx/Utilities/Configuration/UI/FocusLostType.cs b/src/Ryujinx/Utilities/Configuration/UI/FocusLostType.cs
new file mode 100644
index 000000000..eea588539
--- /dev/null
+++ b/src/Ryujinx/Utilities/Configuration/UI/FocusLostType.cs
@@ -0,0 +1,15 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ava.Utilities.Configuration.UI
+{
+ [JsonConverter(typeof(TypedStringEnumConverter))]
+ public enum FocusLostType
+ {
+ DoNothing,
+ BlockInput,
+ MuteAudio,
+ BlockInputAndMuteAudio,
+ PauseEmulation
+ }
+}
diff --git a/src/Ryujinx/Utilities/Configuration/UI/UpdaterType.cs b/src/Ryujinx/Utilities/Configuration/UI/UpdaterType.cs
new file mode 100644
index 000000000..2ab17a497
--- /dev/null
+++ b/src/Ryujinx/Utilities/Configuration/UI/UpdaterType.cs
@@ -0,0 +1,13 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Ava.Utilities.Configuration.UI
+{
+ [JsonConverter(typeof(TypedStringEnumConverter))]
+ public enum UpdaterType
+ {
+ Off,
+ PromptAtStartup,
+ CheckInBackground
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs
deleted file mode 100644
index f518fb902..000000000
--- a/src/Ryujinx/Utilities/PlayReport.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
-
-namespace Ryujinx.Ava.Utilities
-{
- public static class PlayReport
- {
- public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
- .AddSpec(
- "01007ef00011e000",
- spec => spec
- .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
- // reset to normal status when switching between normal & master mode in title screen
- .AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
- )
- .AddSpec(
- "0100f2c0115b6000",
- spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
- .AddSpec(
- "0100000000010000",
- spec =>
- spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
- )
- .AddSpec(
- "010075000ecbe000",
- spec =>
- spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
- )
- .AddSpec(
- "010028600ebda000",
- spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
- )
- .AddSpec( // Global & China IDs
- ["0100152000022000", "010075100e8ec000"],
- spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
- );
-
- private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
- => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
-
- private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
- value.DoubleValue switch
- {
- > 800d => "Exploring the Sky Islands",
- < -201d => "Exploring the Depths",
- _ => "Roaming Hyrule"
- };
-
- private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
- => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
-
- private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
- => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
-
- private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
- => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
-
- private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
- => value.StringValue switch
- {
- // Single Player
- "Single" => "Single Player",
- // Multiplayer
- "Multi-2players" => "Multiplayer 2 Players",
- "Multi-3players" => "Multiplayer 3 Players",
- "Multi-4players" => "Multiplayer 4 Players",
- // Wireless/LAN Play
- "Local-Single" => "Wireless/LAN Play",
- "Local-2players" => "Wireless/LAN Play 2 Players",
- // CC Classes
- "50cc" => "50cc",
- "100cc" => "100cc",
- "150cc" => "150cc",
- "Mirror" => "Mirror (150cc)",
- "200cc" => "200cc",
- // Modes
- "GrandPrix" => "Grand Prix",
- "TimeAttack" => "Time Trials",
- "VS" => "VS Races",
- "Battle" => "Battle Mode",
- "RaceStart" => "Selecting a Course",
- "Race" => "Racing",
- _ => PlayReportFormattedValue.ForceReset
- };
- }
-}
diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs
new file mode 100644
index 000000000..51047cc32
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs
@@ -0,0 +1,123 @@
+using Gommon;
+using Ryujinx.Ava.Utilities.AppLibrary;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// The entrypoint for the Play Report analysis system.
+ ///
+ public class Analyzer
+ {
+ private readonly List _specs = [];
+
+ public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
+
+ public IReadOnlyList Specs => new ReadOnlyCollection(_specs);
+
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+ ///
+ /// The ID of the game to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(string titleId, Func transform)
+ {
+ Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ return AddSpec(transform(GameSpec.Create(titleId)));
+ }
+
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+ ///
+ /// The ID of the game to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(string titleId, Action transform)
+ {
+ Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ return AddSpec(GameSpec.Create(titleId).Apply(transform));
+ }
+
+ ///
+ /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+ ///
+ /// The IDs of the games to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(IEnumerable titleIds,
+ Func transform)
+ {
+ string[] tids = titleIds.ToArray();
+ Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ return AddSpec(transform(GameSpec.Create(tids)));
+ }
+
+ ///
+ /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+ ///
+ /// The IDs of the games to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(IEnumerable titleIds, Action transform)
+ {
+ string[] tids = titleIds.ToArray();
+ Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ return AddSpec(GameSpec.Create(tids).Apply(transform));
+ }
+
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided pre-configured spec.
+ ///
+ /// The to add.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(GameSpec spec)
+ {
+ _specs.Add(spec);
+ return this;
+ }
+
+
+ ///
+ /// Runs the configured for the specified game title ID.
+ ///
+ /// The game currently running.
+ /// The Application metadata information, including localized game name and play time information.
+ /// The Play Report received from HLE.
+ /// A struct representing a possible formatted value.
+ public FormattedValue Format(
+ string runningGameId,
+ ApplicationMetadata appMeta,
+ Horizon.Prepo.Types.PlayReport playReport
+ )
+ {
+ if (!playReport.ReportData.IsDictionary)
+ return FormattedValue.Unhandled;
+
+ if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
+ return FormattedValue.Unhandled;
+
+ foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority))
+ {
+ if (!formatSpec.Format(appMeta, playReport, out FormattedValue value))
+ continue;
+
+ return value;
+ }
+
+ return FormattedValue.Unhandled;
+ }
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs
new file mode 100644
index 000000000..92569d32e
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs
@@ -0,0 +1,40 @@
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// The delegate type that powers single value formatters.
+ /// Takes in the result value from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate FormattedValue SingleValueFormatter(SingleValue value);
+
+ ///
+ /// The delegate type that powers multiple value formatters.
+ /// Takes in the result values from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate FormattedValue MultiValueFormatter(MultiValue value);
+
+ ///
+ /// The delegate type that powers multiple value formatters.
+ /// The dictionary passed to this delegate is sparsely populated;
+ /// that is, not every key specified in the Play Report needs to match for this to be used.
+ /// Takes in the result values from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate FormattedValue SparseMultiValueFormatter(SparseMultiValue value);
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs
new file mode 100644
index 000000000..46cae678a
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs
@@ -0,0 +1,73 @@
+using MsgPack;
+using Ryujinx.Ava.Utilities.AppLibrary;
+using System.Collections.Generic;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ public abstract class MatchedValue
+ {
+ protected MatchedValue(T matched)
+ {
+ Matched = matched;
+ }
+
+ ///
+ /// The currently running application's .
+ ///
+ public ApplicationMetadata Application { get; init; }
+
+ ///
+ /// The entire play report.
+ ///
+ public Horizon.Prepo.Types.PlayReport PlayReport { get; init; }
+
+ ///
+ /// The matched value from the Play Report.
+ ///
+ public T Matched { get; init; }
+ }
+
+ ///
+ /// The input data to a ,
+ /// containing the currently running application's ,
+ /// and the matched from the Play Report.
+ ///
+ public class SingleValue : MatchedValue
+ {
+ public SingleValue(Value matched) : base(matched)
+ {
+ }
+ }
+
+ ///
+ /// The input data to a ,
+ /// containing the currently running application's ,
+ /// and the matched s from the Play Report.
+ ///
+ public class MultiValue : MatchedValue
+ {
+ public MultiValue(Value[] matched) : base(matched)
+ {
+ }
+
+ public MultiValue(IEnumerable matched) : base(Value.ConvertPackedObjects(matched))
+ {
+ }
+ }
+
+ ///
+ /// The input data to a ,
+ /// containing the currently running application's ,
+ /// and the matched s from the Play Report.
+ ///
+ public class SparseMultiValue : MatchedValue>
+ {
+ public SparseMultiValue(Dictionary matched) : base(matched)
+ {
+ }
+
+ public SparseMultiValue(Dictionary matched) : base(Value.ConvertPackedObjectMap(matched))
+ {
+ }
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs
new file mode 100644
index 000000000..cabace6bb
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs
@@ -0,0 +1,632 @@
+using Gommon;
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ public partial class PlayReports
+ {
+ private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value)
+ => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
+
+ private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) =>
+ value.Matched.DoubleValue switch
+ {
+ > 800d => "Exploring the Sky Islands",
+ < -201d => "Exploring the Depths",
+ _ => "Roaming Hyrule"
+ };
+
+ private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
+ => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
+
+ private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
+ => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
+
+ private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
+ => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
+
+ private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
+ => value.Matched.StringValue switch
+ {
+ // Single Player
+ "Single" => "Single Player",
+ // Multiplayer
+ "Multi-2players" => "Multiplayer 2 Players",
+ "Multi-3players" => "Multiplayer 3 Players",
+ "Multi-4players" => "Multiplayer 4 Players",
+ // Wireless/LAN Play
+ "Local-Single" => "Wireless/LAN Play",
+ "Local-2players" => "Wireless/LAN Play 2 Players",
+ // CC Classes
+ "50cc" => "50cc",
+ "100cc" => "100cc",
+ "150cc" => "150cc",
+ "Mirror" => "Mirror (150cc)",
+ "200cc" => "200cc",
+ // Modes
+ "GrandPrix" => "Grand Prix",
+ "TimeAttack" => "Time Trials",
+ "VS" => "VS Races",
+ "Battle" => "Battle Mode",
+ "RaceStart" => "Selecting a Course",
+ "Race" => "Racing",
+ _ => FormattedValue.ForceReset
+ };
+
+ private static FormattedValue PokemonSVUnionCircle(SingleValue value)
+ => value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
+
+ private static FormattedValue PokemonSVArea(SingleValue value)
+ => value.Matched.StringValue switch
+ {
+ // Base Game Locations
+ "a_w01" => "South Area One",
+ "a_w02" => "Mesagoza",
+ "a_w03" => "The Pokemon League",
+ "a_w04" => "South Area Two",
+ "a_w05" => "South Area Four",
+ "a_w06" => "South Area Six",
+ "a_w07" => "South Area Five",
+ "a_w08" => "South Area Three",
+ "a_w09" => "West Area One",
+ "a_w10" => "Asado Desert",
+ "a_w11" => "West Area Two",
+ "a_w12" => "Medali",
+ "a_w13" => "Tagtree Thicket",
+ "a_w14" => "East Area Three",
+ "a_w15" => "Artazon",
+ "a_w16" => "East Area Two",
+ "a_w18" => "Casseroya Lake",
+ "a_w19" => "Glaseado Mountain",
+ "a_w20" => "North Area Three",
+ "a_w21" => "North Area One",
+ "a_w22" => "North Area Two",
+ "a_w23" => "The Great Crater of Paldea",
+ "a_w24" => "South Paldean Sea",
+ "a_w25" => "West Paldean Sea",
+ "a_w26" => "East Paldean Sea",
+ "a_w27" => "Nouth Paldean Sea",
+ //TODO DLC Locations
+ _ => FormattedValue.ForceReset
+ };
+
+ private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values)
+ {
+ // Check if the PlayReport is for a challenger approach or an achievement.
+ if (values.Matched.TryGetValue("fighter", out Value fighter) && values.Matched.ContainsKey("reason"))
+ {
+ return $"Challenger Approaches - {SuperSmashBrosUltimate_Character(fighter)}";
+ }
+
+ if (values.Matched.TryGetValue("fighter", out fighter) && values.Matched.ContainsKey("challenge_count"))
+ {
+ return $"Fighter Unlocked - {SuperSmashBrosUltimate_Character(fighter)}";
+ }
+
+ if (values.Matched.TryGetValue("anniversary", out Value anniversary))
+ {
+ return $"Achievement Unlocked - ID: {anniversary}";
+ }
+
+ if (values.Matched.ContainsKey("adv_slot"))
+ {
+ return
+ "Playing Adventure Mode"; // Doing this as it can be a placeholder until we can grab the character.
+ }
+
+ // Check if we have a match_mode at this point, if not, go to default.
+ if (!values.Matched.TryGetValue("match_mode", out Value matchMode))
+ {
+ return "Smashing";
+ }
+
+ return matchMode.BoxedValue switch
+ {
+ 0 when values.Matched.TryGetValue("player_1_fighter", out Value player) &&
+ values.Matched.TryGetValue("player_2_fighter", out Value challenger)
+ => $"Last Smashed: {SuperSmashBrosUltimate_Character(challenger)}'s Fighter Challenge - {SuperSmashBrosUltimate_Character(player)}",
+ 1 => $"Last Smashed: Normal Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 2 when values.Matched.TryGetValue("player_1_rank", out Value team)
+ => team.BoxedValue is 0
+ ? "Last Smashed: Squad Strike - Red Team Wins"
+ : "Last Smashed: Squad Strike - Blue Team Wins",
+ 3 => $"Last Smashed: Custom Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 4 => $"Last Smashed: Super Sudden Death - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 5 => $"Last Smashed: Smashdown - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 6 => $"Last Smashed: Tourney Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 7 when values.Matched.TryGetValue("player_1_fighter", out Value player)
+ => $"Last Smashed: Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}",
+ 8 when values.Matched.TryGetValue("player_1_fighter", out Value player)
+ => $"Playing Adventure Mode as {SuperSmashBrosUltimate_Character(player)}",
+ 10 when values.Matched.TryGetValue("match_submode", out Value battle) &&
+ values.Matched.TryGetValue("player_1_fighter", out Value player)
+ => $"Last Smashed: Classic Mode, Battle {(int)battle.BoxedValue + 1}/8 as {SuperSmashBrosUltimate_Character(player)}",
+ 12 => $"Last Smashed: Century Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 13 => $"Last Smashed: All-Star Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 14 => $"Last Smashed: Cruel Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 15 when values.Matched.TryGetValue("player_1_fighter", out Value player)
+ => $"Last Smashed: Home-Run Contest - {SuperSmashBrosUltimate_Character(player)}",
+ 16 when values.Matched.TryGetValue("player_1_fighter", out Value player1) &&
+ values.Matched.TryGetValue("player_2_fighter", out Value player2)
+ => $"Last Smashed: Home-Run Content (Co-op) - {SuperSmashBrosUltimate_Character(player1)} and {SuperSmashBrosUltimate_Character(player2)}",
+ 17 => $"Last Smashed: Home-Run Contest (Versus) - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 18 when values.Matched.TryGetValue("player_1_fighter", out Value player1) &&
+ values.Matched.TryGetValue("player_2_fighter", out Value player2)
+ => $"Fresh out of Training mode - {SuperSmashBrosUltimate_Character(player1)} with {SuperSmashBrosUltimate_Character(player2)}",
+ 58 => $"Last Smashed: LDN Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
+ 63 when values.Matched.TryGetValue("player_1_fighter", out Value player)
+ => $"Last Smashed: DLC Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}",
+ _ => "Smashing"
+ };
+ }
+
+ private static string SuperSmashBrosUltimate_Character(Value value) =>
+ BinaryPrimitives.ReverseEndianness(
+ BitConverter.ToInt64(((MsgPack.MessagePackExtendedTypeObject)value.BoxedValue).GetBody(), 0)) switch
+ {
+ 0x0 => "Mario",
+ 0x1 => "Donkey Kong",
+ 0x2 => "Link",
+ 0x3 => "Samus",
+ 0x4 => "Dark Samus",
+ 0x5 => "Yoshi",
+ 0x6 => "Kirby",
+ 0x7 => "Fox",
+ 0x8 => "Pikachu",
+ 0x9 => "Luigi",
+ 0xA => "Ness",
+ 0xB => "Captain Falcon",
+ 0xC => "Jigglypuff",
+ 0xD => "Peach",
+ 0xE => "Daisy",
+ 0xF => "Bowser",
+ 0x10 => "Ice Climbers",
+ 0x11 => "Sheik",
+ 0x12 => "Zelda",
+ 0x13 => "Dr. Mario",
+ 0x14 => "Pichu",
+ 0x15 => "Falco",
+ 0x16 => "Marth",
+ 0x17 => "Lucina",
+ 0x18 => "Young Link",
+ 0x19 => "Ganondorf",
+ 0x1A => "Mewtwo",
+ 0x1B => "Roy",
+ 0x1C => "Chrom",
+ 0x1D => "Mr Game & Watch",
+ 0x1E => "Meta Knight",
+ 0x1F => "Pit",
+ 0x20 => "Dark Pit",
+ 0x21 => "Zero Suit Samus",
+ 0x22 => "Wario",
+ 0x23 => "Snake",
+ 0x24 => "Ike",
+ 0x25 => "Pokémon Trainer",
+ 0x26 => "Diddy Kong",
+ 0x27 => "Lucas",
+ 0x28 => "Sonic",
+ 0x29 => "King Dedede",
+ 0x2A => "Olimar",
+ 0x2B => "Lucario",
+ 0x2C => "R.O.B.",
+ 0x2D => "Toon Link",
+ 0x2E => "Wolf",
+ 0x2F => "Villager",
+ 0x30 => "Mega Man",
+ 0x31 => "Wii Fit Trainer",
+ 0x32 => "Rosalina & Luma",
+ 0x33 => "Little Mac",
+ 0x34 => "Greninja",
+ 0x35 => "Palutena",
+ 0x36 => "Pac-Man",
+ 0x37 => "Robin",
+ 0x38 => "Shulk",
+ 0x39 => "Bowser Jr.",
+ 0x3A => "Duck Hunt",
+ 0x3B => "Ryu",
+ 0x3C => "Ken",
+ 0x3D => "Cloud",
+ 0x3E => "Corrin",
+ 0x3F => "Bayonetta",
+ 0x40 => "Richter",
+ 0x41 => "Inkling",
+ 0x42 => "Ridley",
+ 0x43 => "King K. Rool",
+ 0x44 => "Simon",
+ 0x45 => "Isabelle",
+ 0x46 => "Incineroar",
+ 0x47 => "Mii Brawler",
+ 0x48 => "Mii Swordfighter",
+ 0x49 => "Mii Gunner",
+ 0x4A => "Piranha Plant",
+ 0x4B => "Joker",
+ 0x4C => "Hero",
+ 0x4D => "Banjo",
+ 0x4E => "Terry",
+ 0x4F => "Byleth",
+ 0x50 => "Min Min",
+ 0x51 => "Steve",
+ 0x52 => "Sephiroth",
+ 0x53 => "Pyra/Mythra",
+ 0x54 => "Kazuya",
+ 0x55 => "Sora",
+ 0xFE => "Random",
+ 0xFF => "Scripted Entity",
+ _ => "Unknown"
+ };
+
+ private static string SuperSmashBrosUltimate_PlayerListing(SparseMultiValue values)
+ {
+ List<(string Character, int PlayerNumber, int? Rank)> players = [];
+
+ foreach (KeyValuePair player in values.Matched)
+ {
+ if (player.Key.StartsWith("player_") && player.Key.EndsWith("_fighter") &&
+ player.Value.BoxedValue is not null)
+ {
+ if (!int.TryParse(player.Key.Split('_')[1], out int playerNumber))
+ continue;
+
+ string character = SuperSmashBrosUltimate_Character(player.Value);
+ int? rank = values.Matched.TryGetValue($"player_{playerNumber}_rank", out Value rankValue)
+ ? rankValue.IntValue
+ : null;
+
+ players.Add((character, playerNumber, rank));
+ }
+ }
+
+ players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList();
+
+ return players.Count > 4
+ ? $"{players.Count} Players - {
+ players.Take(3)
+ .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
+ {
+ 0 => "🥇",
+ 1 => "🥈",
+ 2 => "🥉",
+ _ => ""
+ };
+ }
+
+ private static FormattedValue NsoEmulator_LaunchedGame(SingleValue value) => value.Matched.StringValue switch
+ {
+ #region SEGA Genesis
+
+ "m_0054_e" => Playing("Alien Soldier"),
+ "m_3978_e" => Playing("Alien Storm"),
+ "m_5234_e" => Playing("ALISIA DRAGOON"),
+ "m_5003_e" => Playing("Streets of Rage 2"),
+ "m_4843_e" => Playing("Kid Chameleon"),
+ "m_2874_e" => Playing("Columns"),
+ "m_3167_e" => Playing("Comix Zone"),
+ "m_5007_e" => Playing("Contra: Hard Corps"),
+ "m_0865_e" => Playing("Ghouls 'n Ghosts"),
+ "m_0935_e" => Playing("Dynamite Headdy"),
+ "m_8314_e" => Playing("Earthworm Jim"),
+ "m_5012_e" => Playing("Ecco the Dolphin"),
+ "m_2207_e" => Playing("Flicky"),
+ "m_9432_e" => Playing("Golden Axe II"),
+ "m_5015_e" => Playing("Golden Axe"),
+ "m_5017_e" => Playing("Gunstar Heroes"),
+ "m_0732_e" => Playing("Altered Beast"),
+ "m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"),
+ "m_1654_e" => Playing("Target Earth"),
+ "m_7050_e" => Playing("Light Crusader"),
+ "m_5027_e" => Playing("M.U.S.H.A."),
+ "m_5028_e" => Playing("Phantasy Star IV"),
+ "m_9155_e" => Playing("Pulseman"),
+ "m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"),
+ "m_0098_e" => Playing("Crusader of Centy"),
+ "m_0098_k" => Playing("신창세기 라그나센티"),
+ "m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"),
+ "m_5033_e" => Playing("Ristar"),
+ "m_1987_e" => Playing("MEGA MAN: THE WILY WARS"),
+ "m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"),
+ "m_3353_e" => Playing("Shining Force II"),
+ "m_5036_e" => Playing("Shining Force"),
+ "m_9866_e" => Playing("Sonic The Hedgehog Spinball"),
+ "m_5041_e" => Playing("Sonic The Hedgehog 2"),
+ "m_5523_e" => Playing("Space Harrier II"),
+ "m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"),
+ "m_5044_e" => Playing("STRIDER"),
+ "m_6353_e" => Playing("Super Fantasy Zone"),
+ "m_9569_e" => Playing("Beyond Oasis"),
+ "m_9569_k" => Playing("스토리 오브 도어"),
+ "m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"),
+ "m_9569_pf" => Playing("La Légende de Thor"),
+ "m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"),
+ "m_6811_e" => Playing("The Revenge of Shinobi"),
+ "m_4372_e" => Playing("Thunder Force II"),
+ "m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"),
+ "m_0432_e" => Playing("ToeJam & Earl"),
+ "m_5052_e" => Playing("Castlevania: BLOODLINES"),
+ "m_3626_e" => Playing("VectorMan"),
+ "m_7955_e" => Playing("Sword of Vermilion"),
+ "m_0394_e" => Playing("Virtua Fighter 2"),
+ "m_9417_e" => Playing("Zero Wing"),
+
+ #endregion
+
+ #region Nintendo 64
+
+ "n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"),
+ "n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"),
+ "n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"),
+ "n_3083_e" or "n_3083_p" => Playing("Blast Corps"),
+ "n_3007_e" => Playing("Dr. Mario™ 64"),
+ "n_4238_e" => Playing("Excitebike™ 64"),
+ "n_1870_e" => Playing("Extreme G"),
+ "n_2456_e" => Playing("F-Zero™ X"),
+ "n_4631_e" => Playing("GoldenEye 007"),
+ "n_1635_e" => Playing("Harvest Moon 64"),
+ "n_2225_e" => Playing("Iggy’s Reckin’ Balls"),
+ "n_1625_e" or "n_1625_p" => Playing("JET FORCE GEMINI™"),
+ "n_3052_e" => Playing("Kirby 64™: The Crystal Shards"),
+ "n_4371_e" => Playing("Mario Golf™"),
+ "n_3013_e" => Playing("Mario Kart™ 64"),
+ "n_1053_e" or "n_1053_p" => Playing("Mario Party™ 2"),
+ "n_2965_e" or "n_2965_p" => Playing("Mario Party™ 3"),
+ "n_4737_e" or "n_4737_p" => Playing("Mario Party™"),
+ "n_3017_e" => Playing("Mario Tennis™"),
+ "n_2992_e" or "n_2992_p" => Playing("Paper Mario™"),
+ "n_3783_e" or "n_3783_p" => Playing("Pilotwings™ 64"),
+ "n_1848_e" or "n_1848_pd" or "n_1848_pf" => Playing("Pokémon™ Puzzle League"),
+ "n_3240_e" or "n_3240_pd" or "n_3240_pf" or "n_3240_pi" or "n_3240_ps" => Playing("Pokémon Snap™"),
+ "n_4590_e" or "n_4590_pd" or "n_4590_pf" or "n_4590_pi" or "n_4590_ps" => Playing("Pokémon Stadium™"),
+ "n_3309_e" or "n_3309_pd" or "n_3309_pf" or "n_3309_pi" or "n_3309_ps" => Playing("Pokémon Stadium 2™"),
+ "n_3029_e" => Playing("Sin & Punishment™"),
+ "n_3030_e" => Playing("Star Fox™ 64"),
+ "n_3030_p" => Playing("Lylat Wars™"),
+ "n_3031_e" or "n_3031_p" => Playing("Super Mario 64™"),
+ "n_4813_e" or "n_4813_p" => Playing("Wave Race™ 64"),
+ "n_3034_e" => Playing("WIN BACK: COVERT OPERATIONS"),
+ "n_3034_p" => Playing("OPERATION: WIN BACK"),
+ "n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"),
+ "n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"),
+ "n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"),
+
+ #endregion
+
+ #region NES
+
+ "clv_p_naaae" => Playing("Super Mario Bros.™"),
+ "clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"),
+ "clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"),
+ "clv_p_naade" => Playing("Super Mario Bros.™ 2"),
+ "clv_p_naaee" => Playing("Donkey Kong™"),
+ "clv_p_naafe" => Playing("Donkey Kong Jr.™"),
+ "clv_p_naage" => Playing("Donkey Kong™ 3"),
+ "clv_p_naahe" => Playing("Excitebike™"),
+ "clv_p_naaje" => Playing("EarthBound Beginnings"),
+ "clv_p_naame" => Playing("NES™ Open Tournament Golf"),
+ "clv_p_naane" or "clv_p_naane_sp1" => Playing("The Legend of Zelda™"),
+ "clv_p_naape" or "clv_p_naape_sp1" => Playing("Kirby's Adventure™"),
+ "clv_p_naaqe" or "clv_p_naaqe_sp1" or "clv_p_naaqe_sp2" => Playing("Metroid™"),
+ "clv_p_naare" => Playing("Balloon Fight™"),
+ "clv_p_naase" or "clv_p_naase_sp1" => Playing("Zelda II - The Adventure of Link™"),
+ "clv_p_naate" => Playing("Punch-Out!!™ Featuring Mr. Dream"),
+ "clv_p_naaue" => Playing("Ice Climber™"),
+ "clv_p_naave" or "clv_p_naave_sp1" => Playing("Kid Icarus™"),
+ "clv_p_naawe" => Playing("Mario Bros.™"),
+ "clv_p_naaxe" or "clv_p_naaxe_sp1" => Playing("Dr. Mario™"),
+ "clv_p_naaye" => Playing("Yoshi™"),
+ "clv_p_naaze" => Playing("StarTropics™"),
+ "clv_p_nabce" or "clv_p_nabce_sp1" => Playing("Ghosts'n Goblins™"),
+ "clv_p_nabre" or "clv_p_nabre_sp1" or "clv_p_nabre_sp2" => Playing("Gradius"),
+ "clv_p_nacbe" or "clv_p_nacbe_sp1" => Playing("Ninja Gaiden"),
+ "clv_p_nacce" => Playing("Solomon's Key"),
+ "clv_p_nacde" => Playing("Tecmo Bowl"),
+ "clv_p_nacfe" => Playing("Double Dragon"),
+ "clv_p_nache" => Playing("Double Dragon II: The Revenge"),
+ "clv_p_nacje" => Playing("River City Ransom"),
+ "clv_p_nacke" => Playing("Super Dodge Ball"),
+ "clv_p_nacle" => Playing("Downtown Nekketsu March Super-Awesome Field Day!"),
+ "clv_p_nacpe" => Playing("The Mystery of Atlantis"),
+ "clv_p_nacre" => Playing("Soccer"),
+ "clv_p_nacse" or "clv_p_nacse_sp1" => Playing("Ninja JaJaMaru-kun"),
+ "clv_p_nacte" => Playing("Ice Hockey"),
+ "clv_p_nacue" or "clv_p_nacue_sp1" => Playing("Blaster Master"),
+ "clv_p_nacwe" => Playing("ADVENTURES OF LOLO"),
+ "clv_p_nacxe" => Playing("Wario's Woods™"),
+ "clv_p_nacye" => Playing("Tennis"),
+ "clv_p_nacze" => Playing("Wrecking Crew™"),
+ "clv_p_nadbe" => Playing("Joy Mech Fight™"),
+ "clv_p_nadde" or "clv_p_nadde_sp1" => Playing("Star Soldier"),
+ "clv_p_nadke" => Playing("Tetris®"),
+ "clv_p_nadle" => Playing("Pro Wrestling"),
+ "clv_p_nadpe" => Playing("Baseball"),
+ "clv_p_nadte" or "clv_p_nadte_sp1" => Playing("TwinBee"),
+ "clv_p_nadue" or "clv_p_nadue_sp1" => Playing("Mighty Bomb Jack"),
+ "clv_p_nadve" => Playing("Kung-Fu Heroes"),
+ "clv_p_nadxe" => Playing("City Connection"),
+ "clv_p_nadye" => Playing("Rygar"),
+ "clv_p_naeae" => Playing("Crystalis"),
+ "clv_p_naece" => Playing("Vice: Project Doom"),
+ "clv_p_naehe" => Playing("Clu Clu Land™"),
+ "clv_p_naeie" => Playing("VS. Excitebike™"),
+ "clv_p_naeje" => Playing("Volleyball™"),
+ "clv_p_naeke" => Playing("JOURNEY TO SILIUS"),
+ "clv_p_naele" => Playing("S.C.A.T.: Special Cybernetic Attack Team"),
+ "clv_p_naeme" => Playing("Shadow of the Ninja"),
+ "clv_p_naene" => Playing("Nightshade"),
+ "clv_p_naepe" => Playing("The Immortal"),
+ "clv_p_naeqe" => Playing("Eliminator Boat Duel"),
+ "clv_p_naere" => Playing("Fire 'n Ice"),
+ "clv_p_nafce" => Playing("XEVIOUS"),
+ "clv_p_nagpe" => Playing("DAIVA STORY 6 IMPERIAL OF NIRSARTIA"),
+ "clv_p_nagqe" => Playing("DIG DUGⅡ"),
+ "clv_p_nague" => Playing("MAPPY-LAND"),
+ "clv_p_nahhe" => Playing("Mach Rider™"),
+ "clv_p_nahje" => Playing("Pinball"),
+ "clv_p_nahre" => Playing("Mystery Tower"),
+ "clv_p_nahte" => Playing("Urban Champion™"),
+ "clv_p_nahue" => Playing("Donkey Kong Jr.™ Math"),
+ "clv_p_nahve" => Playing("The Mysterious Murasame Castle"),
+ "clv_p_najae" => Playing("DEVIL WORLD™"),
+ "clv_p_najbe" => Playing("Golf"),
+ "clv_p_najpe" => Playing("R.C. PRO-AM™"),
+ "clv_p_najre" => Playing("COBRA TRIANGLE™"),
+ "clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"),
+ "clv_p_najte" => Playing("SOLAR® JETMAN"),
+
+ #endregion
+
+ #region SNES
+
+ "s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"),
+ "s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"),
+ "s_2182_e" => Playing("BIG RUN"),
+ "s_2156_e" => Playing("Bombuzal"),
+ "s_2002_e" => Playing("BRAWL BROTHERS"),
+ "s_2025_e" => Playing("Breath of Fire II"),
+ "s_2003_e" => Playing("Breath Of Fire"),
+ "s_2163_e" => Playing("Claymates"),
+ "s_2150_e" => Playing("Congo's Caper"),
+ "s_2171_e" => Playing("COSMO GANG THE PUZZLE"),
+ "s_2004_e" => Playing("Demon's Crest"),
+ "s_2026_e" => Playing("Kunio-kun no Dodgeball da yo Zen'in Shūgō!"),
+ "s_2060_e" => Playing("Donkey Kong Country 2: Diddy's Kong Quest"),
+ "s_2061_e" => Playing("Donkey Kong Country 3: Dixie Kong's Double Trouble!"),
+ "s_2055_e" => Playing("Donkey Kong Country"),
+ "s_2139_e" => Playing("DOOMSDAY WARRIOR"),
+ "s_2051_e" => Playing("EarthBound"),
+ "s_2162_e" => Playing("Earthworm Jim™ 2"),
+ "s_2005_e" => Playing("F-ZERO™"),
+ "s_2183_e" => Playing("FATAL FURY 2"),
+ "s_2174_e" => Playing("Fighter's History"),
+ "s_2037_e" => Playing("Harvest Moon"),
+ "s_2161_e" => Playing("Jelly Boy"),
+ "s_2006_e" => Playing("Joe & Mac 2: Lost in the Tropics"),
+ "s_2169_e" => Playing("Caveman Ninja"),
+ "s_2181_e" => Playing("KILLER INSTINCT™"),
+ "s_2029_e" or "s_2029_e_sp1" => Playing("Kirby Super Star™"),
+ "s_2121_e" => Playing("Kirby's Avalanche™"),
+ "s_2007_e" or "s_2007_e_sp1" => Playing("Kirby's Dream Course™"),
+ "s_2008_e" or "s_2008_e_sp1" => Playing("Kirby's Dream Land™ 3"),
+ "s_2172_e" => Playing("Kirby’s Star Stacker™"),
+ "s_2151_e" => Playing("Magical Drop2"),
+ "s_2044_e" => Playing("Mario's Super Picross"),
+ "s_2038_e" => Playing("Natsume Championship Wrestling"),
+ "s_2140_e" => Playing("Operation Logic Bomb"),
+ "s_2034_e" => Playing("Panel de Pon"),
+ "s_2009_e" => Playing("Pilotwings™"),
+ "s_2010_e" => Playing("Pop'n TwinBee"),
+ "s_2157_e" => Playing("Prehistorik Man"),
+ "s_2145_e" => Playing("Psycho Dream"),
+ "s_2141_e" => Playing("Rival Turf!"),
+ "s_2152_e" => Playing("SIDE POCKET"),
+ "s_2158_e" => Playing("Spanky’s™ Quest"),
+ "s_2031_e" => Playing("Star Fox™ 2"),
+ "s_2011_e" => Playing("Star Fox™"),
+ "s_2012_e" => Playing("Stunt Race FX™"),
+ "s_2032_e" => Playing("Amazing Hebereke"),
+ "s_2159_e" => Playing("Super Baseball Simulator 1.000"),
+ "s_2013_e" => Playing("SUPER E.D.F. EARTH DEFENSE FORCE"),
+ "s_2014_e" => Playing("Smash Tennis"),
+ "s_2015_e" => Playing("Super Ghouls'n Ghosts™"),
+ "s_2033_e" => Playing("Super Mario All-Stars™"),
+ "s_2016_e" or "s_2016_e_sp1" => Playing("Super Mario Kart™"),
+ "s_2017_e" or "s_2017_e_sp1" => Playing("Super Mario World™"),
+ "s_2018_e" or "s_2018_e_sp1" => Playing("Super Metroid™"),
+ "s_2184_e" => Playing("Super Ninja Boy"),
+ "s_2019_e" or "s_2019_e_sp1" => Playing("Super Punch-Out!!™"),
+ "s_2020_e" => Playing("Super Puyo Puyo 2"),
+ "s_2133_e" => Playing("SUPER R-TYPE"),
+ "s_2021_e" => Playing("Super Soccer"),
+ "s_2022_e" => Playing("Super Tennis"),
+ "s_2136_e" => Playing("Sutte Hakkun"),
+ "s_2142_e" => Playing("The Ignition Factor"),
+ "s_2143_e" => Playing("The Peace Keepers"),
+ "s_2146_e" => Playing("Tuff E Nuff"),
+ "s_2144_e" => Playing("SUPER VALIS Ⅳ"),
+ "s_2049_e" => Playing("Wild Guns"),
+ "s_2096_e" => Playing("Wrecking Crew™ '98"),
+ "s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"),
+ "s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"),
+
+ #endregion
+
+ #region GameBoy
+
+ "c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"),
+ "c_5022_e" => Playing("Blaster Master: Enemy Below"),
+ "c_3381_e" => Playing("Game & Watch™ Gallery 3"),
+ "c_0282_e" => Playing("Kirby Tilt ‘n’ Tumble™"),
+ "c_4471_e" or "c_4471_p" => Playing("Mario Golf™"),
+ "c_9947_e" => Playing("Mario Tennis™"),
+ "c_3191_e" or "c_3191_p" or "c_3191_x" => Playing("Pokémon™ Trading Card Game"),
+ "c_8914_e" or "c_8914_p" => Playing("Quest for Camelot™"),
+ "c_2648_e" => Playing("Tetris® DX"),
+ "c_5928_e" => Playing("Wario Land™ 3"),
+ "c_3996_e" or "c_3996_pd" or "c_3996_pf" => Playing("The Legend of Zelda™: Link's Awakening DX™"),
+ "c_8852_e" or "c_8852_p" => Playing("The Legend of Zelda™: Oracle of Ages™"),
+ "c_9130_e" or "c_9130_p" => Playing("The Legend of Zelda™: Oracle of Seasons™"),
+ "d_6879_e" => Playing("Alleyway™"),
+ "d_7618_e" => Playing("Baseball"),
+ "d_6005_e" => Playing("BurgerTime Deluxe"),
+ "d_7120_e" => Playing("Castlevania Legends"),
+ "d_2744_e" => Playing("Dr. Mario™"),
+ "d_1593_e" => Playing("Donkey Kong Land 2™"),
+ "d_7216_e" => Playing("Donkey Kong Land III™"),
+ "d_4971_e" => Playing("Donkey Kong Land™"),
+ "d_7984_e" => Playing("GARGOYLE'S QUEST"),
+ "d_8212_e" => Playing("Kirby's Dream Land™ 2"),
+ "d_5661_e" => Playing("Kirby's Dream Land™"),
+ "d_3837_e" => Playing("MEGA MAN II"),
+ "d_1965_e" => Playing("MEGA MAN III"),
+ "d_0194_e" => Playing("MEGA MAN IV"),
+ "d_1425_e" => Playing("MEGA MAN V"),
+ "d_9324_e" => Playing("MEGA MAN: DR. WILY'S REVENGE"),
+ "d_1577_e" => Playing("Metroid™ II - Return of Samus™"),
+ "d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"),
+ "d_7970_e" => Playing("Super Mario Land™"),
+ "d_8484_e" => Playing("Tetris®"),
+
+ #endregion
+
+ #region GameBoy Advance
+
+ "a_9694_e" => Playing("Densetsu no Starfy 1"),
+ "a_5600_e" => Playing("Densetsu no Starfy 2"),
+ "a_7565_e" => Playing("Densetsu no Starfy 3"),
+ "a_6553_e" => Playing("F-ZERO CLIMAX"),
+ "a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"),
+ "a_9283_e" => Playing("F-Zero™ Maximum Velocity"),
+ "a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"),
+ "a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"),
+ "a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"),
+ "a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"),
+ "a_7279_p" => Playing("Kuru Kuru Kururin™"),
+ "a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"),
+ "a_6845_e" => Playing("Mario Kart™: Super Circuit™"),
+ "a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"),
+ "a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"),
+ "a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"),
+ "a_9444_e" => Playing("Super Mario™ Advance"),
+ "a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"),
+ "a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"),
+ "a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"),
+ "a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"),
+ "a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."),
+ "a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"),
+ "a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"),
+ "a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"),
+
+ #endregion
+
+ _ => FormattedValue.ForceReset
+ };
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs
new file mode 100644
index 000000000..d5568962e
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs
@@ -0,0 +1,68 @@
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ public static partial class PlayReports
+ {
+ public static Analyzer Analyzer { get; } = new Analyzer()
+ .AddSpec(
+ "01007ef00011e000",
+ spec => spec
+ .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
+ // reset to normal status when switching between normal & master mode in title screen
+ .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
+ )
+ .AddSpec(
+ "0100f2c0115b6000",
+ spec => spec
+ .AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
+ .AddSpec(
+ "0100000000010000",
+ spec =>
+ spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
+ )
+ .AddSpec(
+ "010075000ecbe000",
+ spec =>
+ spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
+ )
+ .AddSpec(
+ "010028600ebda000",
+ spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
+ )
+ .AddSpec( // Global & China IDs
+ ["0100152000022000", "010075100e8ec000"],
+ spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
+ )
+ .AddSpec(
+ ["0100a3d008c5c000", "01008f6008c5e000"],
+ spec => spec
+ .AddValueFormatter("area_no", PokemonSVArea)
+ .AddValueFormatter("team_circle", PokemonSVUnionCircle)
+ )
+ .AddSpec(
+ "01006a800016e000",
+ spec => spec
+ .AddSparseMultiValueFormatter(
+ [
+ // Metadata to figure out what PlayReport we have.
+ "match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count",
+ "adv_slot",
+ // List of Fighters
+ "player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter",
+ "player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter",
+ // List of rankings/placements
+ "player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank",
+ "player_6_rank", "player_7_rank", "player_8_rank"
+ ],
+ SuperSmashBrosUltimate_Mode
+ )
+ )
+ .AddSpec(
+ [
+ "0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
+ "010012f017576000", "0100c62011050000", "0100b3c014bda000"],
+ spec => spec.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
+ );
+
+ private static string Playing(string game) => $"Playing {game}";
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs
new file mode 100644
index 000000000..f1b94de68
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs
@@ -0,0 +1,235 @@
+using MsgPack;
+using Ryujinx.Ava.Utilities.AppLibrary;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// A mapping of title IDs to value formatter specs.
+ ///
+ /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself.
+ ///
+ public class GameSpec
+ {
+ public static GameSpec Create(string requiredTitleId, params IEnumerable otherTitleIds)
+ => new() { TitleIds = otherTitleIds.Prepend(requiredTitleId).ToArray() };
+
+ public static GameSpec Create(IEnumerable titleIds)
+ => new() { TitleIds = titleIds.ToArray() };
+
+ private int _lastPriority;
+
+ public required string[] TitleIds { get; init; }
+
+ public List ValueFormatters { get; } = [];
+
+
+ ///
+ /// Add a value formatter to the current
+ /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The key name to match.
+ /// The function which can return a potential formatted value.
+ /// The current , for chaining convenience.
+ public GameSpec AddValueFormatter(
+ string reportKey,
+ SingleValueFormatter valueFormatter
+ ) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter);
+
+ ///
+ /// Add a value formatter at a specific priority to the current
+ /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key name to match.
+ /// The function which can return a potential formatted value.
+ /// The current , for chaining convenience.
+ public GameSpec AddValueFormatter(
+ int priority,
+ string reportKey,
+ SingleValueFormatter valueFormatter
+ ) => AddValueFormatter(new FormatterSpec
+ {
+ Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter
+ });
+
+ ///
+ /// Add a multi-value formatter to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddMultiValueFormatter(
+ string[] reportKeys,
+ MultiValueFormatter valueFormatter
+ ) => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
+
+ ///
+ /// Add a multi-value formatter at a specific priority to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddMultiValueFormatter(
+ int priority,
+ string[] reportKeys,
+ MultiValueFormatter valueFormatter
+ ) => AddValueFormatter(new MultiFormatterSpec
+ {
+ Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
+ });
+
+ ///
+ /// Add a multi-value formatter to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The 'Sparse' multi-value formatters do not require every key to be present.
+ /// If you need this requirement, use .
+ ///
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddSparseMultiValueFormatter(
+ string[] reportKeys,
+ SparseMultiValueFormatter valueFormatter
+ ) => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
+
+ ///
+ /// Add a multi-value formatter at a specific priority to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The 'Sparse' multi-value formatters do not require every key to be present.
+ /// If you need this requirement, use .
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddSparseMultiValueFormatter(
+ int priority,
+ string[] reportKeys,
+ SparseMultiValueFormatter valueFormatter
+ ) => AddValueFormatter(new SparseMultiFormatterSpec
+ {
+ Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
+ });
+
+ private GameSpec AddValueFormatter(T formatterSpec) where T : FormatterSpecBase
+ {
+ ValueFormatters.Add(formatterSpec);
+ return this;
+ }
+ }
+
+ ///
+ /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
+ ///
+ public class FormatterSpec : FormatterSpecBase
+ {
+ public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
+ {
+ if (!playReport.ReportData.AsDictionary().TryGetValue(ReportKeys[0], out MessagePackObject valuePackObject))
+ {
+ result = null;
+ return false;
+ }
+
+ result = valuePackObject;
+ return true;
+ }
+ }
+
+ ///
+ /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
+ ///
+ public class MultiFormatterSpec : FormatterSpecBase
+ {
+ public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
+ {
+ List packedObjects = [];
+ foreach (string reportKey in ReportKeys)
+ {
+ if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
+ {
+ result = null;
+ return false;
+ }
+
+ packedObjects.Add(valuePackObject);
+ }
+
+ result = packedObjects;
+ return true;
+ }
+ }
+
+ ///
+ /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
+ ///
+ public class SparseMultiFormatterSpec : FormatterSpecBase
+ {
+ public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
+ {
+ Dictionary packedObjects = [];
+ foreach (string reportKey in ReportKeys)
+ {
+ if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
+ continue;
+
+ packedObjects.Add(reportKey, valuePackObject);
+ }
+
+ result = packedObjects;
+ return true;
+ }
+ }
+
+ public abstract class FormatterSpecBase
+ {
+ public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data);
+
+ public int Priority { get; init; }
+ public string[] ReportKeys { get; init; }
+ public Delegate Formatter { get; init; }
+
+ public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
+ out FormattedValue formattedValue)
+ {
+ formattedValue = default;
+ if (!GetData(playReport, out object data))
+ return false;
+
+ if (data is FormattedValue fv)
+ {
+ formattedValue = fv;
+ return true;
+ }
+
+ switch (Formatter)
+ {
+ case SingleValueFormatter svf when data is MessagePackObject match:
+ formattedValue = svf(
+ new SingleValue(match) { Application = appMeta, PlayReport = playReport }
+ );
+ return true;
+ case MultiValueFormatter mvf when data is List matches:
+ formattedValue = mvf(
+ new MultiValue(matches) { Application = appMeta, PlayReport = playReport }
+ );
+ return true;
+ case SparseMultiValueFormatter smvf when data is Dictionary sparseMatches:
+ formattedValue = smvf(
+ new SparseMultiValue(sparseMatches) { Application = appMeta, PlayReport = playReport }
+ );
+ return true;
+ default:
+ throw new InvalidOperationException("Formatter delegate is not of a known type!");
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs
new file mode 100644
index 000000000..b3108a41e
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Value.cs
@@ -0,0 +1,160 @@
+using MsgPack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// The base input data to a ValueFormatter delegate,
+ /// and the matched from the Play Report.
+ ///
+ public readonly struct Value
+ {
+ public Value(MessagePackObject packedValue)
+ {
+ PackedValue = packedValue;
+ }
+
+ ///
+ /// The matched value from the Play Report.
+ ///
+ public MessagePackObject PackedValue { get; init; }
+
+ ///
+ /// Access the as its underlying .NET type.
+ ///
+ /// Does not seem to work well with comparing numeric types,
+ /// so use XValue properties for that.
+ ///
+ public object BoxedValue => PackedValue.ToObject();
+
+ public override string ToString()
+ {
+ object boxed = BoxedValue;
+ return boxed == null
+ ? "null"
+ : boxed.ToString();
+ }
+
+ public static implicit operator Value(MessagePackObject matched) => new(matched);
+
+ public static Value[] ConvertPackedObjects(IEnumerable packObjects)
+ => packObjects.Select(packObject => new Value(packObject)).ToArray();
+
+ public static Dictionary ConvertPackedObjectMap(Dictionary packObjects)
+ => packObjects.ToDictionary(
+ x => x.Key,
+ x => new Value(x.Value)
+ );
+
+ #region AsX accessors
+
+ public bool BooleanValue => PackedValue.AsBoolean();
+ public byte ByteValue => PackedValue.AsByte();
+ public sbyte SByteValue => PackedValue.AsSByte();
+ public short ShortValue => PackedValue.AsInt16();
+ public ushort UShortValue => PackedValue.AsUInt16();
+ public int IntValue => PackedValue.AsInt32();
+ public uint UIntValue => PackedValue.AsUInt32();
+ public long LongValue => PackedValue.AsInt64();
+ public ulong ULongValue => PackedValue.AsUInt64();
+ public float FloatValue => PackedValue.AsSingle();
+ public double DoubleValue => PackedValue.AsDouble();
+ public string StringValue => PackedValue.AsString();
+ public Span BinaryValue => PackedValue.AsBinary();
+
+ #endregion
+ }
+
+ ///
+ /// A potential formatted value returned by a ValueFormatter delegate.
+ ///
+ public readonly struct FormattedValue
+ {
+ ///
+ /// Was any handler able to match anything in the Play Report?
+ ///
+ public bool Handled { get; private init; }
+
+ ///
+ /// Did the handler request the caller of the to reset the existing value?
+ ///
+ public bool Reset { get; private init; }
+
+ ///
+ /// The formatted value, only present if is true, and is false.
+ ///
+ public string FormattedString { get; private init; }
+
+ ///
+ /// The intended path of execution for having a string to return: simply return the string.
+ /// This implicit conversion will make the struct for you.
+ ///
+ /// If the input is null, is returned.
+ ///
+ /// The formatted string value.
+ /// The automatically constructed struct.
+ public static implicit operator FormattedValue(string formattedValue)
+ => formattedValue is not null
+ ? new FormattedValue { Handled = true, FormattedString = formattedValue }
+ : Unhandled;
+
+ public override string ToString()
+ {
+ if (!Handled)
+ return "";
+
+ if (Reset)
+ return "";
+
+ return FormattedString;
+ }
+
+ ///
+ /// Return this to tell the caller there is no value to return.
+ ///
+ public static readonly FormattedValue Unhandled = default;
+
+ ///
+ /// Return this to suggest the caller reset the value it's using the for.
+ ///
+ public static readonly FormattedValue ForceReset = new() { Handled = true, Reset = true };
+
+ ///
+ /// A delegate singleton you can use to always return in a .
+ ///
+ public static readonly SingleValueFormatter SingleAlwaysResets = _ => ForceReset;
+
+ ///
+ /// A delegate singleton you can use to always return in a .
+ ///
+ public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
+
+ ///
+ /// A delegate singleton you can use to always return in a .
+ ///
+ public static readonly SparseMultiValueFormatter SparseMultiAlwaysResets = _ => ForceReset;
+
+ ///
+ /// A delegate factory you can use to always return the specified
+ /// in a .
+ ///
+ /// The string to always return for this delegate instance.
+ public static SingleValueFormatter SingleAlwaysReturns(string formattedValue) => _ => formattedValue;
+
+ ///
+ /// A delegate factory you can use to always return the specified
+ /// in a .
+ ///
+ /// The string to always return for this delegate instance.
+ public static MultiValueFormatter MultiAlwaysReturns(string formattedValue) => _ => formattedValue;
+
+ ///
+ /// A delegate factory you can use to always return the specified
+ /// in a .
+ ///
+ /// The string to always return for this delegate instance.
+ public static SparseMultiValueFormatter SparseMultiAlwaysReturns(string formattedValue) => _ => formattedValue;
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
deleted file mode 100644
index 47c36a396..000000000
--- a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using Gommon;
-using MsgPack;
-using Ryujinx.Ava.Utilities.AppLibrary;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-
-namespace Ryujinx.Ava.Utilities
-{
- ///
- /// The entrypoint for the Play Report analysis system.
- ///
- public class PlayReportAnalyzer
- {
- private readonly List _specs = [];
-
- ///
- /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
- ///
- /// The ID of the game to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(string titleId, Func transform)
- {
- Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
- return this;
- }
-
- ///
- /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
- ///
- /// The ID of the game to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(string titleId, Action transform)
- {
- Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
- return this;
- }
-
- ///
- /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
- ///
- /// The IDs of the games to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(IEnumerable titleIds,
- Func transform)
- {
- string[] tids = titleIds.ToArray();
- Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
- return this;
- }
-
- ///
- /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
- ///
- /// The IDs of the games to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action transform)
- {
- string[] tids = titleIds.ToArray();
- Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
- return this;
- }
-
-
- ///
- /// Runs the configured for the specified game title ID.
- ///
- /// The game currently running.
- /// The Application metadata information, including localized game name and play time information.
- /// The Play Report received from HLE.
- /// A struct representing a possible formatted value.
- public FormattedValue Format(
- string runningGameId,
- ApplicationMetadata appMeta,
- MessagePackObject playReport
- )
- {
- if (!playReport.IsDictionary)
- return FormattedValue.Unhandled;
-
- if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
- return FormattedValue.Unhandled;
-
- foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
- {
- if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
- continue;
-
- return formatSpec.ValueFormatter(new PlayReportValue
- {
- Application = appMeta, PackedValue = valuePackObject
- });
- }
-
- return FormattedValue.Unhandled;
- }
-
- ///
- /// A potential formatted value returned by a .
- ///
- public readonly struct FormattedValue
- {
- ///
- /// Was any handler able to match anything in the Play Report?
- ///
- public bool Handled { get; private init; }
-
- ///
- /// Did the handler request the caller of the to reset the existing value?
- ///
- public bool Reset { get; private init; }
-
- ///
- /// The formatted value, only present if is true, and is false.
- ///
- public string FormattedString { get; private init; }
-
- ///
- /// The intended path of execution for having a string to return: simply return the string.
- /// This implicit conversion will make the struct for you.
- ///
- /// If the input is null, is returned.
- ///
- /// The formatted string value.
- /// The automatically constructed struct.
- public static implicit operator FormattedValue(string formattedValue)
- => formattedValue is not null
- ? new FormattedValue { Handled = true, FormattedString = formattedValue }
- : Unhandled;
-
- ///
- /// Return this to tell the caller there is no value to return.
- ///
- public static FormattedValue Unhandled => default;
-
- ///
- /// Return this to suggest the caller reset the value it's using the for.
- ///
- public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
-
- ///
- /// A delegate singleton you can use to always return in a .
- ///
- public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
-
- ///
- /// A delegate factory you can use to always return the specified
- /// in a .
- ///
- /// The string to always return for this delegate instance.
- public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
- }
- }
-
- ///
- /// A mapping of title IDs to value formatter specs.
- ///
- /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself.
- ///
- public class PlayReportGameSpec
- {
- public required string[] TitleIds { get; init; }
- public List SimpleValueFormatters { get; } = [];
-
- ///
- /// Add a value formatter to the current
- /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
- ///
- /// The key name to match.
- /// The function which can return a potential formatted value.
- /// The current , for chaining convenience.
- public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
- {
- SimpleValueFormatters.Add(new FormatterSpec
- {
- Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
- });
- return this;
- }
-
- ///
- /// Add a value formatter at a specific priority to the current
- /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
- ///
- /// The resolution priority of this value formatter. Higher resolves sooner.
- /// The key name to match.
- /// The function which can return a potential formatted value.
- /// The current , for chaining convenience.
- public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
- PlayReportValueFormatter valueFormatter)
- {
- SimpleValueFormatters.Add(new FormatterSpec
- {
- Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
- });
- return this;
- }
-
- ///
- /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
- ///
- public struct FormatterSpec
- {
- public required int Priority { get; init; }
- public required string ReportKey { get; init; }
- public PlayReportValueFormatter ValueFormatter { get; init; }
- }
- }
-
- ///
- /// The input data to a ,
- /// containing the currently running application's ,
- /// and the matched from the Play Report.
- ///
- public class PlayReportValue
- {
- ///
- /// The currently running application's .
- ///
- public ApplicationMetadata Application { get; init; }
-
- ///
- /// The matched value from the Play Report.
- ///
- public MessagePackObject PackedValue { get; init; }
-
- ///
- /// Access the as its underlying .NET type.
- ///
- /// Does not seem to work well with comparing numeric types,
- /// so use and the AsX (where X is a numerical type name i.e. Int32) methods for that.
- ///
- public object BoxedValue => PackedValue.ToObject();
-
- #region AsX accessors
-
- public bool BooleanValue => PackedValue.AsBoolean();
- public byte ByteValye => PackedValue.AsByte();
- public sbyte SByteValye => PackedValue.AsSByte();
- public short ShortValye => PackedValue.AsInt16();
- public ushort UShortValye => PackedValue.AsUInt16();
- public int IntValye => PackedValue.AsInt32();
- public uint UIntValye => PackedValue.AsUInt32();
- public long LongValye => PackedValue.AsInt64();
- public ulong ULongValye => PackedValue.AsUInt64();
- public float FloatValue => PackedValue.AsSingle();
- public double DoubleValue => PackedValue.AsDouble();
- public string StringValue => PackedValue.AsString();
- public Span BinaryValue => PackedValue.AsBinary();
-
- #endregion
- }
-
- ///
- /// The delegate type that powers the entire analysis system (as it currently is).
- /// Takes in the result value from the Play Report, and outputs:
- ///
- /// a formatted string,
- ///
- /// a signal that nothing was available to handle it,
- ///
- /// OR a signal to reset the value that the caller is using the for.
- ///
- public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
-}