Compare commits

..

30 Commits

Author SHA1 Message Date
IvonWei
68066bd79b Merge 5e8af26516 into 1c0813d09d 2025-02-09 02:35:42 -06:00
Evan Husted
5e8af26516 Merge branch 'master' into master 2025-02-04 20:28:40 -06:00
madwind
9356b68f26 Add a '*' to label the virtual controller. 2025-01-13 08:41:32 +08:00
IvonWei
14aafebaa6 Merge branch 'Ryubing:master' into master 2025-01-13 08:33:30 +08:00
Evan Husted
4518666a04 Merge branch 'master' into master 2025-01-10 21:39:41 -06:00
madwind
4399edaa9f Fix typo: SQL_JOYBATTERYUPDATED => SDL_JOYBATTERYUPDATED 2025-01-02 11:50:20 +08:00
Evan Husted
4e77bcb55a Merge branch 'master' into master 2025-01-01 21:19:02 -06:00
IvonWei
3cbd7dc1a1 Merge branch 'Ryubing:master' into master 2024-12-31 19:21:10 +08:00
IvonWei
536f792558 Merge branch 'Ryubing:master' into master 2024-12-30 22:04:08 +08:00
madwind
7a451ab160 fix GetMappedStateSnapshot 2024-12-30 22:01:21 +08:00
IvonWei
99c7c3fb14 Merge branch 'Ryubing:master' into master 2024-12-29 18:37:03 +08:00
Evan Husted
09e7b660f4 Merge branch 'master' into master 2024-12-29 03:42:20 -06:00
madwind
69dfd8c60e fix right Stick 2024-12-29 02:11:31 +08:00
madwind
8e50dd9fa6 fix right JoyCon stick 2024-12-29 01:49:25 +08:00
madwind
68c03051ad For the JoyCon controller, wrap SDL2Gamepad as SDL2JoyCon to use a suitable layout and correct the motion sensing and joystick orientation. 2024-12-29 00:56:03 +08:00
Evan Husted
a837294b11 Merge branch 'master' into master 2024-12-28 06:01:06 -06:00
IvonWei
f1c0cc8076 Merge branch 'Ryubing:master' into master 2024-12-28 09:09:10 +08:00
madwind
6dec7ff8ba fix motionData 2024-12-28 09:07:22 +08:00
madwind
20fdbff964 clean log 2024-12-26 14:47:40 +08:00
IvonWei
e426680cb0 Merge branch 'GreemDev:master' into master 2024-12-26 11:58:50 +08:00
madwind
7863e97cb0 invoke OnGamepadConnected and OnGamepadDisconnected 2024-12-26 11:58:00 +08:00
madwind
c4dea0ee28 add SQL_JOYBATTERYUPDATED , OnJoyBatteryUpdated 2024-12-26 11:54:52 +08:00
IvonWei
e0b6a01e9d Merge branch 'GreemDev:master' into master 2024-12-25 17:00:53 +08:00
madwind
e509ffa716 delay 2000ms before ShowPowerLevel 2024-12-25 16:57:36 +08:00
IvonWei
714c68b548 Merge branch 'GreemDev:master' into master 2024-12-25 10:41:20 +08:00
madwind
fec197d9ec log powerLevel 2024-12-25 10:39:07 +08:00
IvonWei
a4b2feef79 Merge branch 'GreemDev:master' into master 2024-12-23 22:13:47 +08:00
IvonWei
ad7d9d1ce0 Update NpadController.cs
add ?
2024-12-23 18:55:49 +08:00
IvonWei
86f9544910 Update NpadController.cs
back to Debug
2024-12-23 18:54:11 +08:00
madwind
e9ecbd44fc Add a virtual controller to merge Joy-Cons. 2024-12-23 17:57:55 +08:00
69 changed files with 1362 additions and 1520 deletions

View File

@@ -29,7 +29,7 @@ env:
jobs:
tag:
name: Create tag
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- name: Get version info
id: version_info
@@ -202,7 +202,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4

View File

@@ -18,7 +18,7 @@ env:
jobs:
tag:
name: Create tag
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- name: Get version info
id: version_info
@@ -183,7 +183,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4

View File

@@ -39,12 +39,12 @@
<p align="center">
Click below to join the Discord:
<br>
<a href="https://discord.gg/PEuzjrFXUA">
<a href="https://discord.gg/dHPrkBkkyA">
<img src="https://img.shields.io/discord/1294443224030511104?color=5865F2&label=Ryubing&logo=discord&logoColor=white" alt="Discord">
</a>
<br>
<br>
<img src="https://raw.githubusercontent.com/Ryubing/Ryujinx/refs/heads/master/docs/shell.png">
<img src="https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/docs/shell.png">
</p>
## Usage

View File

@@ -1249,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;slow;gpu,ingame,2025-02-12 16:03:00
0100E1F013674000,"FUSER™",nvdec;UE4,playable,2022-10-17 20:58:32
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
@@ -2063,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
,"nx-hbmenu",Needs Update;homebrew,boots,2024-04-06 22:05:32
0000000000000000,"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
@@ -2471,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
,"RSDKv5u",homebrew,ingame,2024-04-01 16:25:34
0000000000000000,"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
@@ -2480,7 +2480,6 @@
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
@@ -2674,10 +2673,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
,"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
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
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
@@ -2694,7 +2693,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
,"SpaceCadetPinball",homebrew,ingame,2024-04-18 19:30:04
0000000000000000,"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
@@ -2839,7 +2838,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
,"Super Mario World",homebrew,boots,2024-06-13 01:40:31
0000000000000000,"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 + Bowsers Fury",ldn-works,playable,2024-07-31 10:45:37
01004F8006A78000,"Super Meat Boy",services,playable,2020-04-02 23:10:07
@@ -2988,8 +2987,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
01006fe0096ac000,"The Jackbox Party Pack 5",ldn-untested,boots,2025-02-03 22:32:00
01005a400db52000,"The Jackbox Party Pack 6",ldn-untested,boots,2025-02-03 22:32: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
1 title_id game_name labels status last_updated
1249 0100A6B00D4EC000 Furwind playable 2021-02-19 19:44:08
1250 0100ECE00C0C4000 Fury Unleashed crash;services ingame 2020-10-18 11:52:40
1251 010070000ED9E000 Fury Unleashed Demo playable 2020-10-08 20:09:21
1252 0100E1F013674000 FUSER™ nvdec;UE4;slow;gpu nvdec;UE4 ingame playable 2025-02-12 16:03:00 2022-10-17 20:58:32
1253 0100A7A015E4C000 Fushigi no Gensokyo Lotus Labyrinth Needs Update;audio;gpu;nvdec ingame 2021-01-20 15:30:02
1254 01003C300B274000 Futari de! Nyanko Daisensou playable 2024-01-05 22:26:52
1255 010055801134E000 FUZE Player online-broken;vulkan-backend-bug ingame 2022-10-18 12:23:53
2063 010002700C34C000 Numbala playable 2020-05-11 12:01:07
2064 010020500C8C8000 Number Place 10000 gpu menus 2021-11-24 09:14:23
2065 010003701002C000 Nurse Love Syndrome playable 2022-10-13 10:05:22
2066 0000000000000000 nx-hbmenu Needs Update;homebrew boots 2024-04-06 22:05:32
2067 nxquake2 services;crash;homebrew nothing 2022-08-04 23:14:04
2068 010049F00EC30000 Nyan Cat: Lost in Space online playable 2021-06-12 13:22:03
2069 01002E6014FC4000 O---O playable 2022-10-29 12:12:14
2471 0100AFE00DDAC000 Royal Roads playable 2020-11-17 12:54:38
2472 0100E2C00B414000 RPG Maker MV nvdec playable 2021-01-05 20:12:01
2473 01005CD015986000 rRootage Reloaded playable 2022-08-05 23:20:18
2474 0000000000000000 RSDKv5u homebrew ingame 2024-04-01 16:25:34
2475 010009B00D33C000 Rugby Challenge 4 slow;online-broken;UE4 playable 2022-10-06 12:45:53
2476 01006EC00F2CC000 RUINER UE4 playable 2022-10-03 14:11:33
2477 010074F00DE4A000 Run the Fan playable 2021-02-27 13:36:28
2480 010081C0191D8000 Rune Factory 3 Special playable 2023-10-15 08:32:49
2481 010051D00E3A4000 Rune Factory 4 Special 32-bit;crash;nvdec ingame 2023-05-06 08:49:17
2482 010014D01216E000 Rune Factory 5 (JP) gpu ingame 2021-06-01 12:00:36
010071E0145F8000 Rustler playable 2025-02-10 20:17:12
2483 0100E21013908000 RWBY: Grimm Eclipse - Definitive Edition online-broken playable 2022-11-03 10:44:01
2484 010012C0060F0000 RXN -Raijin- nvdec playable 2021-01-10 16:05:43
2485 0100B8B012ECA000 S.N.I.P.E.R. - Hunter Scope playable 2021-04-19 15:58:09
2673 01004F401BEBE000 Song of Nunu: A League of Legends Story ingame 2024-07-12 18:53:44
2674 0100E5400BF94000 Songbird Symphony playable 2021-02-27 02:44:04
2675 010031D00A604000 Songbringer playable 2020-06-22 10:42:02
2676 0000000000000000 Sonic 1 (2013) crash;homebrew ingame 2024-04-06 18:31:20
2677 0000000000000000 Sonic 2 (2013) crash;homebrew ingame 2024-04-01 16:25:30
2678 0000000000000000 Sonic A.I.R homebrew ingame 2024-04-01 16:25:32
2679 0000000000000000 Sonic CD crash;homebrew ingame 2024-04-01 16:25:31
2680 010040E0116B8000 Sonic Colors: Ultimate playable 2022-11-12 21:24:26
2681 01001270012B6000 SONIC FORCES™ playable 2024-07-28 13:11:21
2682 01004AD014BF0000 Sonic Frontiers gpu;deadlock;amd-vendor-bug;intel-vendor-bug ingame 2024-09-05 09:18:53
2693 0100707011722000 Space Elite Force playable 2020-11-27 15:21:05
2694 010047B010260000 Space Pioneer playable 2022-10-20 12:24:37
2695 010010A009830000 Space Ribbon playable 2022-08-15 17:17:10
2696 0000000000000000 SpaceCadetPinball homebrew ingame 2024-04-18 19:30:04
2697 0100D9B0041CE000 Spacecats with Lasers playable 2022-08-15 17:22:44
2698 010034800FB60000 Spaceland playable 2020-11-01 14:31:56
2699 010028D0045CE000 Sparkle 2 playable 2020-10-19 11:51:39
2838 0100000000010000 Super Mario Odyssey™ nvdec;intel-vendor-bug;mac-bug playable 2024-08-25 01:32:34
2839 010036B0034E4000 Super Mario Party™ gpu;Needs Update;ldn-works ingame 2024-06-21 05:10:16
2840 0100BC0018138000 Super Mario RPG™ gpu;audio;nvdec ingame 2024-06-19 17:43:42
2841 0000000000000000 Super Mario World homebrew boots 2024-06-13 01:40:31
2842 010049900F546000 Super Mario™ 3D All-Stars services-horizon;slow;vulkan;amd-vendor-bug ingame 2024-05-07 02:38:16
2843 010028600EBDA000 Super Mario™ 3D World + Bowser’s Fury ldn-works playable 2024-07-31 10:45:37
2844 01004F8006A78000 Super Meat Boy services playable 2020-04-02 23:10:07
2987 010015D003EE4000 The Jackbox Party Pack 2 online-working playable 2022-08-22 18:23:40
2988 0100CC80013D6000 The Jackbox Party Pack 3 slow;online-working playable 2022-08-22 18:41:06
2989 0100E1F003EE8000 The Jackbox Party Pack 4 online-working playable 2022-08-22 18:56:34
2990 01006fe0096ac000 The Jackbox Party Pack 5 slow;online-working ldn-untested ingame boots 2025-02-14 05:32:00 2025-02-03 22:32:00
2991 01005a400db52000 The Jackbox Party Pack 6 slow;online-working ldn-untested ingame boots 2025-02-14 05:26:00 2025-02-03 22:32:00
2992 010052C00B184000 The Journey Down: Chapter One nvdec playable 2021-02-24 13:32:41
2993 01006BC00B188000 The Journey Down: Chapter Three nvdec playable 2021-02-24 13:45:27
2994 01009AB00B186000 The Journey Down: Chapter Two nvdec playable 2021-02-24 13:32:13

View File

@@ -7,7 +7,6 @@ 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;
@@ -22,7 +21,6 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity;
}
Allocator = allocator;
Block = allocator.Reserve(maxSize);
_maxSize = maxSize;
_sizeGranularity = granularity;

View File

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

View File

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

View File

@@ -219,7 +219,6 @@ 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

View File

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

View File

@@ -12,7 +12,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
private const int CodeAlignment = 4; // Bytes.
private const int SharedCacheSize = 2047 * 1024 * 1024;
private const int LocalCacheSize = 256 * 1024 * 1024;
private const int LocalCacheSize = 128 * 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.

View File

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

View File

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

View File

@@ -150,7 +150,6 @@ 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 },

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
@@ -36,6 +37,7 @@ namespace Ryujinx.Input.SDL2
SDL2Driver.Instance.Initialize();
SDL2Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
SDL2Driver.Instance.OnJoyBatteryUpdated += HandleJoyBatteryUpdated;
// Add already connected gamepads
int numJoysticks = SDL_NumJoysticks();
@@ -83,19 +85,30 @@ namespace Ryujinx.Input.SDL2
private void HandleJoyStickDisconnected(int joystickInstanceId)
{
bool joyConPairDisconnected = false;
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
return;
lock (_lock)
{
_gamepadsIds.Remove(id);
if (!SDL2JoyConPair.IsCombinable(_gamepadsIds))
{
_gamepadsIds.Remove(SDL2JoyConPair.Id);
joyConPairDisconnected = true;
}
}
OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected)
{
OnGamepadDisconnected?.Invoke(SDL2JoyConPair.Id);
}
}
private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId)
{
bool joyConPairConnected = false;
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
{
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
@@ -120,13 +133,29 @@ namespace Ryujinx.Input.SDL2
_gamepadsIds.Insert(joystickDeviceId, id);
else
_gamepadsIds.Add(id);
if (SDL2JoyConPair.IsCombinable(_gamepadsIds))
{
_gamepadsIds.Remove(SDL2JoyConPair.Id);
_gamepadsIds.Add(SDL2JoyConPair.Id);
joyConPairConnected = true;
}
}
OnGamepadConnected?.Invoke(id);
if (joyConPairConnected)
{
OnGamepadConnected?.Invoke(SDL2JoyConPair.Id);
}
}
}
}
private void HandleJoyBatteryUpdated(int joystickDeviceId, SDL_JoystickPowerLevel powerLevel)
{
Logger.Info?.Print(LogClass.Hid,
$"{SDL_GameControllerNameForIndex(joystickDeviceId)} power level: {powerLevel}");
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
@@ -157,6 +186,14 @@ namespace Ryujinx.Input.SDL2
public IGamepad GetGamepad(string id)
{
if (id == SDL2JoyConPair.Id)
{
lock (_lock)
{
return SDL2JoyConPair.GetGamepad(_gamepadsIds);
}
}
int joystickIndex = GetJoystickIndexByGamepadId(id);
if (joystickIndex == -1)
@@ -165,12 +202,16 @@ namespace Ryujinx.Input.SDL2
}
nint gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == nint.Zero)
{
return null;
}
if (SDL_GameControllerName(gamepadHandle).StartsWith(SDL2JoyCon.Prefix))
{
return new SDL2JoyCon(gamepadHandle, id);
}
return new SDL2Gamepad(gamepadHandle, id);
}

View File

@@ -0,0 +1,409 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
internal class SDL2JoyCon : IGamepad
{
private bool HasConfiguration => _configuration != null;
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
}
private StandardControllerInputConfig _configuration;
private readonly Dictionary<GamepadButtonInputId,SDL_GameControllerButton> _leftButtonsDriverMapping = new()
{
{ GamepadButtonInputId.LeftStick , SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK },
{GamepadButtonInputId.DpadUp ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y},
{GamepadButtonInputId.DpadDown ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A},
{GamepadButtonInputId.DpadLeft ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B},
{GamepadButtonInputId.DpadRight ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X},
{GamepadButtonInputId.Minus ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START},
{GamepadButtonInputId.LeftShoulder,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE2},
{GamepadButtonInputId.LeftTrigger,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE4},
{GamepadButtonInputId.SingleRightTrigger0,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{GamepadButtonInputId.SingleLeftTrigger0,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
};
private readonly Dictionary<GamepadButtonInputId,SDL_GameControllerButton> _rightButtonsDriverMapping = new()
{
{GamepadButtonInputId.RightStick,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK},
{GamepadButtonInputId.A,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B},
{GamepadButtonInputId.B,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y},
{GamepadButtonInputId.X,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A},
{GamepadButtonInputId.Y,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X},
{GamepadButtonInputId.Plus,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START},
{GamepadButtonInputId.RightShoulder,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE1},
{GamepadButtonInputId.RightTrigger,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE3},
{GamepadButtonInputId.SingleRightTrigger1,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{GamepadButtonInputId.SingleLeftTrigger1,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER}
};
private readonly Dictionary<GamepadButtonInputId, SDL_GameControllerButton> _buttonsDriverMapping;
private readonly Lock _userMappingLock = new();
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound, StickInputId.Left, StickInputId.Right,
};
public GamepadFeaturesFlag Features { get; }
private nint _gamepadHandle;
private enum JoyConType
{
Left, Right
}
public const string Prefix = "Nintendo Switch Joy-Con";
public const string LeftName = "Nintendo Switch Joy-Con (L)";
public const string RightName = "Nintendo Switch Joy-Con (R)";
private readonly JoyConType _joyConType;
public SDL2JoyCon(nint gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
Name = SDL_GameControllerName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL,
SDL_bool.SDL_TRUE) != 0)
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO,
SDL_bool.SDL_TRUE) != 0)
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
switch (Name)
{
case LeftName:
{
_buttonsDriverMapping = _leftButtonsDriverMapping;
_joyConType = JoyConType.Left;
break;
}
case RightName:
{
_buttonsDriverMapping = _rightButtonsDriverMapping;
_joyConType = JoyConType.Right;
break;
}
}
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) == SDL_bool.SDL_TRUE &&
SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Motion;
}
int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
if (error == 0)
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GameControllerGetAttached(_gamepadHandle) == SDL_bool.SDL_TRUE;
protected virtual void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
SDL_GameControllerClose(_gamepadHandle);
_gamepadHandle = nint.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Rumble))
return;
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
if (durationMs == uint.MaxValue)
{
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) !=
0)
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
else if (durationMs > SDL_HAPTIC_INFINITY)
{
Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
}
else
{
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0)
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
}
public Vector3 GetMotionData(MotionInputId inputId)
{
SDL_SensorType sensorType = inputId switch
{
MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL,
MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO,
_ => SDL_SensorType.SDL_SENSOR_INVALID
};
if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID)
return Vector3.Zero;
const int ElementCount = 3;
unsafe
{
float* values = stackalloc float[ElementCount];
int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (nint)values, ElementCount);
if (result != 0)
return Vector3.Zero;
Vector3 value = _joyConType switch
{
JoyConType.Left => new Vector3(-values[2], values[1], values[0]),
JoyConType.Right => new Vector3(values[2], values[1], -values[0])
};
return inputId switch
{
MotionInputId.Gyroscope => RadToDegree(value),
MotionInputId.Accelerometer => GsToMs2(value),
_ => value
};
}
}
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY;
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
_buttonsUserMapping.Clear();
// First update sticks
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
switch (_joyConType)
{
case JoyConType.Left:
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
break;
case JoyConType.Right:
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
break;
default:
throw new ArgumentOutOfRangeException();
}
SetTriggerThreshold(_configuration.TriggerThreshold);
}
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
return rawState;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid)
continue;
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
}
return result;
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId>
GetLogicalJoyStickConfig(StickInputId inputId)
{
switch (inputId)
{
case StickInputId.Left:
if (_configuration.RightJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Left)
return _configuration.RightJoyconStick;
else
return _configuration.LeftJoyconStick;
case StickInputId.Right:
if (_configuration.LeftJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Right)
return _configuration.LeftJoyconStick;
else
return _configuration.RightJoyconStick;
}
return null;
}
public (float, float) GetStick(StickInputId inputId)
{
if (inputId == StickInputId.Unbound)
return (0.0f, 0.0f);
if (inputId == StickInputId.Left && _joyConType == JoyConType.Right || inputId == StickInputId.Right && _joyConType == JoyConType.Left)
{
return (0.0f, 0.0f);
}
(short stickX, short stickY) = GetStickXY();
float resultX = ConvertRawStickValue(stickX);
float resultY = -ConvertRawStickValue(stickY);
if (HasConfiguration)
{
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
if (joyconStickConfig != null)
{
if (joyconStickConfig.InvertStickX)
resultX = -resultX;
if (joyconStickConfig.InvertStickY)
resultY = -resultY;
if (joyconStickConfig.Rotate90CW)
{
float temp = resultX;
resultX = resultY;
resultY = -temp;
}
}
}
return inputId switch
{
StickInputId.Left when _joyConType == JoyConType.Left => (resultY, -resultX),
StickInputId.Right when _joyConType == JoyConType.Right => (-resultY, resultX),
_ => (0.0f, 0.0f)
};
}
private (short, short) GetStickXY()
{
return (
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX),
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY));
}
public bool IsPressed(GamepadButtonInputId inputId)
{
if (!_buttonsDriverMapping.TryGetValue(inputId, out var button))
{
return false;
}
return SDL_GameControllerGetButton(_gamepadHandle, button) == 1;
}
}
}

View File

@@ -0,0 +1,142 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
internal class SDL2JoyConPair(IGamepad left, IGamepad right) : IGamepad
{
private StandardControllerInputConfig _configuration;
private readonly StickInputId[] _stickUserMapping =
[
StickInputId.Unbound,
StickInputId.Left,
StickInputId.Right
];
public GamepadFeaturesFlag Features => (left?.Features ?? GamepadFeaturesFlag.None) |
(right?.Features ?? GamepadFeaturesFlag.None);
public const string Id = "JoyConPair";
string IGamepad.Id => Id;
public string Name => "* Nintendo Switch Joy-Con (L/R)";
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
public void Dispose()
{
left?.Dispose();
right?.Dispose();
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
return GetStateSnapshot();
}
public Vector3 GetMotionData(MotionInputId inputId)
{
return inputId switch
{
MotionInputId.Accelerometer or
MotionInputId.Gyroscope => left.GetMotionData(inputId),
MotionInputId.SecondAccelerometer => right.GetMotionData(MotionInputId.Accelerometer),
MotionInputId.SecondGyroscope => right.GetMotionData(MotionInputId.Gyroscope),
_ => Vector3.Zero
};
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public (float, float) GetStick(StickInputId inputId)
{
return inputId switch
{
StickInputId.Left => left.GetStick(StickInputId.Left),
StickInputId.Right => right.GetStick(StickInputId.Right),
_ => (0, 0)
};
}
public bool IsPressed(GamepadButtonInputId inputId)
{
return left.IsPressed(inputId) || right.IsPressed(inputId);
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (lowFrequency != 0)
{
right.Rumble(lowFrequency, lowFrequency, durationMs);
}
if (highFrequency != 0)
{
left.Rumble(highFrequency, highFrequency, durationMs);
}
if (lowFrequency == 0 && highFrequency == 0)
{
left.Rumble(0, 0, durationMs);
right.Rumble(0, 0, durationMs);
}
}
public void SetConfiguration(InputConfig configuration)
{
left.SetConfiguration(configuration);
right.SetConfiguration(configuration);
}
public void SetTriggerThreshold(float triggerThreshold)
{
left.SetTriggerThreshold(triggerThreshold);
right.SetTriggerThreshold(triggerThreshold);
}
public static bool IsCombinable(List<string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
return leftIndex >= 0 && rightIndex >= 0;
}
private static (int leftIndex, int rightIndex) DetectJoyConPair(List<string> gamepadsIds)
{
var gamepadNames = gamepadsIds.Where(gamepadId => gamepadId != Id)
.Select((_, index) => SDL_GameControllerNameForIndex(index)).ToList();
int leftIndex = gamepadNames.IndexOf(SDL2JoyCon.LeftName);
int rightIndex = gamepadNames.IndexOf(SDL2JoyCon.RightName);
return (leftIndex, rightIndex);
}
public static IGamepad GetGamepad(List<string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
if (leftIndex == -1 || rightIndex == -1)
{
return null;
}
nint leftGamepadHandle = SDL_GameControllerOpen(leftIndex);
nint rightGamepadHandle = SDL_GameControllerOpen(rightIndex);
if (leftGamepadHandle == nint.Zero || rightGamepadHandle == nint.Zero)
{
return null;
}
return new SDL2JoyConPair(new SDL2JoyCon(leftGamepadHandle, gamepadsIds[leftIndex]),
new SDL2JoyCon(rightGamepadHandle, gamepadsIds[rightIndex]));
}
}
}

View File

@@ -269,6 +269,7 @@ namespace Ryujinx.Input.HLE
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
{
_leftMotionInput = new MotionInput();
_rightMotionInput = new MotionInput();
}
else
{
@@ -301,7 +302,20 @@ namespace Ryujinx.Input.HLE
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
{
_rightMotionInput = _leftMotionInput;
if (gamepad.Id== "JoyConPair")
{
Vector3 rightAccelerometer = gamepad.GetMotionData(MotionInputId.SecondAccelerometer);
Vector3 rightGyroscope = gamepad.GetMotionData(MotionInputId.SecondGyroscope);
rightAccelerometer = new Vector3(rightAccelerometer.X, -rightAccelerometer.Z, rightAccelerometer.Y);
rightGyroscope = new Vector3(rightGyroscope.X, -rightGyroscope.Z, rightGyroscope.Y);
_rightMotionInput.Update(rightAccelerometer, rightGyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
}
else
{
_rightMotionInput = _leftMotionInput;
}
}
}
}
@@ -336,6 +350,7 @@ namespace Ryujinx.Input.HLE
// Reset states
State = default;
_leftMotionInput = null;
_rightMotionInput = null;
}
}

View File

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

View File

@@ -21,5 +21,17 @@ namespace Ryujinx.Input
/// </summary>
/// <remarks>Values are in degrees</remarks>
Gyroscope,
/// <summary>
/// Second accelerometer.
/// </summary>
/// <remarks>Values are in m/s^2</remarks>
SecondAccelerometer,
/// <summary>
/// Second gyroscope.
/// </summary>
/// <remarks>Values are in degrees</remarks>
SecondGyroscope
}
}

View File

@@ -25,14 +25,17 @@ namespace Ryujinx.SDL2.Common
public static Action<Action> MainThreadDispatcher { get; set; }
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK |
SDL_INIT_AUDIO | SDL_INIT_VIDEO;
private bool _isRunning;
private uint _refereceCount;
private Thread _worker;
private const uint SDL_JOYBATTERYUPDATED = 1543;
public event Action<int, int> OnJoyStickConnected;
public event Action<int> OnJoystickDisconnected;
public event Action<int, SDL_JoystickPowerLevel> OnJoyBatteryUpdated;
private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
@@ -78,12 +81,14 @@ namespace Ryujinx.SDL2.Common
// First ensure that we only enable joystick events (for connected/disconnected).
if (SDL_GameControllerEventState(SDL_IGNORE) != SDL_IGNORE)
{
Logger.Error?.PrintMsg(LogClass.Application, "Couldn't change the state of game controller events.");
Logger.Error?.PrintMsg(LogClass.Application,
"Couldn't change the state of game controller events.");
}
if (SDL_JoystickEventState(SDL_ENABLE) < 0)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Failed to enable joystick event polling: {SDL_GetError()}");
Logger.Error?.PrintMsg(LogClass.Application,
$"Failed to enable joystick event polling: {SDL_GetError()}");
}
// Disable all joysticks information, we don't need them no need to flood the event queue for that.
@@ -143,7 +148,12 @@ namespace Ryujinx.SDL2.Common
OnJoystickDisconnected?.Invoke(evnt.cbutton.which);
}
else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN or SDL_EventType.SDL_MOUSEBUTTONUP)
else if ((uint)evnt.type == SDL_JOYBATTERYUPDATED)
{
OnJoyBatteryUpdated?.Invoke(evnt.cbutton.which, (SDL_JoystickPowerLevel)evnt.user.code);
}
else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN
or SDL_EventType.SDL_MOUSEBUTTONUP)
{
if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
{

View File

@@ -517,7 +517,7 @@ namespace Ryujinx.Ava
Device?.System.ChangeDockedModeState(e.NewValue);
}
public void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
{
Device?.SetVolume(e.NewValue);
@@ -1041,7 +1041,6 @@ namespace Ryujinx.Ava
if (_viewModel.StartGamesInFullscreen)
{
_viewModel.WindowState = WindowState.FullScreen;
_viewModel.Window.TitleBar.ExtendsContentIntoTitleBar = true;
}
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
using System;
namespace Ryujinx.Ava.Common
{
public static class ThemeManager
{
public static event Action ThemeChanged;
public static void OnThemeChanged()
{
ThemeChanged?.Invoke();
}
}
}

View File

@@ -1,5 +1,6 @@
using DiscordRPC;
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
@@ -10,6 +11,7 @@ using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
using Ryujinx.Horizon.Prepo.Types;
using System.Linq;
using System.Text;
namespace Ryujinx.Ava
@@ -56,7 +58,6 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
HorizonStatic.PlayReport += HandlePlayReport;
PlayReports.Initialize();
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)

View File

@@ -148,7 +148,7 @@ namespace Ryujinx.Headless
IgnoreMissingServices = configurationState.System.IgnoreMissingServices;
if (NeedsOverride(nameof(IgnoreControllerApplet)))
IgnoreControllerApplet = configurationState.System.IgnoreControllerApplet;
IgnoreControllerApplet = configurationState.System.IgnoreApplet;
return;
@@ -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.Vulkan, HelpText = "Change Graphics Backend to use.")]
[Option("graphics-backend", Required = false, Default = GraphicsBackend.OpenGl, 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.")]

View File

@@ -513,7 +513,7 @@ namespace Ryujinx.Headless
Exit();
}
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText, (uint Module, uint Description)? errorCode = null)
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
{
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++)

View File

@@ -22,8 +22,6 @@ 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)}"
@@ -114,7 +112,7 @@ namespace Ryujinx.Ava
baseStyle = ConfigurationState.Instance.UI.BaseStyle;
}
ThemeChanged?.Invoke();
ThemeManager.OnThemeChanged();
RequestedThemeVariant = baseStyle switch
{

View File

@@ -41,7 +41,7 @@ namespace Ryujinx.Ava.UI.Applet
bool okPressed = false;
if (ConfigurationState.Instance.System.IgnoreControllerApplet)
if (ConfigurationState.Instance.System.IgnoreApplet)
return false;
Dispatcher.UIThread.InvokeAsync(async () =>
@@ -75,32 +75,31 @@ 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)
{
@@ -111,9 +110,7 @@ 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();
}
@@ -137,9 +134,7 @@ 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)
{
@@ -151,9 +146,7 @@ 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
{
@@ -184,8 +177,7 @@ 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;
@@ -209,13 +201,11 @@ 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();
}
@@ -227,8 +217,7 @@ namespace Ryujinx.Ava.UI.Applet
_parent.ViewModel.AppHost?.Stop();
}
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons,
(uint Module, uint Description)? errorCode = null)
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
{
ManualResetEvent dialogCloseEvent = new(false);
@@ -240,7 +229,9 @@ 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();
@@ -258,9 +249,7 @@ 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));
}
});
@@ -270,36 +259,38 @@ 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<BaseModel> 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));
ProfileSelectorDialogViewModel viewModel = new()
UserSelectorDialogViewModel viewModel = new()
{
Profiles = profiles, SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
Profiles = profiles,
SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
};
(selected, _) = await ProfileSelectorDialog.ShowInputDialog(viewModel);
UserSelectorDialog content = new(viewModel);
(selected, _) = await UserSelectorDialog.ShowInputDialog(content);
dialogCloseEvent.Set();
});
dialogCloseEvent.WaitOne();
UserProfile profile = _parent.AccountManager.LastOpenedUser;
if (selected == guest.UserId)
{
@@ -320,7 +311,6 @@ namespace Ryujinx.Ava.UI.Applet
}
}
}
return profile;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,19 +93,8 @@
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0"
Padding="0">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding LocalizedStatusTooltip}" />
<Separator
Margin="0, 10, 0, 10"
IsVisible="{Binding HasCompatibilityLabels}" />
<TextBlock
IsVisible="{Binding HasCompatibilityLabels}"
Text="{Binding FormattedCompatibilityLabels}" />
</StackPanel>
</ToolTip.Tip>
Padding="0"
ToolTip.Tip="{Binding LocalizedStatusTooltip}">
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"

View File

@@ -7,6 +7,7 @@ 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

View File

@@ -1,7 +1,6 @@
using Avalonia.Logging;
using Avalonia.Utilities;
using Gommon;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Text;
@@ -15,19 +14,13 @@ 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,

View File

@@ -2,7 +2,6 @@ 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;
@@ -25,35 +24,30 @@ namespace Ryujinx.Ava.UI.ViewModels
Version = RyujinxApp.FullAppName + "\n" + Program.Version;
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value);
RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
}
private void Ryujinx_ThemeChanged()
private void ThemeManager_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";
GithubLogo = LoadBitmap(LogoPathFormat.Format("GitHub", themeName));
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
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");
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
public void Dispose()
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
GithubLogo.Dispose();
DiscordLogo.Dispose();
ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
GC.SuppressFinalize(this);
}
}

View File

@@ -264,7 +264,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}");
// Neither local or remote files are valid JSON, close window.
await ShowInfoDialog();
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.
await ShowInfoDialog();
ShowInfoDialog();
}
}
@@ -525,7 +525,7 @@ namespace Ryujinx.Ava.UI.ViewModels
AmiiboImage = bitmap;
}
private static async Task ShowInfoDialog()
private static async void ShowInfoDialog()
{
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage],

View File

@@ -7,7 +7,6 @@ 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;
@@ -105,13 +104,6 @@ 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;
@@ -1155,10 +1147,10 @@ namespace Ryujinx.Ava.UI.ViewModels
List<string> dirs = result.Select(it => it.Path.LocalPath).ToList();
int numAdded = onDirsSelected(dirs, out int numRemoved);
string msg = string.Join("\n",
string msg = String.Join("\r\n", new string[] {
string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved),
string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded)
);
});
await Dispatcher.UIThread.InvokeAsync(async () =>
{

View File

@@ -13,7 +13,6 @@ 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;
@@ -49,7 +48,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private int _graphicsBackendMultithreadingIndex;
private float _volume;
[ObservableProperty] private bool _isVulkanAvailable = true;
[ObservableProperty] private bool _gameListNeedsRefresh;
[ObservableProperty] private bool _gameDirectoryChanged;
[ObservableProperty] private bool _autoloadDirectoryChanged;
private readonly List<string> _gpuIds = [];
private int _graphicsBackendIndex;
private int _scalingFilter;
@@ -121,14 +121,9 @@ 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;
@@ -209,7 +204,6 @@ 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; }
@@ -481,8 +475,6 @@ 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);
@@ -502,7 +494,6 @@ 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);
@@ -526,7 +517,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
DramSize = config.System.DramSize;
IgnoreMissingServices = config.System.IgnoreMissingServices;
IgnoreApplet = config.System.IgnoreControllerApplet;
IgnoreApplet = config.System.IgnoreApplet;
// CPU
EnablePptc = config.System.EnablePtc;
@@ -569,7 +560,6 @@ 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;
@@ -590,10 +580,16 @@ 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;
config.UI.GameDirs.Value = [..GameDirectories];
config.UI.AutoloadDirs.Value = [..AutoloadDirectories];
if (GameDirectoryChanged)
{
config.UI.GameDirs.Value = [..GameDirectories];
}
if (AutoloadDirectoryChanged)
{
config.UI.AutoloadDirs.Value = [..AutoloadDirectories];
}
config.UI.BaseStyle.Value = BaseStyleIndex switch
{
@@ -607,18 +603,14 @@ 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();
// System
config.System.Region.Value = (Region)Region;
if (config.System.Language.Value != (Language)Language)
GameListNeedsRefresh = true;
config.System.Language.Value = (Language)Language;
if (_validTzRegions.Contains(TimeZone))
{
config.System.TimeZone.Value = TimeZone;
@@ -629,7 +621,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
config.System.DramSize.Value = DramSize;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
config.System.IgnoreControllerApplet.Value = IgnoreApplet;
config.System.IgnoreApplet.Value = IgnoreApplet;
// CPU
config.System.EnablePtc.Value = EnablePptc;
@@ -687,7 +679,6 @@ 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;
@@ -709,7 +700,8 @@ namespace Ryujinx.Ava.UI.ViewModels
SaveSettingsEvent?.Invoke();
GameListNeedsRefresh = false;
GameDirectoryChanged = false;
AutoloadDirectoryChanged = false;
}
private static void RevertIfNotSaved()

View File

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

View File

@@ -51,8 +51,12 @@ 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 = MainWindowViewModel.UpdateCommand;
UpdateMenuItem.Command = Commands.Create(async () =>
{
if (Updater.CanUpdate(true))
await Updater.BeginUpdateAsync(true);
});
FaqMenuItem.Command =
SetupGuideMenuItem.Command =

View File

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

View File

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

View File

@@ -316,8 +316,8 @@
</CheckBox>
<CheckBox
IsChecked="{Binding IgnoreApplet}"
ToolTip.Tip="{ext:Locale IgnoreControllerAppletTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemIgnoreControllerApplet}" />
ToolTip.Tip="{ext:Locale IgnoreAppletTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemIgnoreApplet}" />
</CheckBox>
<CheckBox
IsChecked="{Binding EnableCustomVSyncInterval}"

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsUiView()
{
InitializeComponent();
ShowTitleBarBox.IsVisible = OperatingSystem.IsWindows();
AddGameDirButton.Command =
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true));
AddAutoloadDirButton.Command =
@@ -36,8 +37,11 @@ namespace Ryujinx.Ava.UI.Views.Settings
directories.Add(path);
addDirBox.Clear();
ViewModel.GameListNeedsRefresh = true;
if (isGameList)
ViewModel.GameDirectoryChanged = true;
else
ViewModel.AutoloadDirectoryChanged = true;
}
else
{
@@ -47,7 +51,10 @@ namespace Ryujinx.Ava.UI.Views.Settings
{
directories.Add(folder.Value.Path.LocalPath);
ViewModel.GameListNeedsRefresh = true;
if (isGameList)
ViewModel.GameDirectoryChanged = true;
else
ViewModel.AutoloadDirectoryChanged = true;
}
}
}
@@ -59,7 +66,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
foreach (string path in new List<string>(GameDirsList.SelectedItems.Cast<string>()))
{
ViewModel.GameDirectories.Remove(path);
ViewModel.GameListNeedsRefresh = true;
ViewModel.GameDirectoryChanged = true;
}
if (GameDirsList.ItemCount > 0)
@@ -75,7 +82,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
foreach (string path in new List<string>(AutoloadDirsList.SelectedItems.Cast<string>()))
{
ViewModel.AutoloadDirectories.Remove(path);
ViewModel.GameListNeedsRefresh = true;
ViewModel.AutoloadDirectoryChanged = true;
}
if (AutoloadDirsList.ItemCount > 0)

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading;
@@ -20,7 +19,6 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
@@ -402,21 +400,10 @@ namespace Ryujinx.Ava.UI.Windows
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
}
if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates)
return;
switch (ConfigurationState.Instance.UpdateCheckerType.Value)
if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate())
{
case UpdaterType.PromptAtStartup:
await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
break;
case UpdaterType.CheckInBackground:
if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions))
{
Dispatcher.UIThread.Post(() => RyujinxApp.MainWindow.ViewModel.UpdateAvailable = versions.Current < versions.Incoming);
}
break;
await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
}
}
@@ -762,119 +749,5 @@ namespace Ryujinx.Ava.UI.Windows
_intelMacWarningShown = true;
}
private void InputElement_OnGotFocus(object sender, GotFocusEventArgs e)
{
if (ViewModel.AppHost is null) return;
if (!_focusLoss.Active)
return;
switch (_focusLoss.Type)
{
case FocusLostType.BlockInput:
{
if (!ViewModel.AppHost.NpadManager.InputUpdatesBlocked)
{
_focusLoss = default;
return;
}
ViewModel.AppHost.NpadManager.UnblockInputUpdates();
_focusLoss = default;
break;
}
case FocusLostType.MuteAudio:
{
if (!ViewModel.AppHost.Device.IsAudioMuted())
{
_focusLoss = default;
return;
}
ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute);
_focusLoss = default;
break;
}
case FocusLostType.BlockInputAndMuteAudio:
{
if (!ViewModel.AppHost.Device.IsAudioMuted())
goto case FocusLostType.BlockInput;
ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute);
ViewModel.AppHost.NpadManager.UnblockInputUpdates();
_focusLoss = default;
break;
}
case FocusLostType.PauseEmulation:
{
if (!ViewModel.AppHost.Device.System.IsPaused)
{
_focusLoss = default;
return;
}
ViewModel.AppHost.Resume();
_focusLoss = default;
break;
}
}
}
private (FocusLostType Type, bool Active) _focusLoss;
private void InputElement_OnLostFocus(object sender, RoutedEventArgs e)
{
if (ConfigurationState.Instance.FocusLostActionType.Value is FocusLostType.DoNothing)
return;
if (ViewModel.AppHost is null) return;
switch (ConfigurationState.Instance.FocusLostActionType.Value)
{
case FocusLostType.BlockInput:
{
if (ViewModel.AppHost.NpadManager.InputUpdatesBlocked)
return;
ViewModel.AppHost.NpadManager.BlockInputUpdates();
_focusLoss = (FocusLostType.BlockInput, ViewModel.AppHost.NpadManager.InputUpdatesBlocked);
break;
}
case FocusLostType.MuteAudio:
{
if (ViewModel.AppHost.Device.GetVolume() is 0)
return;
ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume();
ViewModel.AppHost.Device.SetVolume(0);
_focusLoss = (FocusLostType.MuteAudio, ViewModel.AppHost.Device.GetVolume() is 0f);
break;
}
case FocusLostType.BlockInputAndMuteAudio:
{
if (ViewModel.AppHost.Device.GetVolume() is 0)
goto case FocusLostType.BlockInput;
ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume();
ViewModel.AppHost.Device.SetVolume(0);
ViewModel.AppHost.NpadManager.BlockInputUpdates();
_focusLoss = (FocusLostType.BlockInputAndMuteAudio, ViewModel.AppHost.Device.GetVolume() is 0f && ViewModel.AppHost.NpadManager.InputUpdatesBlocked);
break;
}
case FocusLostType.PauseEmulation:
{
if (ViewModel.AppHost.Device.System.IsPaused)
return;
ViewModel.AppHost.Pause();
_focusLoss = (FocusLostType.PauseEmulation, ViewModel.AppHost.Device.System.IsPaused);
break;
}
}
}
}
}

View File

@@ -45,7 +45,7 @@ namespace Ryujinx.Ava.UI.Windows
{
InputPage.InputView?.SaveCurrentProfile();
if (Owner is MainWindow window && ViewModel.GameListNeedsRefresh)
if (Owner is MainWindow window && (ViewModel.GameDirectoryChanged || ViewModel.AutoloadDirectoryChanged))
{
window.LoadApplications();
}

View File

@@ -43,18 +43,7 @@ namespace Ryujinx.Ava
private const int ConnectionCount = 4;
private static string _buildVer;
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 _platformExt;
private static string _buildUrl;
private static long _buildSize;
private static bool _updateSuccessful;
@@ -62,8 +51,30 @@ namespace Ryujinx.Ava
private static readonly string[] _windowsDependencyDirs = [];
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
public static async Task BeginUpdateAsync(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!");
@@ -74,7 +85,7 @@ namespace Ryujinx.Ava
_running = false;
return default;
return;
}
Logger.Info?.Print(LogClass.Application, "Checking for updates.");
@@ -112,7 +123,7 @@ namespace Ryujinx.Ava
_running = false;
return default;
return;
}
break;
@@ -138,7 +149,7 @@ namespace Ryujinx.Ava
_running = false;
return default;
return;
}
}
catch (Exception exception)
@@ -150,7 +161,7 @@ namespace Ryujinx.Ava
_running = false;
return default;
return;
}
if (!Version.TryParse(_buildVer, out Version newVersion))
@@ -163,27 +174,9 @@ 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)

View File

@@ -1,4 +1,3 @@
using Gommon;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
@@ -15,7 +14,6 @@ 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
@@ -34,12 +32,33 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
set
{
_id = value;
Compatibility = CompatibilityCsv.Find(Id);
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
}
}
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
public bool HasPlayabilityInfo => PlayabilityStatus != null;
public string LocalizedStatus =>
PlayabilityStatus.HasValue
? LocaleManager.Instance[PlayabilityStatus!.Value]
: string.Empty;
public LocaleKeys? PlayabilityStatus { get; set; }
public string LocalizedStatusTooltip =>
PlayabilityStatus.HasValue
#pragma warning disable CS8509 // It is exhaustive for any value this property can contain.
? LocaleManager.Instance[PlayabilityStatus!.Value switch
#pragma warning restore CS8509
{
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
}]
: string.Empty;
public int PlayerCount { get; set; }
public int GameCount { get; set; }
@@ -59,39 +78,11 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public bool HasPlayedPreviously => TimePlayed.TotalSeconds > 1;
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
public Optional<CompatibilityEntry> Compatibility { get; private set; }
public bool HasPlayabilityInfo => Compatibility.HasValue;
public string LocalizedStatus => Compatibility.Convert(x => x.LocalizedStatus);
public bool HasCompatibilityLabels => !FormattedCompatibilityLabels.Equals(string.Empty);
public string FormattedCompatibilityLabels
=> Compatibility.Convert(x => x.FormattedIssueLabels).OrElse(string.Empty);
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
public string LocalizedStatusTooltip =>
Compatibility.Convert(x =>
#pragma warning disable CS8509 // It is exhaustive for all possible values this can contain.
LocaleManager.Instance[x.Status switch
#pragma warning restore CS8509
{
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
}]
).OrElse(string.Empty);
[JsonIgnore] public string IdString => Id.ToString("x16");
@@ -101,16 +92,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();

View File

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

View File

@@ -60,21 +60,10 @@ 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)
=> Find(titleId)?.Status;
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(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

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 67;
public const int CurrentVersion = 63;
/// <summary>
/// Version of the configuration file format
@@ -111,11 +111,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages
/// </summary>
public bool LoggingEnableFsAccessLog { get; set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public bool LoggingEnableAvalonia { get; set; }
/// <summary>
/// Controls which log messages are written to the log targets
@@ -163,19 +158,9 @@ namespace Ryujinx.Ava.Utilities.Configuration
public bool EnableDiscordIntegration { get; set; }
/// <summary>
/// DEPRECATED: Checks for updates when Ryujinx starts when enabled
/// Checks for updates when Ryujinx starts when enabled
/// </summary>
public bool CheckUpdatesOnStart { get; set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public UpdaterType UpdateCheckerType { get; set; }
/// <summary>
/// How the emulator should behave when you click off/on the window.
/// </summary>
public FocusLostType FocusLostActionType { get; set; }
/// <summary>
/// Show "Confirm Exit" Dialog
@@ -183,7 +168,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
public bool ShowConfirmExit { get; set; }
/// <summary>
/// Ignore Controller Applet dialog
/// ignore "Applet" dialog
/// </summary>
public bool IgnoreApplet { get; set; }
@@ -388,11 +373,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enable or disable mouse support (Independent from controllers binding)
/// </summary>
public bool EnableMouse { get; set; }
/// <summary>
/// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
/// </summary>
public bool DisableInputWhenOutOfFocus { get; set; }
/// <summary>
/// Hotkey Keyboard Bindings

View File

@@ -45,8 +45,6 @@ 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;
@@ -88,7 +86,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.Region.Value = cff.SystemRegion;
System.TimeZone.Value = cff.SystemTimeZone;
System.SystemTimeOffset.Value = cff.SystemTimeOffset;
System.MatchSystemTime.Value = cff.MatchSystemTime;
System.EnableDockedMode.Value = cff.DockedMode;
System.EnablePtc.Value = cff.EnablePtc;
System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc;
@@ -100,7 +97,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.MemoryManagerMode.Value = cff.MemoryManagerMode;
System.DramSize.Value = cff.DramSize;
System.IgnoreMissingServices.Value = cff.IgnoreMissingServices;
System.IgnoreControllerApplet.Value = cff.IgnoreApplet;
System.IgnoreApplet.Value = cff.IgnoreApplet;
System.UseHypervisor.Value = cff.UseHypervisor;
UI.GuiColumns.FavColumn.Value = cff.GuiColumns.FavColumn;
@@ -141,7 +138,6 @@ 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;
@@ -434,11 +430,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
}
}),
(62, static cff => cff.RainbowSpeed = 1f),
(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)
(63, static cff => cff.MatchSystemTime = false)
);
}
}

View File

@@ -1,7 +1,6 @@
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;
@@ -255,11 +254,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages
/// </summary>
public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public ReactiveObject<bool> EnableAvaloniaLog { get; private set; }
/// <summary>
/// Controls which log messages are written to the log targets
@@ -287,7 +281,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableTrace = new ReactiveObject<bool>();
EnableGuest = new ReactiveObject<bool>();
EnableFsAccessLog = new ReactiveObject<bool>();
EnableAvaloniaLog = new ReactiveObject<bool>();
FilteredClasses = new ReactiveObject<LogClass[]>();
EnableFileLog = new ReactiveObject<bool>();
EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
@@ -383,7 +376,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// Ignore Controller Applet
/// </summary>
public ReactiveObject<bool> IgnoreControllerApplet { get; private set; }
public ReactiveObject<bool> IgnoreApplet { get; private set; }
/// <summary>
/// Uses Hypervisor over JIT if available
@@ -424,8 +417,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
DramSize.LogChangesToValue(nameof(DramSize));
IgnoreMissingServices = new ReactiveObject<bool>();
IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices));
IgnoreControllerApplet = new ReactiveObject<bool>();
IgnoreControllerApplet.LogChangesToValue(nameof(IgnoreControllerApplet));
IgnoreApplet = new ReactiveObject<bool>();
IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet));
AudioVolume = new ReactiveObject<float>();
AudioVolume.LogChangesToValue(nameof(AudioVolume));
UseHypervisor = new ReactiveObject<bool>();
@@ -447,11 +440,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enable or disable mouse support (Independent from controllers binding)
/// </summary>
public ReactiveObject<bool> EnableMouse { get; private set; }
/// <summary>
/// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
/// </summary>
public ReactiveObject<bool> DisableInputWhenOutOfFocus { get; private set; }
/// <summary>
/// Hotkey Keyboard Bindings
@@ -474,7 +462,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
{
EnableKeyboard = new ReactiveObject<bool>();
EnableMouse = new ReactiveObject<bool>();
DisableInputWhenOutOfFocus = new ReactiveObject<bool>();
Hotkeys = new ReactiveObject<KeyboardHotkeys>();
InputConfig = new ReactiveObject<List<InputConfig>>();
RainbowSpeed = new ReactiveObject<float>();
@@ -774,16 +761,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Checks for updates when Ryujinx starts when enabled
/// </summary>
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
/// <summary>
/// How the emulator should behave when you click off/on the window.
/// </summary>
public ReactiveObject<FocusLostType> FocusLostActionType { get; private set; }
/// <summary>
/// Show "Confirm Exit" Dialog
@@ -821,8 +798,6 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hacks = new HacksSection();
EnableDiscordIntegration = new ReactiveObject<bool>();
CheckUpdatesOnStart = new ReactiveObject<bool>();
UpdateCheckerType = new ReactiveObject<UpdaterType>();
FocusLostActionType = new ReactiveObject<FocusLostType>();
ShowConfirmExit = new ReactiveObject<bool>();
RememberWindowState = new ReactiveObject<bool>();
ShowTitleBar = new ReactiveObject<bool>();

View File

@@ -46,19 +46,15 @@ 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,
SystemRegion = System.Region,
SystemTimeZone = System.TimeZone,
SystemTimeOffset = System.SystemTimeOffset,
MatchSystemTime = System.MatchSystemTime,
DockedMode = System.EnableDockedMode,
EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart,
UpdateCheckerType = UpdateCheckerType,
FocusLostActionType = FocusLostActionType,
ShowConfirmExit = ShowConfirmExit,
RememberWindowState = RememberWindowState,
ShowTitleBar = ShowTitleBar,
@@ -81,7 +77,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
MemoryManagerMode = System.MemoryManagerMode,
DramSize = System.DramSize,
IgnoreMissingServices = System.IgnoreMissingServices,
IgnoreApplet = System.IgnoreControllerApplet,
IgnoreApplet = System.IgnoreApplet,
UseHypervisor = System.UseHypervisor,
GuiColumns = new GuiColumns
{
@@ -133,7 +129,6 @@ 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,
@@ -170,7 +165,6 @@ 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;
@@ -179,8 +173,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.SystemTimeOffset.Value = 0;
System.EnableDockedMode.Value = true;
EnableDiscordIntegration.Value = true;
UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
FocusLostActionType.Value = FocusLostType.DoNothing;
CheckUpdatesOnStart.Value = true;
ShowConfirmExit.Value = true;
RememberWindowState.Value = true;
ShowTitleBar.Value = !OperatingSystem.IsWindows();
@@ -205,7 +198,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.MemoryManagerMode.Value = MemoryManagerMode.HostMappedUnsafe;
System.DramSize.Value = MemoryConfiguration.MemoryConfiguration4GiB;
System.IgnoreMissingServices.Value = false;
System.IgnoreControllerApplet.Value = false;
System.IgnoreApplet.Value = false;
System.UseHypervisor.Value = true;
Multiplayer.LanInterfaceId.Value = "0";
Multiplayer.Mode.Value = MultiplayerMode.Disabled;
@@ -249,7 +242,6 @@ 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,

View File

@@ -1,15 +0,0 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.Configuration.UI
{
[JsonConverter(typeof(TypedStringEnumConverter<FocusLostType>))]
public enum FocusLostType
{
DoNothing,
BlockInput,
MuteAudio,
BlockInputAndMuteAudio,
PauseEmulation
}
}

View File

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

View File

@@ -1,6 +1,6 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -28,12 +28,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(string titleId, Func<GameSpec, GameSpec> transform)
{
if (ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _))
return AddSpec(transform(GameSpec.Create(titleId)));
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
Logger.Notice.PrintMsg(LogClass.Application,
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{titleId}'");
return this;
return AddSpec(transform(GameSpec.Create(titleId)));
}
/// <summary>
@@ -44,12 +42,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(string titleId, Action<GameSpec> transform)
{
if (ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _))
return AddSpec(GameSpec.Create(titleId).Apply(transform));
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
Logger.Notice.PrintMsg(LogClass.Application,
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{titleId}'");
return this;
return AddSpec(GameSpec.Create(titleId).Apply(transform));
}
/// <summary>
@@ -62,19 +58,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Func<GameSpec, GameSpec> transform)
{
string[] tids = titleIds.ToArray();
if (tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _) && !string.IsNullOrEmpty(x)))
return AddSpec(transform(GameSpec.Create(tids)));
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)}.");
Logger.Notice.PrintMsg(LogClass.Application,
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{
tids.FormatCollection(
x => x,
separator: ", ",
prefix: "[",
suffix: "]"
)
}'");
return this;
return AddSpec(transform(GameSpec.Create(tids)));
}
/// <summary>
@@ -86,21 +73,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> transform)
{
string[] tids = titleIds.ToArray();
if (tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _) && !string.IsNullOrEmpty(x)))
return AddSpec(GameSpec.Create(tids).Apply(transform));
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)}.");
Logger.Notice.PrintMsg(LogClass.Application,
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{
tids.FormatCollection(
x => x,
separator: ", ",
prefix: "[",
suffix: "]"
)
}'");
return this;
return AddSpec(GameSpec.Create(tids).Apply(transform));
}
/// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided pre-configured spec.
/// </summary>

View File

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

View File

@@ -1,6 +1,4 @@
using Gommon;
using Humanizer;
using System;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
@@ -19,9 +17,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static FormattedValue SkywardSwordHD_Rupees(SingleValue value)
=> "rupee".ToQuantity(value.Matched.IntValue);
private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
@@ -286,14 +281,9 @@ namespace Ryujinx.Ava.Utilities.PlayReport
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(", ");
? $"{players.Count} Players - " + string.Join(", ",
players.Take(3).Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}"))
: string.Join(", ", players.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}"));
string RankMedal(int? rank) => rank switch
{

View File

@@ -1,81 +1,72 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public static partial class PlayReports
{
public static void Initialize()
{
// init lazy value
_ = Analyzer;
}
public static Analyzer Analyzer => _analyzerLazy.Value;
private static readonly Lazy<Analyzer> _analyzerLazy = new(() =>
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)
)
.AddSpec("01002da013484000", spec => spec.AddValueFormatter("rupees", SkywardSwordHD_Rupees))
);
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}";
}

View File

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