Compare commits

...

44 Commits

Author SHA1 Message Date
Vladimir Sokolov eb9d6ea3ab Merge 8efceb3c28 into 6ab899f621 2025-02-11 08:57:00 +10:00
Vladimir Sokolov 8efceb3c28 Merge branch 'master' into master 2025-02-11 08:56:57 +10:00
Evan Husted 6ab899f621 misc: chore: [ci skip] Use explicit types & target-typed new 2025-02-10 16:44:26 -06:00
FluffyOMC faacec9801 JIT Cache Regions + HLE SoNoSigpipe BSD socket mapping (#615)
Instead of one big 2048MB JIT Cache that'd crash the emulator when maxed
out, we now have it where we add 256MB JIT Cache regions when needed,
helping reduce allocated memory where games don't use the JIT cache for
it, and helping bigger games that DO need JIT cache bigger than 2048MB!

![image](https://github.com/user-attachments/assets/ff17dc48-6028-4377-8c73-746ab21ab83b)
(SSBU goes past the 2048MB JIT Cache limit that would normally crash
Ryujinx ^)

Also I added a BSD socket that Baba is You's networking for downloading
custom levels uses.
2025-02-10 15:53:44 -06:00
Evan Husted 55fdb3f6b2 headless: Default to Vulkan 2025-02-10 15:45:09 -06:00
Evan Husted 1129ab0e8c misc: chore: Remove unused property in ApplicationData 2025-02-10 15:44:58 -06:00
Evan Husted b6b391b2cf misc: chore: [ci skip] Remove unused 'using' directives from solution 2025-02-10 14:34:12 -06:00
Evan Husted f3cf03495d misc: add the ability to ignore UI logs when using trace & debug log levels 2025-02-10 14:25:47 -06:00
Evan Husted 7bce8206d5 misc: chore: small cleanups 2025-02-10 13:59:25 -06:00
Vladimir Sokolov 3e40532dee Merge branch 'master' into master 2025-02-10 15:36:15 +10:00
Vova 1d68546aa0 Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-10 15:26:21 +10:00
Vova 966860a464 Change: initialization of user configuration is now a separate function 2025-02-10 15:25:19 +10:00
Evan Husted efa0cc7554 UI: Show issue labels of games in the tooltip on playability status 2025-02-09 21:15:38 -06:00
Vladimir Sokolov b9982c02e3 Merge branch 'master' into master 2025-02-09 16:51:11 +10:00
Evan Husted 1c0813d09d misc: chore: [ci skip] shorten lines in FormatterSpecBase.Format & consistently format them 2025-02-09 00:50:11 -06:00
Vladimir Sokolov a301a14074 Merge branch 'master' into master 2025-02-09 16:50:08 +10:00
Evan Husted 8bec09d7ff Merge remote-tracking branch 'origin/master' 2025-02-09 00:45:54 -06:00
Evan Husted e4b4e94b56 misc: chore: cleanup Specs.cs 2025-02-09 00:45:46 -06:00
Evan Husted 764c9e9d4e misc: chore: unify GameSpec creation 2025-02-09 00:37:54 -06:00
Evan Husted 05e991db87 misc: chore: collapse all NSO emulator formatters into one method 2025-02-09 00:37:35 -06:00
Vova 7568dd4d7a Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-09 15:30:57 +10:00
Vova 4bbcec2076 Fixed a bug where control would not return after changing settings.
Added a function to update the gamelist status after the game is released
2025-02-09 15:30:33 +10:00
Vladimir Sokolov 57ac90a55b Merge branch 'master' into master 2025-02-09 15:20:03 +10:00
Vladimir Sokolov cdc4557d82 Merge branch 'master' into master 2025-02-09 09:15:26 +10:00
Vova ae16360685 Code cleaning 2025-02-08 22:27:38 +10:00
Vova 45b1794a45 Returned an erroneously modified Convert time string 2025-02-08 20:41:16 +10:00
Vova 39fbbb39e0 Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-08 20:19:23 +10:00
Vova 11a68a204f Fixed bug crash due to incorrect System.SystemTimeOffset.Value,
hotkeys should now also be read from the global configuration file
2025-02-08 20:16:38 +10:00
Vladimir Sokolov e4e38c4bdd Merge branch 'master' into master 2025-02-08 18:30:35 +10:00
Vova 5f5c76107c Fixed a ban where a custom setting was mistakenly created when starting the game if it did not exist.
Now when starting the game, if a custom setting was created, the current game will be displayed in the settings window.
Code cleanup.
2025-02-08 18:29:31 +10:00
Vova a92475b8fd Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-08 10:51:57 +10:00
Vova 4d5ae23b0b Added game name to user settings window 2025-02-08 10:51:39 +10:00
Vladimir Sokolov 4cb2170ad1 Merge branch 'master' into master 2025-02-08 10:49:55 +10:00
Vladimir Sokolov fc01f7b58f Merge branch 'master' into master 2025-02-08 10:17:13 +10:00
Vova fa463c51f8 code cleaning 2025-02-07 23:37:35 +10:00
Vova 3a8e6e3117 Added gamepad configuration for custom configuration 2025-02-07 21:48:51 +10:00
Vladimir Sokolov 7f4a161ca5 Merge branch 'master' into master 2025-02-07 20:48:10 +10:00
Vova df1c7613e8 returned the deleted field 2025-02-06 23:24:52 +10:00
Vova 6d37d79cc7 Ops 2025-02-06 22:58:43 +10:00
Vova afdfcc1720 fix 2025-02-06 22:54:45 +10:00
Vova fe94224d01 keys returned to their place, minor fixes, local ones added 2025-02-06 22:51:08 +10:00
Vova fbe1a7d966 Added custom setting function for each game 2025-02-06 04:54:00 -06:00
Vova 5dc7fb4495 Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-06 20:34:31 +10:00
Vova 2640f083b6 Added custom setting function for each game 2025-02-06 20:34:15 +10:00
46 changed files with 1377 additions and 439 deletions
+2
View File
@@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
public const int DefaultGranularity = 65536; // Mapping granularity in Windows. public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
public IJitMemoryBlock Block { get; } public IJitMemoryBlock Block { get; }
public IJitMemoryAllocator Allocator { get; }
public nint Pointer => Block.Pointer; public nint Pointer => Block.Pointer;
@@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity; granularity = DefaultGranularity;
} }
Allocator = allocator;
Block = allocator.Reserve(maxSize); Block = allocator.Reserve(maxSize);
_maxSize = maxSize; _maxSize = maxSize;
_sizeGranularity = granularity; _sizeGranularity = granularity;
+77 -35
View File
@@ -2,6 +2,8 @@ using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Unwinding; using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Native; using ARMeilleure.Native;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -18,9 +20,8 @@ namespace ARMeilleure.Translation.Cache
private static readonly int _pageMask = _pageSize - 1; private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024; private const int CacheSize = 256 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator; private static JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator; private static CacheMemoryAllocator _cacheAllocator;
@@ -30,6 +31,9 @@ namespace ARMeilleure.Translation.Cache
private static readonly Lock _lock = new(); private static readonly Lock _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize); public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
@@ -48,7 +52,9 @@ namespace ARMeilleure.Translation.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, CacheSize); ReservedRegion firstRegion = new(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{ {
@@ -59,7 +65,9 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize)); JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
} }
_initialized = true; _initialized = true;
@@ -75,8 +83,8 @@ namespace ARMeilleure.Translation.Cache
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length); int funcOffset = Allocate(code.Length);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = _jitRegion.Pointer + funcOffset; nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@@ -90,9 +98,9 @@ namespace ARMeilleure.Translation.Cache
} }
else else
{ {
ReprotectAsWritable(funcOffset, code.Length); ReprotectAsWritable(targetRegion, funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@@ -116,52 +124,83 @@ namespace ARMeilleure.Translation.Cache
{ {
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); foreach (ReservedRegion region in _jitRegions)
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{ {
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); if (pointer.ToInt64() < region.Pointer.ToInt64() ||
_cacheEntries.RemoveAt(entryIndex); pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
} }
} }
} }
private static void ReprotectAsWritable(int offset, int size) private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static void ReprotectAsExecutable(int offset, int size) private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static int Allocate(int codeSize) private static int Allocate(int codeSize)
{ {
codeSize = AlignCodeSize(codeSize); codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize); for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
if (allocOffset < 0)
{ {
throw new OutOfMemoryException("JIT Cache exhausted."); int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
} }
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex;
return allocOffset; Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
} }
private static int AlignCodeSize(int codeSize) private static int AlignCodeSize(int codeSize)
{ {
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1); return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
@@ -185,18 +224,21 @@ namespace ARMeilleure.Translation.Cache
{ {
lock (_lock) lock (_lock)
{ {
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default)); foreach (ReservedRegion _ in _jitRegions)
if (index < 0)
{ {
index = ~index - 1; int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
}
if (index >= 0) if (index < 0)
{ {
entry = _cacheEntries[index]; index = ~index - 1;
entryIndex = index; }
return true;
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
} }
} }
@@ -144,17 +144,15 @@ namespace ARMeilleure.Translation.PTC
public List<ulong> GetBlacklistedFunctions() public List<ulong> GetBlacklistedFunctions()
{ {
List<ulong> funcs = new List<ulong>(); List<ulong> funcs = [];
foreach (var profiledFunc in ProfiledFuncs) foreach ((ulong ptr, FuncProfile funcProfile) in ProfiledFuncs)
{ {
if (profiledFunc.Value.Blacklist) if (!funcProfile.Blacklist)
{ continue;
if (!funcs.Contains(profiledFunc.Key))
{ if (!funcs.Contains(ptr))
funcs.Add(profiledFunc.Key); funcs.Add(ptr);
}
}
} }
return funcs; return funcs;
+61 -33
View File
@@ -1,4 +1,6 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -15,9 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly int _pageMask = _pageSize - 1; private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024; private const int CacheSize = 256 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator; private static JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator; private static CacheMemoryAllocator _cacheAllocator;
@@ -26,6 +27,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly Lock _lock = new(); private static readonly Lock _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
@@ -45,7 +48,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, CacheSize); ReservedRegion firstRegion = new(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{ {
@@ -65,8 +70,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length); int funcOffset = Allocate(code.Length);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = _jitRegion.Pointer + funcOffset; nint funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@@ -80,18 +85,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
} }
else else
{ {
ReprotectAsWritable(funcOffset, code.Length); ReprotectAsWritable(targetRegion, funcOffset, code.Length);
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) _jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
{
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
}
else
{
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
} }
Add(funcOffset, code.Length); Add(funcOffset, code.Length);
@@ -106,50 +104,80 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
Debug.Assert(_initialized); Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); foreach (ReservedRegion region in _jitRegions)
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{ {
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); if (pointer.ToInt64() < region.Pointer.ToInt64() ||
_cacheEntries.RemoveAt(entryIndex); pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
} }
} }
} }
private static void ReprotectAsWritable(int offset, int size) private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static void ReprotectAsExecutable(int offset, int size) private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static int Allocate(int codeSize) private static int Allocate(int codeSize)
{ {
codeSize = AlignCodeSize(codeSize); codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize); for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
if (allocOffset < 0)
{ {
throw new OutOfMemoryException("JIT Cache exhausted."); int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
} }
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); int exhaustedRegion = _activeRegionIndex;
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex;
return allocOffset; Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
} }
private static int AlignCodeSize(int codeSize) private static int AlignCodeSize(int codeSize)
@@ -12,7 +12,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int SharedCacheSize = 2047 * 1024 * 1024; private const int SharedCacheSize = 2047 * 1024 * 1024;
private const int LocalCacheSize = 128 * 1024 * 1024; private const int LocalCacheSize = 256 * 1024 * 1024;
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there // How many calls to the same function we allow until we pad the shared cache to force the function to become available there
// and allow the guest to take the fast path. // and allow the guest to take the fast path.
-1
View File
@@ -1,5 +1,4 @@
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System.Text.RegularExpressions;
namespace Ryujinx.Graphics.Vulkan namespace Ryujinx.Graphics.Vulkan
{ {
@@ -15,7 +15,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
namespace Ryujinx.HLE.HOS.Applets.Error namespace Ryujinx.HLE.HOS.Applets.Error
{ {
@@ -150,6 +150,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{ BsdSocketOption.SoLinger, SocketOptionName.Linger }, { BsdSocketOption.SoLinger, SocketOptionName.Linger },
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline }, { BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress }, { BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
{ BsdSocketOption.SoNoSigpipe, SocketOptionName.DontLinger },
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer }, { BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer }, { BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater }, { BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },
-1
View File
@@ -1,4 +1,3 @@
using MsgPack;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Memory; using Ryujinx.Memory;
+7
View File
@@ -582,6 +582,13 @@ namespace Ryujinx.Ava
Rainbow.Disable(); Rainbow.Disable();
Rainbow.Reset(); Rainbow.Reset();
// Reload settings when the game is turned off
// (resets custom settings if there were any)
Program.ReloadConfig();
//Updates the gameList (changes the status of the user setting if it was added or removed during the game)
RyujinxApp.MainWindow.GameListUpdate();
_isStopped = true; _isStopped = true;
Stop(); Stop();
} }
+18 -2
View File
@@ -1,7 +1,8 @@
<Styles <Styles
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"> xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia">
<Design.PreviewWith> <Design.PreviewWith>
<Border Height="2000" <Border Height="2000"
@@ -30,7 +31,8 @@
<Button <Button
Name="btnRem" Name="btnRem"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Content="Add" /> Content="Add"
Classes="red"/>
<TextBox <TextBox
Width="100" Width="100"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -41,7 +43,11 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<ui:NumberBox Value="1" /> <ui:NumberBox Value="1" />
<MenuItem
Header="123 0000"
ToolTip.Tip="What this"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="DropDownButton"> <Style Selector="DropDownButton">
@@ -373,6 +379,16 @@
<Setter Property="Background" <Setter Property="Background"
Value="{DynamicResource AppListHoverBackgroundColor}" /> Value="{DynamicResource AppListHoverBackgroundColor}" />
</Style> </Style>
<Style Selector="Button.red /template/ ContentPresenter">
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Background" Value="red"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<Style Selector="Button.red:pointerover /template/ ContentPresenter">
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Background" Value="{DynamicResource WarningBackgroundColor}" />
<Setter Property="Foreground" Value="White"/>
</Style>
<Styles.Resources> <Styles.Resources>
<SolidColorBrush x:Key="ThemeAccentColorBrush" <SolidColorBrush x:Key="ThemeAccentColorBrush"
Color="{DynamicResource SystemAccentColor}" /> Color="{DynamicResource SystemAccentColor}" />
+7 -1
View File
@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default"> <ResourceDictionary x:Key="Default">
@@ -12,11 +12,13 @@
<Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color> <Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color>
<Color x:Key="AppListBackgroundColor">#b3ffffff</Color> <Color x:Key="AppListBackgroundColor">#b3ffffff</Color>
<Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color> <Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color>
<Color x:Key="WarningBackgroundColor">#FF6347</Color>
<Color x:Key="SecondaryTextColor">#A0000000</Color> <Color x:Key="SecondaryTextColor">#A0000000</Color>
<Color x:Key="FavoriteApplicationIconColor">#fffcd12a</Color> <Color x:Key="FavoriteApplicationIconColor">#fffcd12a</Color>
<Color x:Key="Switch">#FF2EEAC9</Color> <Color x:Key="Switch">#FF2EEAC9</Color>
<Color x:Key="Unbounded">#FFFF4554</Color> <Color x:Key="Unbounded">#FFFF4554</Color>
<Color x:Key="Custom">#6483F5</Color> <Color x:Key="Custom">#6483F5</Color>
<Color x:Key="Warning">#800080</Color>
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="DataGridSelectionBackgroundBrush" <SolidColorBrush x:Key="DataGridSelectionBackgroundBrush"
@@ -29,11 +31,13 @@
<Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color> <Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color>
<Color x:Key="AppListBackgroundColor">#b3ffffff</Color> <Color x:Key="AppListBackgroundColor">#b3ffffff</Color>
<Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color> <Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color>
<Color x:Key="WarningBackgroundColor">#FF6347</Color>
<Color x:Key="SecondaryTextColor">#A0000000</Color> <Color x:Key="SecondaryTextColor">#A0000000</Color>
<Color x:Key="FavoriteApplicationIconColor">#fffcd12a</Color> <Color x:Key="FavoriteApplicationIconColor">#fffcd12a</Color>
<Color x:Key="Switch">#13c3a4</Color> <Color x:Key="Switch">#13c3a4</Color>
<Color x:Key="Unbounded">#FFFF4554</Color> <Color x:Key="Unbounded">#FFFF4554</Color>
<Color x:Key="Custom">#6483F5</Color> <Color x:Key="Custom">#6483F5</Color>
<Color x:Key="Warning">#800080</Color>
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="Dark"> <ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="DataGridSelectionBackgroundBrush" <SolidColorBrush x:Key="DataGridSelectionBackgroundBrush"
@@ -46,11 +50,13 @@
<Color x:Key="MenuFlyoutPresenterBorderColor">#3D3D3D</Color> <Color x:Key="MenuFlyoutPresenterBorderColor">#3D3D3D</Color>
<Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color> <Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color>
<Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color> <Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color>
<Color x:Key="WarningBackgroundColor">#FF6347</Color>
<Color x:Key="SecondaryTextColor">#A0FFFFFF</Color> <Color x:Key="SecondaryTextColor">#A0FFFFFF</Color>
<Color x:Key="FavoriteApplicationIconColor">#fffcd12a</Color> <Color x:Key="FavoriteApplicationIconColor">#fffcd12a</Color>
<Color x:Key="Switch">#FF2EEAC9</Color> <Color x:Key="Switch">#FF2EEAC9</Color>
<Color x:Key="Unbounded">#FFFF4554</Color> <Color x:Key="Unbounded">#FFFF4554</Color>
<Color x:Key="Custom">#6483F5</Color> <Color x:Key="Custom">#6483F5</Color>
<Color x:Key="Warning">#FFA500</Color>
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
</ResourceDictionary> </ResourceDictionary>
+150
View File
@@ -2722,6 +2722,31 @@
"zh_TW": "建立桌面捷徑,啟動選取的應用程式" "zh_TW": "建立桌面捷徑,啟動選取的應用程式"
} }
}, },
{
"ID": "GameListContextMenuEditGameConfiguration",
"Translations": {
"ar_SA": "تعديل تكوين اللعبة",
"de_DE": "Spielkonfiguration bearbeiten",
"el_GR": "Επεξεργασία ρυθμίσεων παιχνιδιού",
"en_US": "Edit Game Configuration",
"es_ES": "Editar configuración del juego",
"fr_FR": "Modifier la configuration du jeu",
"he_IL": "ערוך את הגדרת המשחק",
"it_IT": "Modifica configurazione gioco",
"ja_JP": "ゲーム設定を編集",
"ko_KR": "게임 설정 편집",
"no_NO": "Rediger spillkonfig.",
"pl_PL": "Edytuj konfigurację gry",
"pt_BR": "Editar configuração do jogo",
"ru_RU": "Редактировать конфигурацию игры",
"sv_SE": "Redigera spelkonfiguration",
"th_TH": "แก้ไขการตั้งค่าเกม",
"tr_TR": "Oyunun yapılandırmasını düzenle",
"uk_UA": "Редагувати конфігурацію гри",
"zh_CN": "编辑游戏配置",
"zh_TW": "編輯遊戲設定"
}
},
{ {
"ID": "GameListContextMenuCreateShortcutToolTipMacOS", "ID": "GameListContextMenuCreateShortcutToolTipMacOS",
"Translations": { "Translations": {
@@ -2747,6 +2772,31 @@
"zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式" "zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式"
} }
}, },
{
"ID": "EditGameConfigurationToolTip",
"Translations": {
"ar_SA": "ينشئ تكوينًا مستقلًا للعبة الحالية",
"de_DE": "Erstellt eine unabhängige Konfiguration für das aktuelle Spiel",
"el_GR": "Δημιουργεί μια ανεξάρτητη διαμόρφωση για το τρέχον παιχνίδι",
"en_US": "Creates an independent configuration for the current game",
"es_ES": "Crea una configuración independiente para el juego actual",
"fr_FR": "Crée une configuration indépendante pour le jeu en cours",
"he_IL": "יוצר תצורה עצמאית למשחק הנוכחי",
"it_IT": "Crea una configurazione indipendente per il gioco attuale",
"ja_JP": "現在のゲーム用の独立した設定を作成します",
"ko_KR": "현재 게임에 대한 독립적인 설정을 생성합니다",
"no_NO": "Oppretter en uavhengig konfigurasjon for det gjeldende spillet",
"pl_PL": "Tworzy niezależną konfigurację dla bieżącej gry",
"pt_BR": "Cria uma configuração independente para o jogo atual",
"ru_RU": "Создает независимую конфигурацию для текущей игры",
"sv_SE": "Skapar en oberoende konfiguration för det aktuella spelet",
"th_TH": "สร้างการกำหนดค่าที่เป็นอิสระสำหรับเกมปัจจุบัน",
"tr_TR": "Mevcut oyun için bağımsız bir yapılandırma oluşturur",
"uk_UA": "Створює незалежну конфігурацію для поточної гри",
"zh_CN": "为当前游戏创建独立的配置",
"zh_TW": "為當前遊戲創建獨立的配置"
}
},
{ {
"ID": "GameListContextMenuShowCompatEntry", "ID": "GameListContextMenuShowCompatEntry",
"Translations": { "Translations": {
@@ -3272,6 +3322,31 @@
"zh_TW": "設定" "zh_TW": "設定"
} }
}, },
{
"ID": "SettingsWithInfo",
"Translations": {
"ar_SA": "{0} - إعدادات",
"de_DE": "Einstellungen - {0}",
"el_GR": "Ρυθμίσεις - {0}",
"en_US": "Settings - {0}",
"es_ES": "Configuración - {0}",
"fr_FR": "Paramètres - {0}",
"he_IL": "{0} - הגדרות",
"it_IT": "Impostazioni - {0}",
"ja_JP": "設定 - {0}",
"ko_KR": "설정 - {0}",
"no_NO": "Innstillinger - {0}",
"pl_PL": "Ustawienia - {0}",
"pt_BR": "Configurações - {0}",
"ru_RU": "Параметры - {0}",
"sv_SE": "Inställningar - {0}",
"th_TH": "ตั้งค่า - {0}",
"tr_TR": "Ayarlar - {0}",
"uk_UA": "Налаштування - {0}",
"zh_CN": "设置 - {0}",
"zh_TW": "設定 - {0}"
}
},
{ {
"ID": "SettingsTabGeneral", "ID": "SettingsTabGeneral",
"Translations": { "Translations": {
@@ -5747,6 +5822,31 @@
"zh_TW": "啟用客體日誌" "zh_TW": "啟用客體日誌"
} }
}, },
{
"ID": "SettingsTabLoggingEnableAvaloniaLogs",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Enable UI Logs",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "SettingsTabLoggingEnableFsAccessLogs", "ID": "SettingsTabLoggingEnableFsAccessLogs",
"Translations": { "Translations": {
@@ -16722,6 +16822,31 @@
"zh_TW": "謹慎使用" "zh_TW": "謹慎使用"
} }
}, },
{
"ID": "AvaloniaLogTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prints Avalonia (UI) log messages in the console.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "OpenGlLogLevel", "ID": "OpenGlLogLevel",
"Translations": { "Translations": {
@@ -23672,6 +23797,31 @@
"zh_TW": "" "zh_TW": ""
} }
}, },
{
"ID": "UserConfigurationHeader",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "User Config",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "ExtractAocListHeader", "ID": "ExtractAocListHeader",
"Translations": { "Translations": {
-2
View File
@@ -1,6 +1,5 @@
using DiscordRPC; using DiscordRPC;
using Gommon; using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Ava.Utilities.Configuration;
@@ -11,7 +10,6 @@ using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon; using Ryujinx.Horizon;
using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Horizon.Prepo.Types;
using System.Linq;
using System.Text; using System.Text;
namespace Ryujinx.Ava namespace Ryujinx.Ava
+1 -1
View File
@@ -387,7 +387,7 @@ namespace Ryujinx.Headless
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")] [Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
public string GraphicsShadersDumpPath { get; set; } public string GraphicsShadersDumpPath { get; set; }
[Option("graphics-backend", Required = false, Default = GraphicsBackend.OpenGl, HelpText = "Change Graphics Backend to use.")] [Option("graphics-backend", Required = false, Default = GraphicsBackend.Vulkan, HelpText = "Change Graphics Backend to use.")]
public GraphicsBackend GraphicsBackend { get; set; } public GraphicsBackend GraphicsBackend { get; set; }
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")] [Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
+61 -1
View File
@@ -32,6 +32,7 @@ namespace Ryujinx.Ava
public static double DesktopScaleFactor { get; set; } = 1.0; public static double DesktopScaleFactor { get; set; } = 1.0;
public static string Version { get; private set; } public static string Version { get; private set; }
public static string ConfigurationPath { get; private set; } public static string ConfigurationPath { get; private set; }
public static string GlobalConfigurationPath { get; private set; }
public static bool PreviewerDetached { get; private set; } public static bool PreviewerDetached { get; private set; }
public static bool UseHardwareAcceleration { get; private set; } public static bool UseHardwareAcceleration { get; private set; }
@@ -155,11 +156,43 @@ namespace Ryujinx.Ava
} }
} }
public static bool FindGameConfig(string gameDir)
{
if (File.Exists(gameDir))
{
return true;
}
return false;
}
public static string GetDirGameUserConfig(string gameId, bool rememberGlobalDir = false, bool changeFolderForGame = false)
{
string gameDir = Path.Combine(AppDataManager.GamesDirPath, gameId, ReleaseInformation.ConfigName);
// Should load with the game if there is a custom setting for the game
if (rememberGlobalDir)
{
GlobalConfigurationPath = ConfigurationPath;
}
if (changeFolderForGame)
{
ConfigurationPath = gameDir;
}
return gameDir;
}
public static void ReloadConfig() public static void ReloadConfig()
{ {
//It is necessary that when a user setting appears, the global setting remains available
GlobalConfigurationPath = null;
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
// Now load the configuration as the other subsystems are now registered // Now load the configuration as the other subsystems are now registered
if (File.Exists(localConfigurationPath)) if (File.Exists(localConfigurationPath))
{ {
@@ -232,8 +265,35 @@ namespace Ryujinx.Ava
_ => ConfigurationState.Instance.HideCursor, _ => ConfigurationState.Instance.HideCursor,
}; };
// Check if memoryManagerMode was overridden.
if (CommandLineState.OverrideMemoryManagerMode is not null)
if (Enum.TryParse(CommandLineState.OverrideMemoryManagerMode, true, out MemoryManagerMode result))
{
ConfigurationState.Instance.System.MemoryManagerMode.Value = result;
}
// Check if hardware-acceleration was overridden. // Check if PPTC was overridden.
if (CommandLineState.OverridePPTC is not null)
if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result))
{
ConfigurationState.Instance.System.EnablePtc.Value = result;
}
// Check if region was overridden.
if (CommandLineState.OverrideSystemRegion is not null)
if (Enum.TryParse(CommandLineState.OverrideSystemRegion, true, out Ryujinx.HLE.HOS.SystemState.RegionCode result))
{
ConfigurationState.Instance.System.Region.Value = (Utilities.Configuration.System.Region)result;
}
//Check if language was overridden.
if (CommandLineState.OverrideSystemLanguage is not null)
if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Ryujinx.HLE.HOS.SystemState.SystemLanguage result))
{
ConfigurationState.Instance.System.Language.Value = (Utilities.Configuration.System.Language)result;
}
// Check if hardware-acceleration was overridden. MemoryManagerMode ( outdated! )
if (CommandLineState.OverrideHardwareAcceleration != null) if (CommandLineState.OverrideHardwareAcceleration != null)
UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value; UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value;
} }
@@ -19,6 +19,11 @@
Header="{ext:Locale GameListContextMenuCreateShortcut}" Header="{ext:Locale GameListContextMenuCreateShortcut}"
Icon="{ext:Icon fa-solid fa-bookmark}" Icon="{ext:Icon fa-solid fa-bookmark}"
ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" /> ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
<MenuItem
Click="EditGameConfiguration_Click"
Header="{ext:Locale GameListContextMenuEditGameConfiguration}"
Icon="{ext:Icon fa-solid fa-gear}"
ToolTip.Tip="{ext:Locale EditGameConfigurationToolTip}" />
<MenuItem <MenuItem
IsVisible="{Binding HasCompatibilityEntry}" IsVisible="{Binding HasCompatibilityEntry}"
Click="OpenApplicationCompatibility_Click" Click="OpenApplicationCompatibility_Click"
@@ -386,13 +386,26 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.SelectedApplication.Icon viewModel.SelectedApplication.Icon
); );
} }
public async void EditGameConfiguration_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
{
await new UserConfigWindows(viewModel).ShowDialog((Window)viewModel.TopLevel);
//just checking for file presence
viewModel.SelectedApplication.UserConfig = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString,false,false));
viewModel.RefreshView();
}
}
public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args) public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args)
{ {
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await CompatibilityList.Show(viewModel.SelectedApplication.IdString); await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
} }
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args) public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
{ {
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
@@ -55,9 +55,21 @@
Tag="{Binding AppData.IdString}" Tag="{Binding AppData.IdString}"
Text="{Binding AppData.LocalizedStatus}" Text="{Binding AppData.LocalizedStatus}"
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}" Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
ToolTip.Tip="{Binding AppData.LocalizedStatusTooltip}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap" /> TextWrapping="Wrap">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding AppData.LocalizedStatusTooltip}" />
<Separator
Margin="0, 10, 0, 10"
IsVisible="{Binding AppData.HasCompatibilityLabels}" />
<TextBlock
IsVisible="{Binding AppData.HasCompatibilityLabels}"
Text="{Binding AppData.FormattedCompatibilityLabels}" />
</StackPanel>
</ToolTip.Tip>
</TextBlock>
<Button.Styles> <Button.Styles>
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="MinWidth" <Setter Property="MinWidth"
@@ -1,12 +1,9 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Styling; using Avalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
@@ -7,6 +7,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
Focusable="True" Focusable="True"
@@ -73,12 +74,18 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}"> IsVisible="{Binding $parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}">
<TextBlock <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
HorizontalAlignment="Center" <TextBlock
VerticalAlignment="Center" Text="{Binding Name}"
Text="{Binding Name}" TextAlignment="Center"
TextAlignment="Center" TextWrapping="Wrap" />
TextWrapping="Wrap" /> <TextBlock
IsVisible="{Binding UserConfig}"
Text="{ext:Locale UserConfigurationHeader}"
TextAlignment="Center"
TextWrapping="Wrap"
Foreground="{DynamicResource Warning}" />
</StackPanel>
</Panel> </Panel>
</Grid> </Grid>
</Border> </Border>
@@ -86,10 +93,28 @@
Margin="5,5,0,0" Margin="5,5,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Top" VerticalAlignment="Top"
FontSize="16" FontSize="18"
Foreground="{DynamicResource FavoriteApplicationIconColor}" Foreground="{DynamicResource FavoriteApplicationIconColor}"
IsVisible="{Binding Favorite}" IsVisible="{Binding Favorite}"
Symbol="StarFilled" /> Symbol="StarFilled" />
<Grid IsVisible="{Binding !$parent[UserControl].((viewModels:MainWindowViewModel)DataContext).ShowNames}">
<Border
Margin="15,35,5,15"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Width="90"
Height="20"
CornerRadius="4"
IsVisible="{Binding UserConfig}"
Background="{DynamicResource ThemeContentBackgroundColor}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{ext:Locale UserConfigurationHeader}"
TextAlignment="Center"
TextWrapping="Wrap" />
</Border>
</Grid>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
@@ -6,6 +6,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
Focusable="True" Focusable="True"
@@ -93,8 +94,19 @@
IsVisible="{Binding HasPlayabilityInfo}" IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}" Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0" Margin="-1, 0, 0, 0"
Padding="0" Padding="0">
ToolTip.Tip="{Binding LocalizedStatusTooltip}"> <ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding LocalizedStatusTooltip}" />
<Separator
Margin="0, 10, 0, 10"
IsVisible="{Binding HasCompatibilityLabels}" />
<TextBlock
IsVisible="{Binding HasCompatibilityLabels}"
Text="{Binding FormattedCompatibilityLabels}" />
</StackPanel>
</ToolTip.Tip>
<TextBlock <TextBlock
Margin="1.5" Margin="1.5"
Tag="{Binding IdString}" Tag="{Binding IdString}"
@@ -145,6 +157,13 @@
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}" Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap"/> TextWrapping="Wrap"/>
<TextBlock
HorizontalAlignment="Stretch"
IsVisible="{Binding UserConfig}"
Text="{ext:Locale UserConfigurationHeader}"
TextAlignment="Start"
TextWrapping="Wrap"
Foreground="{DynamicResource Warning}" />
</StackPanel> </StackPanel>
<StackPanel <StackPanel
Grid.Column="4" Grid.Column="4"
@@ -7,7 +7,6 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat; using Ryujinx.Ava.Utilities.Compat;
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Controls
+7
View File
@@ -1,6 +1,7 @@
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Utilities; using Avalonia.Utilities;
using Gommon; using Gommon;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using System; using System;
using System.Text; using System.Text;
@@ -14,13 +15,19 @@ namespace Ryujinx.Ava.UI.Helpers
internal class LoggerAdapter : ILogSink internal class LoggerAdapter : ILogSink
{ {
private static bool _avaloniaLogsEnabled = ConfigurationState.Instance.Logger.EnableAvaloniaLog;
public static void Register() public static void Register()
{ {
AvaLogger.Sink = new LoggerAdapter(); AvaLogger.Sink = new LoggerAdapter();
ConfigurationState.Instance.Logger.EnableAvaloniaLog.Event
+= (_, e) => _avaloniaLogsEnabled = e.NewValue;
} }
private static RyuLogger.Log? GetLog(AvaLogLevel level, string area) private static RyuLogger.Log? GetLog(AvaLogLevel level, string area)
{ {
if (!_avaloniaLogsEnabled) return null;
return level switch return level switch
{ {
AvaLogLevel.Verbose => RyuLogger.Debug, AvaLogLevel.Verbose => RyuLogger.Debug,
@@ -1076,7 +1076,7 @@ namespace Ryujinx.Ava.UI.ViewModels
_rendererWaitEvent.WaitOne(); _rendererWaitEvent.WaitOne();
AppHost?.Start(); AppHost?.Start();
AppHost?.DisposeContext(); AppHost?.DisposeContext();
} }
@@ -1147,10 +1147,10 @@ namespace Ryujinx.Ava.UI.ViewModels
List<string> dirs = result.Select(it => it.Path.LocalPath).ToList(); List<string> dirs = result.Select(it => it.Path.LocalPath).ToList();
int numAdded = onDirsSelected(dirs, out int numRemoved); int numAdded = onDirsSelected(dirs, out int numRemoved);
string msg = String.Join("\r\n", new string[] { string msg = string.Join("\n",
string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved), string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved),
string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded) string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded)
}); );
await Dispatcher.UIThread.InvokeAsync(async () => await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
@@ -1523,8 +1523,34 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public void InitializeUserConfig(ApplicationData application)
{
// Code where conditions will be met before loading the user configuration
BackendThreading backendThreadingValue = ConfigurationState.Instance.Graphics.BackendThreading.Value;
// If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting.
string idGame = application.IdBaseString;
if (ConfigurationFileFormat.TryLoad(Program.GetDirGameUserConfig(idGame), out ConfigurationFileFormat configurationFileFormat))
{
// Loads the user configuration, having previously changed the global configuration to the user configuration
ConfigurationState.Instance.Load(configurationFileFormat, Program.GetDirGameUserConfig(idGame, true, true), idGame);
}
// Code where conditions will be executed after loading user configuration
if (ConfigurationState.Instance.Graphics.BackendThreading != backendThreadingValue)
{
/*
* The function to restart the emulator together with the selected game
Task.Run(async () => await Rebooter.RebootAppWithGame(application.Path));
*/
}
}
public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct<ApplicationControlProperty>? customNacpData = null) public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{ {
InitializeUserConfig(application);
if (AppHost != null) if (AppHost != null)
{ {
await ContentDialogHelper.CreateInfoDialog( await ContentDialogHelper.CreateInfoDialog(
@@ -1540,7 +1566,7 @@ namespace Ryujinx.Ava.UI.ViewModels
#if RELEASE #if RELEASE
await PerformanceCheck(); await PerformanceCheck();
#endif #endif
Logger.RestartTime(); Logger.RestartTime();
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
@@ -1585,6 +1611,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
gameThread.Start(); gameThread.Start();
} }
public void SwitchToRenderer(bool startFullscreen) => public void SwitchToRenderer(bool startFullscreen) =>
+175 -50
View File
@@ -1,8 +1,10 @@
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Humanizer;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Audio.Backends.SDL2;
@@ -26,6 +28,7 @@ using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -68,6 +71,46 @@ namespace Ryujinx.Ava.UI.ViewModels
public SettingsHacksViewModel DirtyHacks { get; } public SettingsHacksViewModel DirtyHacks { get; }
private Bitmap _gameIcon;
private string _gameTitle;
private string _gameId;
public Bitmap GameIcon
{
get => _gameIcon;
set
{
if (_gameIcon != value)
{
_gameIcon = value;
}
}
}
public string GameTitle
{
get => _gameTitle;
set
{
if (_gameTitle != value)
{
_gameTitle = value;
}
}
}
public string GameId
{
get => _gameId;
set
{
if (_gameId != value)
{
_gameId = value;
}
}
}
public int ResolutionScale public int ResolutionScale
{ {
get => _resolutionScale; get => _resolutionScale;
@@ -204,6 +247,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTrace { get; set; } public bool EnableTrace { get; set; }
public bool EnableGuest { get; set; } public bool EnableGuest { get; set; }
public bool EnableFsAccessLog { get; set; } public bool EnableFsAccessLog { get; set; }
public bool EnableAvaloniaLog { get; set; }
public bool EnableDebug { get; set; } public bool EnableDebug { get; set; }
public bool IsOpenAlEnabled { get; set; } public bool IsOpenAlEnabled { get; set; }
public bool IsSoundIoEnabled { get; set; } public bool IsSoundIoEnabled { get; set; }
@@ -329,7 +373,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsInvalidLdnPassphraseVisible { get; set; } public bool IsInvalidLdnPassphraseVisible { get; set; }
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(false)
{ {
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_contentManager = contentManager; _contentManager = contentManager;
@@ -342,7 +386,48 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public SettingsViewModel() public SettingsViewModel(VirtualFileSystem virtualFileSystem,
ContentManager contentManager,
string gamePath,
string gameName,
string gameId,
byte[] gameIconData,
bool enableToLoadCustomConfig) : this(enableToLoadCustomConfig)
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
if (gameIconData != null && gameIconData.Length > 0)
{
using (var ms = new MemoryStream(gameIconData))
{
GameIcon = new Bitmap(ms);
}
}
GameTitle = gameName;
GameId = gameId;
if (enableToLoadCustomConfig) // During the game. If there is no user config, then load the global config window
{
string gameDir = Program.GetDirGameUserConfig(gameId, false, true);
if (ConfigurationFileFormat.TryLoad(gameDir, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationState.Instance.Load(configurationFileFormat, gameDir, gameId);
}
LoadCurrentConfiguration(); // Needed to load custom configuration
}
if (Program.PreviewerDetached)
{
Task.Run(LoadTimeZones);
DirtyHacks = new SettingsHacksViewModel(this);
}
}
public SettingsViewModel(bool noLoadGlobalConfig = false)
{ {
GameDirectories = []; GameDirectories = [];
AutoloadDirectories = []; AutoloadDirectories = [];
@@ -357,7 +442,11 @@ namespace Ryujinx.Ava.UI.ViewModels
if (Program.PreviewerDetached) if (Program.PreviewerDetached)
{ {
Task.Run(LoadAvailableGpus); Task.Run(LoadAvailableGpus);
LoadCurrentConfiguration();
if (!noLoadGlobalConfig)// Default is false, but loading custom config avoids double call
{
LoadCurrentConfiguration();
}
DirtyHacks = new SettingsHacksViewModel(this); DirtyHacks = new SettingsHacksViewModel(this);
} }
@@ -468,27 +557,32 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
ConfigurationState config = ConfigurationState.Instance; ConfigurationState config = ConfigurationState.Instance;
// User Interface //It is necessary that the data is used from the global configuration file
EnableDiscordIntegration = config.EnableDiscordIntegration; if (string.IsNullOrEmpty(GameId))
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
ShowConfirmExit = config.ShowConfirmExit;
RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
AutoloadDirectories.Clear();
AutoloadDirectories.AddRange(config.UI.AutoloadDirs.Value);
BaseStyleIndex = config.UI.BaseStyle.Value switch
{ {
"Auto" => 0, // User Interface
"Light" => 1, EnableDiscordIntegration = config.EnableDiscordIntegration;
"Dark" => 2, CheckUpdatesOnStart = config.CheckUpdatesOnStart;
_ => 0 ShowConfirmExit = config.ShowConfirmExit;
}; RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
AutoloadDirectories.Clear();
AutoloadDirectories.AddRange(config.UI.AutoloadDirs.Value);
BaseStyleIndex = config.UI.BaseStyle.Value switch
{
"Auto" => 0,
"Light" => 1,
"Dark" => 2,
_ => 0
};
}
// Input // Input
EnableDockedMode = config.System.EnableDockedMode; EnableDockedMode = config.System.EnableDockedMode;
@@ -508,7 +602,6 @@ namespace Ryujinx.Ava.UI.ViewModels
DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset); DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset);
CurrentDate = currentDateTime.Date; CurrentDate = currentDateTime.Date;
CurrentTime = currentDateTime.TimeOfDay; CurrentTime = currentDateTime.TimeOfDay;
MatchSystemTime = config.System.MatchSystemTime; MatchSystemTime = config.System.MatchSystemTime;
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval; EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval;
@@ -560,6 +653,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableGuest = config.Logger.EnableGuest; EnableGuest = config.Logger.EnableGuest;
EnableDebug = config.Logger.EnableDebug; EnableDebug = config.Logger.EnableDebug;
EnableFsAccessLog = config.Logger.EnableFsAccessLog; EnableFsAccessLog = config.Logger.EnableFsAccessLog;
EnableAvaloniaLog = config.Logger.EnableAvaloniaLog;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
@@ -568,37 +662,41 @@ namespace Ryujinx.Ava.UI.ViewModels
LdnPassphrase = config.Multiplayer.LdnPassphrase; LdnPassphrase = config.Multiplayer.LdnPassphrase;
LdnServer = config.Multiplayer.LdnServer; LdnServer = config.Multiplayer.LdnServer;
} }
public void SaveSettings() public void SaveSettings()
{ {
ConfigurationState config = ConfigurationState.Instance; ConfigurationState config = ConfigurationState.Instance;
bool userConfigFile = string.IsNullOrEmpty(GameId);
// User Interface if (userConfigFile)
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
if (GameDirectoryChanged)
{ {
config.UI.GameDirs.Value = [..GameDirectories]; // User Interface
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
if (GameDirectoryChanged)
{
config.UI.GameDirs.Value = [.. GameDirectories];
}
if (AutoloadDirectoryChanged)
{
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
}
config.UI.BaseStyle.Value = BaseStyleIndex switch
{
0 => "Auto",
1 => "Light",
2 => "Dark",
_ => "Auto"
};
} }
if (AutoloadDirectoryChanged)
{
config.UI.AutoloadDirs.Value = [..AutoloadDirectories];
}
config.UI.BaseStyle.Value = BaseStyleIndex switch
{
0 => "Auto",
1 => "Light",
2 => "Dark",
_ => "Auto"
};
// Input // Input
config.System.EnableDockedMode.Value = EnableDockedMode; config.System.EnableDockedMode.Value = EnableDockedMode;
config.Hid.EnableKeyboard.Value = EnableKeyboard; config.Hid.EnableKeyboard.Value = EnableKeyboard;
@@ -679,6 +777,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Logger.EnableGuest.Value = EnableGuest; config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableDebug.Value = EnableDebug; config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
@@ -693,7 +792,9 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Hacks.EnableShaderTranslationDelay.Value = DirtyHacks.ShaderTranslationDelayEnabled; config.Hacks.EnableShaderTranslationDelay.Value = DirtyHacks.ShaderTranslationDelayEnabled;
config.Hacks.ShaderTranslationDelay.Value = DirtyHacks.ShaderTranslationDelay; config.Hacks.ShaderTranslationDelay.Value = DirtyHacks.ShaderTranslationDelay;
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
MainWindow.UpdateGraphicsConfig(); MainWindow.UpdateGraphicsConfig();
RyujinxApp.MainWindow.ViewModel.VSyncModeSettingChanged(); RyujinxApp.MainWindow.ViewModel.VSyncModeSettingChanged();
@@ -706,7 +807,11 @@ namespace Ryujinx.Ava.UI.ViewModels
private static void RevertIfNotSaved() private static void RevertIfNotSaved()
{ {
Program.ReloadConfig(); // maybe this is an unnecessary check(all options need to be tested)
if (string.IsNullOrEmpty(Program.GlobalConfigurationPath))
{
Program.ReloadConfig();
}
} }
public void ApplyButton() public void ApplyButton()
@@ -714,6 +819,26 @@ namespace Ryujinx.Ava.UI.ViewModels
SaveSettings(); SaveSettings();
} }
public void DeleteConfigGame()
{
string gameDir = Program.GetDirGameUserConfig(GameId,false,false);
if (File.Exists(gameDir))
{
File.Delete(gameDir);
}
RevertIfNotSaved();
CloseWindow?.Invoke();
}
public void SaveUserConfig()
{
SaveSettings();
RevertIfNotSaved(); // Revert global configuration after saving user configuration
CloseWindow?.Invoke();
}
public void OkButton() public void OkButton()
{ {
SaveSettings(); SaveSettings();
@@ -134,9 +134,26 @@ namespace Ryujinx.Ava.UI.Views.Main
Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager); Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager);
Rainbow.Enable(); Rainbow.Enable();
await Window.SettingsWindow.ShowDialog(Window); if (ViewModel.SelectedApplication is null) // Checks if game data exists
{
await Window.SettingsWindow.ShowDialog(Window);
}
else
{
bool userConfigExist = Program.FindGameConfig(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString, false, false));
if (!ViewModel.IsGameRunning || !userConfigExist)
{
await Window.SettingsWindow.ShowDialog(Window); // The game is not running, or if the user configuration does not exist
}
else
{
// If there is a custom configuration in the folder
await new UserConfigWindows(ViewModel, userConfigExist).ShowDialog((Window)ViewModel.TopLevel);
}
}
Rainbow.Disable(); Rainbow.Disable();
Rainbow.Reset(); Rainbow.Reset();
@@ -74,6 +74,10 @@
ToolTip.Tip="{ext:Locale DebugLogTooltip}"> ToolTip.Tip="{ext:Locale DebugLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableDebugLogs}" /> <TextBlock Text="{ext:Locale SettingsTabLoggingEnableDebugLogs}" />
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding EnableAvaloniaLog}"
ToolTip.Tip="{ext:Locale AvaloniaLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableAvaloniaLogs}" />
</CheckBox>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch"> <StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale FSAccessLogModeTooltip}" ToolTip.Tip="{ext:Locale FSAccessLogModeTooltip}"
@@ -1,5 +1,4 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
@@ -645,6 +645,11 @@ namespace Ryujinx.Ava.UI.Windows
ReloadGameList(); ReloadGameList();
} }
public void GameListUpdate()
{
ReloadGameList();
}
public void ToggleFileType(string fileType) public void ToggleFileType(string fileType)
{ {
switch (fileType) switch (fileType)
@@ -0,0 +1,144 @@
<window:StyleableAppWindow
x:Class="Ryujinx.Ava.UI.Windows.UserConfigWindows"
xmlns="https://github.com/avaloniaui"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
Width="1100"
Height="768"
MinWidth="800"
MinHeight="480"
WindowStartupLocation="CenterOwner"
x:DataType="viewModels:SettingsViewModel"
mc:Ignorable="d"
Focusable="True">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="600" RowDefinitions="Auto,*,Auto">
<ContentPresenter
x:Name="ContentPresenter"
Grid.Row="1"
IsVisible="False"
KeyboardNavigation.IsTabStop="False"/>
<Grid Name="Pages" IsVisible="False" Grid.Row="2">
<settings:SettingsInputView Name="InputPage" />
<settings:SettingsSystemView Name="SystemPage" />
<settings:SettingsCPUView Name="CpuPage" />
<settings:SettingsGraphicsView Name="GraphicsPage" />
<settings:SettingsAudioView Name="AudioPage" />
<settings:SettingsNetworkView Name="NetworkPage" />
<settings:SettingsLoggingView Name="LoggingPage" />
<settings:SettingsHacksView Name="HacksPage" />
</Grid>
<ui:NavigationView
Grid.Row="1"
IsSettingsVisible="False"
Name="NavPanel"
IsBackEnabled="False"
PaneDisplayMode="Left"
Margin="2,10,10,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
OpenPaneLength="200"
IsPaneToggleButtonVisible="False">
<!-- For image -->
<ui:NavigationView.PaneHeader>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding GameId}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,0,0,10"
TextAlignment="Center" Grid.Row="0" />
<Image Source="{Binding GameIcon}"
Width="160"
Height="160"
Grid.Row="1"
Margin="0,0,0,10"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding GameTitle}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,0,0,10"
TextAlignment="Center" Grid.Row="2" />
<Separator Height="1" Grid.Row="3" Margin="0,0,0,10" HorizontalAlignment="Stretch"/>
</Grid>
</ui:NavigationView.PaneHeader>
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabInput}"
Tag="InputPage"
IconSource="Games" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabSystem}"
Tag="SystemPage"
IconSource="Settings" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabCpu}"
Tag="CpuPage">
<ui:NavigationViewItem.IconSource>
<ui:FontIconSource
FontFamily="avares://Ryujinx/Assets/Fonts#Segoe Fluent Icons"
Glyph="{helpers:GlyphValueConverter Chip}" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabGraphics}"
Tag="GraphicsPage"
IconSource="Image" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabAudio}"
IconSource="Audio"
Tag="AudioPage" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabNetwork}"
Tag="NetworkPage"
IconSource="Globe" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabLogging}"
Tag="LoggingPage"
IconSource="Document" />
<ui:NavigationViewItem
IsVisible="{Binding ShowDirtyHacks}"
Content="Dirty Hacks"
Tag="HacksPage"
IconSource="Code" />
</ui:NavigationView.MenuItems>
</ui:NavigationView>
<ReversibleStackPanel
Grid.Row="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Content="{ext:Locale SettingsButtonSave}"
Command="{Binding SaveUserConfig}" />
<Button
HotKey="Escape"
Content="{ext:Locale SettingsButtonClose}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale UserProfilesDelete}"
Command="{Binding DeleteConfigGame}"
Classes="red"/>
</ReversibleStackPanel>
</Grid>
</window:StyleableAppWindow>
@@ -0,0 +1,116 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Projektanker.Icons.Avalonia;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using System;
using System.IO;
using System.Linq;
using Key = Avalonia.Input.Key;
namespace Ryujinx.Ava.UI.Windows
{
public partial class UserConfigWindows : StyleableAppWindow
{
internal readonly SettingsViewModel ViewModel;
public UserConfigWindows(MainWindowViewModel viewModel, bool findUserConfigDir = true)
{
Title = string.Format(LocaleManager.Instance[LocaleKeys.SettingsWithInfo], viewModel.SelectedApplication.Name, viewModel.SelectedApplication.IdString);
DataContext = ViewModel = new SettingsViewModel(
viewModel.VirtualFileSystem,
viewModel.ContentManager,
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.IdString,
viewModel.SelectedApplication.Icon,
findUserConfigDir);
ViewModel.CloseWindow += Close;
ViewModel.SaveSettingsEvent += SaveSettings;
InitializeComponent();
Load();
#if DEBUG
this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Alt));
#endif
}
public void SaveSettings()
{
InputPage.InputView?.SaveCurrentProfile();
}
private void Load()
{
Pages.Children.Clear();
NavPanel.SelectionChanged += NavPanelOnSelectionChanged;
NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
}
private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.SelectedItem is NavigationViewItem navItem && navItem.Tag is not null)
{
switch (navItem.Tag.ToString())
{
case nameof(InputPage):
NavPanel.Content = InputPage;
break;
case nameof(SystemPage):
SystemPage.ViewModel = ViewModel;
NavPanel.Content = SystemPage;
break;
case nameof(CpuPage):
NavPanel.Content = CpuPage;
break;
case nameof(GraphicsPage):
NavPanel.Content = GraphicsPage;
break;
case nameof(AudioPage):
NavPanel.Content = AudioPage;
break;
case nameof(NetworkPage):
NetworkPage.ViewModel = ViewModel;
NavPanel.Content = NetworkPage;
break;
case nameof(LoggingPage):
NavPanel.Content = LoggingPage;
break;
case nameof(HacksPage):
HacksPage.DataContext = ViewModel;
NavPanel.Content = HacksPage;
break;
default:
throw new NotImplementedException();
}
}
}
protected override void OnClosing(WindowClosingEventArgs e)
{
InputPage.Dispose(); // You need to unload the gamepad settings, otherwise the controls will be blocked
base.OnClosing(e);
}
}
}
@@ -1,3 +1,4 @@
using Gommon;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
@@ -14,6 +15,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary namespace Ryujinx.Ava.Utilities.AppLibrary
@@ -21,6 +23,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public class ApplicationData public class ApplicationData
{ {
public bool Favorite { get; set; } public bool Favorite { get; set; }
public bool UserConfig { get; set; }
public byte[] Icon { get; set; } public byte[] Icon { get; set; }
public string Name { get; set; } = "Unknown"; public string Name { get; set; } = "Unknown";
@@ -32,33 +35,12 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
set set
{ {
_id = value; _id = value;
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
Compatibility = CompatibilityCsv.Find(Id);
} }
} }
public string Developer { get; set; } = "Unknown"; public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0"; public string Version { get; set; } = "0";
public bool HasPlayabilityInfo => PlayabilityStatus != null;
public string LocalizedStatus =>
PlayabilityStatus.HasValue
? LocaleManager.Instance[PlayabilityStatus!.Value]
: string.Empty;
public LocaleKeys? PlayabilityStatus { get; set; }
public string LocalizedStatusTooltip =>
PlayabilityStatus.HasValue
#pragma warning disable CS8509 // It is exhaustive for any value this property can contain.
? LocaleManager.Instance[PlayabilityStatus!.Value switch
#pragma warning restore CS8509
{
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
}]
: string.Empty;
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
public int GameCount { get; set; } public int GameCount { get; set; }
@@ -78,11 +60,39 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public bool HasPlayedPreviously => TimePlayedString != string.Empty; public bool HasPlayedPreviously => TimePlayed.TotalSeconds > 1;
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n"); public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
public Optional<CompatibilityEntry> Compatibility { get; private set; }
public bool HasPlayabilityInfo => Compatibility.HasValue;
public string LocalizedStatus => Compatibility.Convert(x => x.LocalizedStatus);
public bool HasCompatibilityLabels => !FormattedCompatibilityLabels.Equals(string.Empty);
public string FormattedCompatibilityLabels
=> Compatibility.Convert(x => x.FormattedIssueLabels).OrElse(string.Empty);
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
public string LocalizedStatusTooltip =>
Compatibility.Convert(x =>
#pragma warning disable CS8509 It is exhaustive for all possible values this can contain.
LocaleManager.Instance[x.Status switch
#pragma warning restore CS8509
{
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
}]
).OrElse(string.Empty);
[JsonIgnore] public string IdString => Id.ToString("x16"); [JsonIgnore] public string IdString => Id.ToString("x16");
@@ -92,16 +102,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{ {
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
if (!System.IO.Path.Exists(titleFilePath)) if (!System.IO.Path.Exists(titleFilePath))
{ {
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist."); Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
return string.Empty; return string.Empty;
} }
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower(); string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();
@@ -505,7 +505,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
if (data.Id != 0) if (data.Id != 0)
{ {
ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata =>
{ {
appMetadata.Title = data.Name; appMetadata.Title = data.Name;
// Only do the migration if time_played has a value and timespan_played hasn't been updated yet. // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
@@ -529,10 +529,11 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
} }
}); });
data.Favorite = appMetadata.Favorite; data.Favorite = appMetadata.Favorite;
data.TimePlayed = appMetadata.TimePlayed; data.TimePlayed = appMetadata.TimePlayed;
data.LastPlayed = appMetadata.LastPlayed; data.LastPlayed = appMetadata.LastPlayed;
data.UserConfig = File.Exists(Program.GetDirGameUserConfig(data.IdBaseString, false, false)); // Just check user config
} }
data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();
+56 -1
View File
@@ -6,11 +6,15 @@ namespace Ryujinx.Ava.Utilities
public static class CommandLineState public static class CommandLineState
{ {
public static string[] Arguments { get; private set; } public static string[] Arguments { get; private set; }
public static int CountArguments { get; private set; }
public static bool? OverrideDockedMode { get; private set; } public static bool? OverrideDockedMode { get; private set; }
public static bool? OverrideHardwareAcceleration { get; private set; } public static bool? OverrideHardwareAcceleration { get; private set; }
public static string OverrideGraphicsBackend { get; private set; } public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideBackendThreading { get; private set; } public static string OverrideBackendThreading { get; private set; }
public static string OverridePPTC { get; private set; }
public static string OverrideMemoryManagerMode { get; private set; }
public static string OverrideSystemRegion { get; private set; }
public static string OverrideSystemLanguage { get; private set; }
public static string OverrideHideCursor { get; private set; } public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; } public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; } public static string Profile { get; private set; }
@@ -19,6 +23,11 @@ namespace Ryujinx.Ava.Utilities
public static bool StartFullscreenArg { get; private set; } public static bool StartFullscreenArg { get; private set; }
public static bool HideAvailableUpdates { get; private set; } public static bool HideAvailableUpdates { get; private set; }
public static void ArgumentsClean()
{
}
public static void ParseArguments(string[] args) public static void ParseArguments(string[] args)
{ {
List<string> arguments = []; List<string> arguments = [];
@@ -28,6 +37,11 @@ namespace Ryujinx.Ava.Utilities
{ {
string arg = args[i]; string arg = args[i];
if (arg.Contains("-") || arg.Contains("--"))
{
CountArguments++;
}
switch (arg) switch (arg)
{ {
case "-r": case "-r":
@@ -85,6 +99,47 @@ namespace Ryujinx.Ava.Utilities
OverrideBackendThreading = args[++i]; OverrideBackendThreading = args[++i];
break; break;
case "--pptc":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverridePPTC = args[++i];
break;
case "-m":
case "--memory-manager-mode":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideMemoryManagerMode = args[++i];
break;
case "--system-region":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideSystemRegion = args[++i];
break;
case "--system-language":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideSystemLanguage = args[++i];
break;
case "-i": case "-i":
case "--application-id": case "--application-id":
LaunchApplicationId = args[++i]; LaunchApplicationId = args[++i];
@@ -60,10 +60,21 @@ namespace Ryujinx.Ava.Utilities.Compat
} }
} }
public static CompatibilityEntry Find(string titleId)
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId));
public static CompatibilityEntry Find(ulong titleId)
=> Find(titleId.ToString("X16"));
public static LocaleKeys? GetStatus(string titleId) public static LocaleKeys? GetStatus(string titleId)
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status; => Find(titleId)?.Status;
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16")); public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
public static string GetLabels(string titleId)
=> Find(titleId)?.FormattedIssueLabels;
public static string GetLabels(ulong titleId) => GetLabels(titleId.ToString("X16"));
} }
public class CompatibilityEntry public class CompatibilityEntry
@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 63; public const int CurrentVersion = 64;
/// <summary> /// <summary>
/// Version of the configuration file format /// Version of the configuration file format
@@ -111,6 +111,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages /// Enables printing FS access log messages
/// </summary> /// </summary>
public bool LoggingEnableFsAccessLog { get; set; } public bool LoggingEnableFsAccessLog { get; set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public bool LoggingEnableAvalonia { get; set; }
/// <summary> /// <summary>
/// Controls which log messages are written to the log targets /// Controls which log messages are written to the log targets
@@ -18,9 +18,15 @@ namespace Ryujinx.Ava.Utilities.Configuration
{ {
public partial class ConfigurationState public partial class ConfigurationState
{ {
public void Load(ConfigurationFileFormat cff, string configurationFilePath) public void Load(ConfigurationFileFormat cff, string configurationFilePath, string gameId="")
{ {
bool configurationFileUpdated = false; bool configurationFileUpdated = false;
bool LoadSetting = true;
if (!string.IsNullOrEmpty(gameId))
{
LoadSetting = false;
}
if (cff.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) if (cff.Version is < 0 or > ConfigurationFileFormat.CurrentVersion)
{ {
@@ -43,14 +49,14 @@ namespace Ryujinx.Ava.Utilities.Configuration
configurationFileUpdated = true; configurationFileUpdated = true;
} }
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration; EnableDiscordIntegration.Value = LoadSetting ? cff.EnableDiscordIntegration : EnableDiscordIntegration.Value; // Get from global config only
CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart; CheckUpdatesOnStart.Value = LoadSetting ? cff.CheckUpdatesOnStart : CheckUpdatesOnStart.Value; // Get from global config only
ShowConfirmExit.Value = cff.ShowConfirmExit; ShowConfirmExit.Value = LoadSetting ? cff.ShowConfirmExit : ShowConfirmExit.Value; // Get from global config only
RememberWindowState.Value = cff.RememberWindowState; RememberWindowState.Value = LoadSetting ? cff.RememberWindowState : RememberWindowState.Value; // Get from global config only
ShowTitleBar.Value = cff.ShowTitleBar; ShowTitleBar.Value = LoadSetting ? cff.ShowTitleBar : ShowTitleBar.Value; // Get from global config only
EnableHardwareAcceleration.Value = cff.EnableHardwareAcceleration; EnableHardwareAcceleration.Value = LoadSetting ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only
HideCursor.Value = cff.HideCursor; HideCursor.Value = LoadSetting ? cff.HideCursor : HideCursor.Value; // Get from global config only
Logger.EnableFileLog.Value = cff.EnableFileLog; Logger.EnableFileLog.Value = cff.EnableFileLog;
Logger.EnableDebug.Value = cff.LoggingEnableDebug; Logger.EnableDebug.Value = cff.LoggingEnableDebug;
Logger.EnableStub.Value = cff.LoggingEnableStub; Logger.EnableStub.Value = cff.LoggingEnableStub;
@@ -85,7 +91,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.Language.Value = cff.SystemLanguage; System.Language.Value = cff.SystemLanguage;
System.Region.Value = cff.SystemRegion; System.Region.Value = cff.SystemRegion;
System.TimeZone.Value = cff.SystemTimeZone; System.TimeZone.Value = cff.SystemTimeZone;
System.SystemTimeOffset.Value = cff.SystemTimeOffset; System.SystemTimeOffset.Value = LoadSetting ? cff.SystemTimeOffset : System.SystemTimeOffset.Value; // Get from global config only
System.EnableDockedMode.Value = cff.DockedMode; System.EnableDockedMode.Value = cff.DockedMode;
System.EnablePtc.Value = cff.EnablePtc; System.EnablePtc.Value = cff.EnablePtc;
System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc; System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc;
@@ -99,46 +105,47 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.IgnoreMissingServices.Value = cff.IgnoreMissingServices; System.IgnoreMissingServices.Value = cff.IgnoreMissingServices;
System.IgnoreApplet.Value = cff.IgnoreApplet; System.IgnoreApplet.Value = cff.IgnoreApplet;
System.UseHypervisor.Value = cff.UseHypervisor; System.UseHypervisor.Value = cff.UseHypervisor;
UI.GuiColumns.FavColumn.Value = cff.GuiColumns.FavColumn; UI.GuiColumns.FavColumn.Value = LoadSetting ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value;
UI.GuiColumns.IconColumn.Value = cff.GuiColumns.IconColumn; UI.GuiColumns.IconColumn.Value = LoadSetting ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value;
UI.GuiColumns.AppColumn.Value = cff.GuiColumns.AppColumn; UI.GuiColumns.AppColumn.Value = LoadSetting ? cff.GuiColumns.AppColumn : UI.GuiColumns.AppColumn.Value;
UI.GuiColumns.DevColumn.Value = cff.GuiColumns.DevColumn; UI.GuiColumns.DevColumn.Value = LoadSetting ? cff.GuiColumns.DevColumn : UI.GuiColumns.DevColumn.Value;
UI.GuiColumns.VersionColumn.Value = cff.GuiColumns.VersionColumn; UI.GuiColumns.VersionColumn.Value = LoadSetting ? cff.GuiColumns.VersionColumn : UI.GuiColumns.VersionColumn.Value;
UI.GuiColumns.TimePlayedColumn.Value = cff.GuiColumns.TimePlayedColumn; UI.GuiColumns.TimePlayedColumn.Value = LoadSetting ? cff.GuiColumns.TimePlayedColumn : UI.GuiColumns.TimePlayedColumn.Value;
UI.GuiColumns.LastPlayedColumn.Value = cff.GuiColumns.LastPlayedColumn; UI.GuiColumns.LastPlayedColumn.Value = LoadSetting ? cff.GuiColumns.LastPlayedColumn : UI.GuiColumns.LastPlayedColumn.Value;
UI.GuiColumns.FileExtColumn.Value = cff.GuiColumns.FileExtColumn; UI.GuiColumns.FileExtColumn.Value = LoadSetting ? cff.GuiColumns.FileExtColumn : UI.GuiColumns.FileExtColumn.Value;
UI.GuiColumns.FileSizeColumn.Value = cff.GuiColumns.FileSizeColumn; UI.GuiColumns.FileSizeColumn.Value = LoadSetting ? cff.GuiColumns.FileSizeColumn : UI.GuiColumns.FileSizeColumn.Value;
UI.GuiColumns.PathColumn.Value = cff.GuiColumns.PathColumn; UI.GuiColumns.PathColumn.Value = LoadSetting ? cff.GuiColumns.PathColumn : UI.GuiColumns.PathColumn.Value;
UI.ColumnSort.SortColumnId.Value = cff.ColumnSort.SortColumnId; UI.ColumnSort.SortColumnId.Value = LoadSetting ? cff.ColumnSort.SortColumnId : UI.ColumnSort.SortColumnId.Value;
UI.ColumnSort.SortAscending.Value = cff.ColumnSort.SortAscending; UI.ColumnSort.SortAscending.Value = LoadSetting ? cff.ColumnSort.SortAscending : UI.ColumnSort.SortAscending.Value;
UI.GameDirs.Value = cff.GameDirs; UI.GameDirs.Value = LoadSetting ? cff.GameDirs : UI.GameDirs.Value;
UI.AutoloadDirs.Value = cff.AutoloadDirs ?? []; UI.AutoloadDirs.Value = LoadSetting ? (cff.AutoloadDirs ?? []) : UI.AutoloadDirs.Value;
UI.ShownFileTypes.NSP.Value = cff.ShownFileTypes.NSP; UI.ShownFileTypes.NSP.Value = LoadSetting ? cff.ShownFileTypes.NSP : UI.ShownFileTypes.NSP.Value;
UI.ShownFileTypes.PFS0.Value = cff.ShownFileTypes.PFS0; UI.ShownFileTypes.PFS0.Value = LoadSetting ? cff.ShownFileTypes.PFS0 : UI.ShownFileTypes.PFS0.Value;
UI.ShownFileTypes.XCI.Value = cff.ShownFileTypes.XCI; UI.ShownFileTypes.XCI.Value = LoadSetting ? cff.ShownFileTypes.XCI : UI.ShownFileTypes.XCI.Value;
UI.ShownFileTypes.NCA.Value = cff.ShownFileTypes.NCA; UI.ShownFileTypes.NCA.Value = LoadSetting ? cff.ShownFileTypes.NCA : UI.ShownFileTypes.NCA.Value;
UI.ShownFileTypes.NRO.Value = cff.ShownFileTypes.NRO; UI.ShownFileTypes.NRO.Value = LoadSetting ? cff.ShownFileTypes.NRO : UI.ShownFileTypes.NRO.Value;
UI.ShownFileTypes.NSO.Value = cff.ShownFileTypes.NSO; UI.ShownFileTypes.NSO.Value = LoadSetting ? cff.ShownFileTypes.NSO : UI.ShownFileTypes.NSO.Value;
UI.LanguageCode.Value = cff.LanguageCode; UI.LanguageCode.Value = LoadSetting ? cff.LanguageCode : UI.LanguageCode.Value;
UI.BaseStyle.Value = cff.BaseStyle; UI.BaseStyle.Value = LoadSetting ? cff.BaseStyle : UI.BaseStyle.Value;
UI.GameListViewMode.Value = cff.GameListViewMode; UI.GameListViewMode.Value = LoadSetting ? cff.GameListViewMode : UI.GameListViewMode.Value;
UI.ShowNames.Value = cff.ShowNames; UI.ShowNames.Value = LoadSetting ? cff.ShowNames : UI.ShowNames.Value;
UI.IsAscendingOrder.Value = cff.IsAscendingOrder; UI.IsAscendingOrder.Value = LoadSetting ? cff.IsAscendingOrder : UI.IsAscendingOrder.Value;
UI.GridSize.Value = cff.GridSize; UI.GridSize.Value = LoadSetting ? cff.GridSize : UI.GridSize.Value;
UI.ApplicationSort.Value = cff.ApplicationSort; UI.ApplicationSort.Value = LoadSetting ? cff.ApplicationSort : UI.ApplicationSort.Value;
UI.StartFullscreen.Value = cff.StartFullscreen; UI.StartFullscreen.Value = LoadSetting ? cff.StartFullscreen : UI.StartFullscreen.Value;
UI.StartNoUI.Value = cff.StartNoUI; UI.StartNoUI.Value = LoadSetting ? cff.StartNoUI : UI.StartNoUI.Value;
UI.ShowConsole.Value = cff.ShowConsole; UI.ShowConsole.Value = LoadSetting ? cff.ShowConsole : UI.ShowConsole.Value;
UI.WindowStartup.WindowSizeWidth.Value = cff.WindowStartup.WindowSizeWidth; UI.WindowStartup.WindowSizeWidth.Value = LoadSetting ? cff.WindowStartup.WindowSizeWidth : UI.WindowStartup.WindowSizeWidth.Value;
UI.WindowStartup.WindowSizeHeight.Value = cff.WindowStartup.WindowSizeHeight; UI.WindowStartup.WindowSizeHeight.Value = LoadSetting ? cff.WindowStartup.WindowSizeHeight : UI.WindowStartup.WindowSizeHeight.Value;
UI.WindowStartup.WindowPositionX.Value = cff.WindowStartup.WindowPositionX; UI.WindowStartup.WindowPositionX.Value = LoadSetting ? cff.WindowStartup.WindowPositionX : UI.WindowStartup.WindowPositionX.Value;
UI.WindowStartup.WindowPositionY.Value = cff.WindowStartup.WindowPositionY; UI.WindowStartup.WindowPositionY.Value = LoadSetting ? cff.WindowStartup.WindowPositionY : UI.WindowStartup.WindowPositionY.Value;
UI.WindowStartup.WindowMaximized.Value = cff.WindowStartup.WindowMaximized; UI.WindowStartup.WindowMaximized.Value = LoadSetting ? cff.WindowStartup.WindowMaximized : UI.WindowStartup.WindowMaximized.Value;
Hid.EnableKeyboard.Value = cff.EnableKeyboard; Hid.EnableKeyboard.Value = cff.EnableKeyboard;
Hid.EnableMouse.Value = cff.EnableMouse; Hid.EnableMouse.Value = cff.EnableMouse;
Hid.Hotkeys.Value = cff.Hotkeys; Hid.Hotkeys.Value = LoadSetting ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only
Hid.InputConfig.Value = cff.InputConfig ?? []; Hid.InputConfig.Value = cff.InputConfig ?? [];
Hid.RainbowSpeed.Value = cff.RainbowSpeed; Hid.RainbowSpeed.Value = cff.RainbowSpeed;
@@ -430,7 +437,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
} }
}), }),
(62, static cff => cff.RainbowSpeed = 1f), (62, static cff => cff.RainbowSpeed = 1f),
(63, static cff => cff.MatchSystemTime = false) (63, static cff => cff.MatchSystemTime = false),
(64, static cff => cff.LoggingEnableAvalonia = false)
); );
} }
} }
@@ -254,6 +254,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages /// Enables printing FS access log messages
/// </summary> /// </summary>
public ReactiveObject<bool> EnableFsAccessLog { get; private set; } public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public ReactiveObject<bool> EnableAvaloniaLog { get; private set; }
/// <summary> /// <summary>
/// Controls which log messages are written to the log targets /// Controls which log messages are written to the log targets
@@ -281,6 +286,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableTrace = new ReactiveObject<bool>(); EnableTrace = new ReactiveObject<bool>();
EnableGuest = new ReactiveObject<bool>(); EnableGuest = new ReactiveObject<bool>();
EnableFsAccessLog = new ReactiveObject<bool>(); EnableFsAccessLog = new ReactiveObject<bool>();
EnableAvaloniaLog = new ReactiveObject<bool>();
FilteredClasses = new ReactiveObject<LogClass[]>(); FilteredClasses = new ReactiveObject<LogClass[]>();
EnableFileLog = new ReactiveObject<bool>(); EnableFileLog = new ReactiveObject<bool>();
EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
@@ -46,6 +46,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
LoggingEnableTrace = Logger.EnableTrace, LoggingEnableTrace = Logger.EnableTrace,
LoggingEnableGuest = Logger.EnableGuest, LoggingEnableGuest = Logger.EnableGuest,
LoggingEnableFsAccessLog = Logger.EnableFsAccessLog, LoggingEnableFsAccessLog = Logger.EnableFsAccessLog,
LoggingEnableAvalonia = Logger.EnableAvaloniaLog,
LoggingFilteredClasses = Logger.FilteredClasses, LoggingFilteredClasses = Logger.FilteredClasses,
LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel, LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel,
SystemLanguage = System.Language, SystemLanguage = System.Language,
@@ -165,6 +166,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Logger.EnableTrace.Value = false; Logger.EnableTrace.Value = false;
Logger.EnableGuest.Value = true; Logger.EnableGuest.Value = true;
Logger.EnableFsAccessLog.Value = false; Logger.EnableFsAccessLog.Value = false;
Logger.EnableAvaloniaLog.Value = false;
Logger.FilteredClasses.Value = []; Logger.FilteredClasses.Value = [];
Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None;
System.Language.Value = Language.AmericanEnglish; System.Language.Value = Language.AmericanEnglish;
+14 -8
View File
@@ -1,5 +1,4 @@
using Gommon; using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -31,8 +30,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _), Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new GameSpec { TitleIds = [titleId] })); return AddSpec(transform(GameSpec.Create(titleId)));
return this;
} }
/// <summary> /// <summary>
@@ -46,8 +44,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _), Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform)); return AddSpec(GameSpec.Create(titleId).Apply(transform));
return this;
} }
/// <summary> /// <summary>
@@ -63,8 +60,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)), 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)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new GameSpec { TitleIds = [..tids] })); return AddSpec(transform(GameSpec.Create(tids)));
return this;
} }
/// <summary> /// <summary>
@@ -79,7 +75,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)), 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)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform)); 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>
/// <param name="spec">The <see cref="GameSpec"/> to add.</param>
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(GameSpec spec)
{
_specs.Add(spec);
return this; return this;
} }
@@ -1,7 +1,6 @@
using MsgPack; using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport namespace Ryujinx.Ava.Utilities.PlayReport
{ {
@@ -1,4 +1,5 @@
using System; using Gommon;
using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -281,9 +282,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList(); players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList();
return players.Count > 4 return players.Count > 4
? $"{players.Count} Players - " + string.Join(", ", ? $"{players.Count} Players - {
players.Take(3).Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")) players.Take(3)
: string.Join(", ", players.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")); .Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
.JoinToString(", ")
}"
: players
.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
.JoinToString(", ");
string RankMedal(int? rank) => rank switch string RankMedal(int? rank) => rank switch
{ {
@@ -294,8 +300,67 @@ namespace Ryujinx.Ava.Utilities.PlayReport
}; };
} }
private static FormattedValue N64_LaunchedGame(SingleValue value) => value.Matched.StringValue switch private static FormattedValue NsoEmulator_LaunchedGame(SingleValue value) => value.Matched.StringValue switch
{ {
#region SEGA Genesis
"m_0054_e" => Playing("Alien Soldier"),
"m_3978_e" => Playing("Alien Storm"),
"m_5234_e" => Playing("ALISIA DRAGOON"),
"m_5003_e" => Playing("Streets of Rage 2"),
"m_4843_e" => Playing("Kid Chameleon"),
"m_2874_e" => Playing("Columns"),
"m_3167_e" => Playing("Comix Zone"),
"m_5007_e" => Playing("Contra: Hard Corps"),
"m_0865_e" => Playing("Ghouls 'n Ghosts"),
"m_0935_e" => Playing("Dynamite Headdy"),
"m_8314_e" => Playing("Earthworm Jim"),
"m_5012_e" => Playing("Ecco the Dolphin"),
"m_2207_e" => Playing("Flicky"),
"m_9432_e" => Playing("Golden Axe II"),
"m_5015_e" => Playing("Golden Axe"),
"m_5017_e" => Playing("Gunstar Heroes"),
"m_0732_e" => Playing("Altered Beast"),
"m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"),
"m_1654_e" => Playing("Target Earth"),
"m_7050_e" => Playing("Light Crusader"),
"m_5027_e" => Playing("M.U.S.H.A."),
"m_5028_e" => Playing("Phantasy Star IV"),
"m_9155_e" => Playing("Pulseman"),
"m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"),
"m_0098_e" => Playing("Crusader of Centy"),
"m_0098_k" => Playing("신창세기 라그나센티"),
"m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"),
"m_5033_e" => Playing("Ristar"),
"m_1987_e" => Playing("MEGA MAN: THE WILY WARS"),
"m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"),
"m_3353_e" => Playing("Shining Force II"),
"m_5036_e" => Playing("Shining Force"),
"m_9866_e" => Playing("Sonic The Hedgehog Spinball"),
"m_5041_e" => Playing("Sonic The Hedgehog 2"),
"m_5523_e" => Playing("Space Harrier II"),
"m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"),
"m_5044_e" => Playing("STRIDER"),
"m_6353_e" => Playing("Super Fantasy Zone"),
"m_9569_e" => Playing("Beyond Oasis"),
"m_9569_k" => Playing("스토리 오브 도어"),
"m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"),
"m_9569_pf" => Playing("La Légende de Thor"),
"m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"),
"m_6811_e" => Playing("The Revenge of Shinobi"),
"m_4372_e" => Playing("Thunder Force II"),
"m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"),
"m_0432_e" => Playing("ToeJam & Earl"),
"m_5052_e" => Playing("Castlevania: BLOODLINES"),
"m_3626_e" => Playing("VectorMan"),
"m_7955_e" => Playing("Sword of Vermilion"),
"m_0394_e" => Playing("Virtua Fighter 2"),
"m_9417_e" => Playing("Zero Wing"),
#endregion
#region Nintendo 64
"n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"), "n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"),
"n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"), "n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"),
"n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"), "n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"),
@@ -331,11 +396,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
"n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"), "n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"),
"n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"), "n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"),
"n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"), "n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"),
_ => FormattedValue.ForceReset
};
private static FormattedValue NES_LaunchedGame(SingleValue value) => value.Matched.StringValue switch #endregion
{
#region NES
"clv_p_naaae" => Playing("Super Mario Bros.™"), "clv_p_naaae" => Playing("Super Mario Bros.™"),
"clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"), "clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"),
"clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"), "clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"),
@@ -415,11 +480,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
"clv_p_najre" => Playing("COBRA TRIANGLE™"), "clv_p_najre" => Playing("COBRA TRIANGLE™"),
"clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"), "clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"),
"clv_p_najte" => Playing("SOLAR® JETMAN"), "clv_p_najte" => Playing("SOLAR® JETMAN"),
_ => FormattedValue.ForceReset
};
private static FormattedValue SNES_LaunchedGame(SingleValue value) => value.Matched.StringValue switch #endregion
{
#region SNES
"s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"), "s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"),
"s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"), "s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"),
"s_2182_e" => Playing("BIG RUN"), "s_2182_e" => Playing("BIG RUN"),
@@ -490,98 +555,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
"s_2096_e" => Playing("Wrecking Crew™ '98"), "s_2096_e" => Playing("Wrecking Crew™ '98"),
"s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"), "s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"),
"s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"), "s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"),
_ => FormattedValue.ForceReset
};
private static FormattedValue Genesis_LaunchedGame(SingleValue value) => value.Matched.StringValue switch #endregion
{
"m_0054_e" => Playing("Alien Soldier"), #region GameBoy
"m_3978_e" => Playing("Alien Storm"),
"m_5234_e" => Playing("ALISIA DRAGOON"),
"m_5003_e" => Playing("Streets of Rage 2"),
"m_4843_e" => Playing("Kid Chameleon"),
"m_2874_e" => Playing("Columns"),
"m_3167_e" => Playing("Comix Zone"),
"m_5007_e" => Playing("Contra: Hard Corps"),
"m_0865_e" => Playing("Ghouls 'n Ghosts"),
"m_0935_e" => Playing("Dynamite Headdy"),
"m_8314_e" => Playing("Earthworm Jim"),
"m_5012_e" => Playing("Ecco the Dolphin"),
"m_2207_e" => Playing("Flicky"),
"m_9432_e" => Playing("Golden Axe II"),
"m_5015_e" => Playing("Golden Axe"),
"m_5017_e" => Playing("Gunstar Heroes"),
"m_0732_e" => Playing("Altered Beast"),
"m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"),
"m_1654_e" => Playing("Target Earth"),
"m_7050_e" => Playing("Light Crusader"),
"m_5027_e" => Playing("M.U.S.H.A."),
"m_5028_e" => Playing("Phantasy Star IV"),
"m_9155_e" => Playing("Pulseman"),
"m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"),
"m_0098_e" => Playing("Crusader of Centy"),
"m_0098_k" => Playing("신창세기 라그나센티"),
"m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"),
"m_5033_e" => Playing("Ristar"),
"m_1987_e" => Playing("MEGA MAN: THE WILY WARS"),
"m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"),
"m_3353_e" => Playing("Shining Force II"),
"m_5036_e" => Playing("Shining Force"),
"m_9866_e" => Playing("Sonic The Hedgehog Spinball"),
"m_5041_e" => Playing("Sonic The Hedgehog 2"),
"m_5523_e" => Playing("Space Harrier II"),
"m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"),
"m_5044_e" => Playing("STRIDER"),
"m_6353_e" => Playing("Super Fantasy Zone"),
"m_9569_e" => Playing("Beyond Oasis"),
"m_9569_k" => Playing("스토리 오브 도어"),
"m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"),
"m_9569_pf" => Playing("La Légende de Thor"),
"m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"),
"m_6811_e" => Playing("The Revenge of Shinobi"),
"m_4372_e" => Playing("Thunder Force II"),
"m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"),
"m_0432_e" => Playing("ToeJam & Earl"),
"m_5052_e" => Playing("Castlevania: BLOODLINES"),
"m_3626_e" => Playing("VectorMan"),
"m_7955_e" => Playing("Sword of Vermilion"),
"m_0394_e" => Playing("Virtua Fighter 2"),
"m_9417_e" => Playing("Zero Wing"),
_ => FormattedValue.ForceReset
};
private static FormattedValue GBA_LaunchedGame(SingleValue value) => value.Matched.StringValue switch
{
"a_9694_e" => Playing("Densetsu no Starfy 1"),
"a_5600_e" => Playing("Densetsu no Starfy 2"),
"a_7565_e" => Playing("Densetsu no Starfy 3"),
"a_6553_e" => Playing("F-ZERO CLIMAX"),
"a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"),
"a_9283_e" => Playing("F-Zero™ Maximum Velocity"),
"a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"),
"a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"),
"a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"),
"a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"),
"a_7279_p" => Playing("Kuru Kuru Kururin™"),
"a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"),
"a_6845_e" => Playing("Mario Kart™: Super Circuit™"),
"a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"),
"a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"),
"a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"),
"a_9444_e" => Playing("Super Mario™ Advance"),
"a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"),
"a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"),
"a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"),
"a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"),
"a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."),
"a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"),
"a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"),
"a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"),
_ => FormattedValue.ForceReset
};
private static FormattedValue GB_LaunchedGame(SingleValue value) => value.Matched.StringValue switch
{
"c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"), "c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"),
"c_5022_e" => Playing("Blaster Master: Enemy Below"), "c_5022_e" => Playing("Blaster Master: Enemy Below"),
"c_3381_e" => Playing("Game & Watch™ Gallery 3"), "c_3381_e" => Playing("Game & Watch™ Gallery 3"),
@@ -615,6 +593,39 @@ namespace Ryujinx.Ava.Utilities.PlayReport
"d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"), "d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"),
"d_7970_e" => Playing("Super Mario Land™"), "d_7970_e" => Playing("Super Mario Land™"),
"d_8484_e" => Playing("Tetris®"), "d_8484_e" => Playing("Tetris®"),
#endregion
#region GameBoy Advance
"a_9694_e" => Playing("Densetsu no Starfy 1"),
"a_5600_e" => Playing("Densetsu no Starfy 2"),
"a_7565_e" => Playing("Densetsu no Starfy 3"),
"a_6553_e" => Playing("F-ZERO CLIMAX"),
"a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"),
"a_9283_e" => Playing("F-Zero™ Maximum Velocity"),
"a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"),
"a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"),
"a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"),
"a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"),
"a_7279_p" => Playing("Kuru Kuru Kururin™"),
"a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"),
"a_6845_e" => Playing("Mario Kart™: Super Circuit™"),
"a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"),
"a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"),
"a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"),
"a_9444_e" => Playing("Super Mario™ Advance"),
"a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"),
"a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"),
"a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"),
"a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"),
"a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."),
"a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"),
"a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"),
"a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"),
#endregion
_ => FormattedValue.ForceReset _ => FormattedValue.ForceReset
}; };
} }
@@ -1,9 +1,4 @@
using System; namespace Ryujinx.Ava.Utilities.PlayReport
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{ {
public static partial class PlayReports public static partial class PlayReports
{ {
@@ -62,25 +57,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
) )
) )
.AddSpec( .AddSpec(
"0100c9a00ece6000", [
spec => spec.AddValueFormatter("launch_title_id", N64_LaunchedGame) "0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
) "010012f017576000", "0100c62011050000", "0100b3c014bda000"],
.AddSpec( spec => spec.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
"01008d300c50c000",
spec => spec.AddValueFormatter("launch_title_id", SNES_LaunchedGame)
)
.AddSpec(
"0100d870045b6000",
spec => spec.AddValueFormatter("launch_title_id", NES_LaunchedGame)
).AddSpec(
"010012f017576000",
spec => spec.AddValueFormatter("launch_title_id", GBA_LaunchedGame)
).AddSpec(
"0100c62011050000",
spec => spec.AddValueFormatter("launch_title_id", GB_LaunchedGame)
).AddSpec(
"0100b3c014bda000",
spec => spec.AddValueFormatter("launch_title_id", Genesis_LaunchedGame)
); );
private static string Playing(string game) => $"Playing {game}"; private static string Playing(string game) => $"Playing {game}";
+63 -43
View File
@@ -1,5 +1,4 @@
using FluentAvalonia.Core; using MsgPack;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -14,8 +13,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// </summary> /// </summary>
public class GameSpec public class GameSpec
{ {
public static GameSpec Create(string requiredTitleId, params IEnumerable<string> otherTitleIds)
=> new() { TitleIds = otherTitleIds.Prepend(requiredTitleId).ToArray() };
public static GameSpec Create(IEnumerable<string> titleIds)
=> new() { TitleIds = titleIds.ToArray() };
private int _lastPriority; private int _lastPriority;
public required string[] TitleIds { get; init; } public required string[] TitleIds { get; init; }
public List<FormatterSpecBase> ValueFormatters { get; } = []; public List<FormatterSpecBase> ValueFormatters { get; } = [];
@@ -28,8 +33,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKey">The key name to match.</param> /// <param name="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param> /// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(string reportKey, SingleValueFormatter valueFormatter) public GameSpec AddValueFormatter(
=> AddValueFormatter(_lastPriority++, reportKey, valueFormatter); string reportKey,
SingleValueFormatter valueFormatter
) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter);
/// <summary> /// <summary>
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/> /// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -39,15 +46,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKey">The key name to match.</param> /// <param name="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param> /// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(int priority, string reportKey, public GameSpec AddValueFormatter(
SingleValueFormatter valueFormatter) int priority,
string reportKey,
SingleValueFormatter valueFormatter
) => AddValueFormatter(new FormatterSpec
{ {
ValueFormatters.Add(new FormatterSpec Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter
{ });
Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter
});
return this;
}
/// <summary> /// <summary>
/// Add a multi-value formatter to the current <see cref="GameSpec"/> /// Add a multi-value formatter to the current <see cref="GameSpec"/>
@@ -56,8 +62,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param> /// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param> /// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter) public GameSpec AddMultiValueFormatter(
=> AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); string[] reportKeys,
MultiValueFormatter valueFormatter
) => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
/// <summary> /// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/> /// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -67,15 +75,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param> /// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param> /// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys, public GameSpec AddMultiValueFormatter(
MultiValueFormatter valueFormatter) int priority,
string[] reportKeys,
MultiValueFormatter valueFormatter
) => AddValueFormatter(new MultiFormatterSpec
{ {
ValueFormatters.Add(new MultiFormatterSpec Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
{ });
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
});
return this;
}
/// <summary> /// <summary>
/// Add a multi-value formatter to the current <see cref="GameSpec"/> /// Add a multi-value formatter to the current <see cref="GameSpec"/>
@@ -87,8 +94,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param> /// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param> /// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter) public GameSpec AddSparseMultiValueFormatter(
=> AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); string[] reportKeys,
SparseMultiValueFormatter valueFormatter
) => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
/// <summary> /// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/> /// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -101,13 +110,18 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param> /// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param> /// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys, public GameSpec AddSparseMultiValueFormatter(
SparseMultiValueFormatter valueFormatter) int priority,
string[] reportKeys,
SparseMultiValueFormatter valueFormatter
) => AddValueFormatter(new SparseMultiFormatterSpec
{ {
ValueFormatters.Add(new SparseMultiFormatterSpec Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
{ });
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
}); private GameSpec AddValueFormatter<T>(T formatterSpec) where T : FormatterSpecBase
{
ValueFormatters.Add(formatterSpec);
return this; return this;
} }
} }
@@ -138,7 +152,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{ {
List<MessagePackObject> packedObjects = []; List<MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys) foreach (string reportKey in ReportKeys)
{ {
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
{ {
@@ -162,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{ {
Dictionary<string, MessagePackObject> packedObjects = []; Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys) foreach (string reportKey in ReportKeys)
{ {
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue; continue;
@@ -174,16 +188,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport
return true; return true;
} }
} }
public abstract class FormatterSpecBase public abstract class FormatterSpecBase
{ {
public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data); public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data);
public int Priority { get; init; } public int Priority { get; init; }
public string[] ReportKeys { get; init; } public string[] ReportKeys { get; init; }
public Delegate Formatter { get; init; } public Delegate Formatter { get; init; }
public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, out FormattedValue formattedValue) public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
out FormattedValue formattedValue)
{ {
formattedValue = default; formattedValue = default;
if (!GetData(playReport, out object data)) if (!GetData(playReport, out object data))
@@ -197,15 +212,20 @@ namespace Ryujinx.Ava.Utilities.PlayReport
switch (Formatter) switch (Formatter)
{ {
case SingleValueFormatter svf when data is MessagePackObject mpo: case SingleValueFormatter svf when data is MessagePackObject match:
formattedValue = svf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); formattedValue = svf(
new SingleValue(match) { Application = appMeta, PlayReport = playReport }
);
return true; return true;
case MultiValueFormatter mvf when data is List<MessagePackObject> messagePackObjects: case MultiValueFormatter mvf when data is List<MessagePackObject> matches:
formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); formattedValue = mvf(
new MultiValue(matches) { Application = appMeta, PlayReport = playReport }
);
return true; return true;
case SparseMultiValueFormatter smvf when case SparseMultiValueFormatter smvf when data is Dictionary<string, MessagePackObject> sparseMatches:
data is Dictionary<string, MessagePackObject> sparseMessagePackObjects: formattedValue = smvf(
formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); new SparseMultiValue(sparseMatches) { Application = appMeta, PlayReport = playReport }
);
return true; return true;
default: default:
throw new InvalidOperationException("Formatter delegate is not of a known type!"); throw new InvalidOperationException("Formatter delegate is not of a known type!");
+16 -11
View File
@@ -12,7 +12,7 @@ namespace Ryujinx.Ava.Utilities
public static class ShortcutHelper public static class ShortcutHelper
{ {
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath) private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath, string args = "")
{ {
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe"); string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
iconPath += ".ico"; iconPath += ".ico";
@@ -22,13 +22,13 @@ namespace Ryujinx.Ava.Utilities
image.Resize(new SKImageInfo(128, 128), SKFilterQuality.High); image.Resize(new SKImageInfo(128, 128), SKFilterQuality.High);
SaveBitmapAsIcon(image, iconPath); SaveBitmapAsIcon(image, iconPath);
Shortcut shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), iconPath, 0); Shortcut shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId, args), iconPath, 0);
shortcut.StringData.NameString = cleanedAppName; shortcut.StringData.NameString = cleanedAppName;
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
} }
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName) private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName, string args = "")
{ {
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh"); string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
string desktopFile = EmbeddedResources.ReadAllText("Ryujinx/Assets/ShortcutFiles/shortcut-template.desktop"); string desktopFile = EmbeddedResources.ReadAllText("Ryujinx/Assets/ShortcutFiles/shortcut-template.desktop");
@@ -40,11 +40,11 @@ namespace Ryujinx.Ava.Utilities
data.SaveTo(file); data.SaveTo(file);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}"); outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId, args)}");
} }
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName) private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName, string args = "")
{ {
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"); string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
string plistFile = EmbeddedResources.ReadAllText("Ryujinx/Assets/ShortcutFiles/shortcut-template.plist"); string plistFile = EmbeddedResources.ReadAllText("Ryujinx/Assets/ShortcutFiles/shortcut-template.plist");
@@ -63,7 +63,7 @@ namespace Ryujinx.Ava.Utilities
string scriptPath = Path.Combine(scriptFolderPath, ScriptName); string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
using StreamWriter scriptFile = new(scriptPath); using StreamWriter scriptFile = new(scriptPath);
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId)); scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId, args));
// Set execute permission // Set execute permission
FileInfo fileInfo = new(scriptPath); FileInfo fileInfo = new(scriptPath);
@@ -87,7 +87,7 @@ namespace Ryujinx.Ava.Utilities
outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName); outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName);
} }
public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData) public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData, string args = "")
{ {
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars())); string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars()));
@@ -96,7 +96,7 @@ namespace Ryujinx.Ava.Utilities
{ {
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app"); string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath); CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath, args);
return; return;
} }
@@ -106,14 +106,14 @@ namespace Ryujinx.Ava.Utilities
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx"); string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
Directory.CreateDirectory(iconPath); Directory.CreateDirectory(iconPath);
CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName); CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName, args);
return; return;
} }
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
{ {
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName); CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName, args);
return; return;
} }
@@ -121,7 +121,7 @@ namespace Ryujinx.Ava.Utilities
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
} }
private static string GetArgsString(string appFilePath, string applicationId) private static string GetArgsString(string appFilePath, string applicationId, string config = "")
{ {
// args are first defined as a list, for easier adjustments in the future // args are first defined as a list, for easier adjustments in the future
List<string> argsList = []; List<string> argsList = [];
@@ -132,6 +132,11 @@ namespace Ryujinx.Ava.Utilities
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\""); argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
} }
if (!string.IsNullOrEmpty(config))
{
argsList.Add(config);
}
if (appFilePath.ToLower().EndsWith(".xci")) if (appFilePath.ToLower().EndsWith(".xci"))
{ {
argsList.Add("--application-id"); argsList.Add("--application-id");