Compare commits

...

21 Commits

Author SHA1 Message Date
Evan Husted
13388e972a UI: RPC: add image asset for Bluey: The Video Game 2025-02-10 20:00:59 -06:00
Evan Husted
1eb78872d8 misc: chore: annoyed I missed this, it's very obvious 2025-02-10 19:51:28 -06:00
Evan Husted
fe9fe2a10f UI: Added option to check for updates in the background 2025-02-10 19:28:46 -06: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
Evan Husted
efa0cc7554 UI: Show issue labels of games in the tooltip on playability status 2025-02-09 21:15:38 -06:00
Evan Husted
1c0813d09d misc: chore: [ci skip] shorten lines in FormatterSpecBase.Format & consistently format them 2025-02-09 00:50:11 -06: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
shinyoyo
2cd876b1cb Update Zh-CN Simplified Chinese (#642) 2025-02-08 21:33:20 -06:00
Evan Husted
93a298523f Dynamic Presence support for every NSO emulator 2025-02-08 16:52:07 -06:00
FluffyOMC
253cbb2810 Initial Implementation of SSBU PlayReport usage! (#638)
Currently, this has as many game modes as I could find to implement,
along with a list of all the characters in the game and their code ID.
2025-02-08 16:23:24 -06:00
Evan Husted
9c226dcc7a misc: chore: [ci skip] rename ValueFormatter to SingleValueFormatter and some minor cleanups 2025-02-08 01:34:44 -06:00
Evan Husted
30a534edcd misc: chore: [ci skip] generify Formatter Specs to be able to run formatters of different types at interleaving priorities 2025-02-08 01:26:23 -06:00
40 changed files with 1384 additions and 422 deletions

View File

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

View File

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

View File

@@ -144,17 +144,15 @@ namespace ARMeilleure.Translation.PTC
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 (!funcs.Contains(profiledFunc.Key))
{
funcs.Add(profiledFunc.Key);
}
}
if (!funcProfile.Blacklist)
continue;
if (!funcs.Contains(ptr))
funcs.Add(ptr);
}
return funcs;

View File

@@ -164,15 +164,16 @@ namespace Ryujinx.Common
"0100ba0018500000", // Splatoon 3: Splatfest World Premiere
//NSO Membership games
"0100ccf019c8c000", // F-ZERO 99
"0100c62011050000", // GB - Nintendo Switch Online
"010012f017576000", // GBA - Nintendo Switch Online
"0100c9a00ece6000", // N64 - Nintendo Switch Online
"0100e0601c632000", // N64 - Nintendo Switch Online 18+
"0100d870045b6000", // NES - Nintendo Switch Online
"0100b3c014bda000", // SEGA Genesis - Nintendo Switch Online
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100ccf019c8c000", // F-ZERO 99
"0100ad9012510000", // PAC-MAN 99
"010040600c5ce000", // Tetris 99
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100277011f1a000", // Super Mario Bros. 35
//Misc Nintendo 1st party games
@@ -218,6 +219,7 @@ namespace Ryujinx.Common
//Misc Games
"010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance
"01008c2019598000", // Bluey: The Video Game
"0100c6800b934000", // Brawlhalla
"0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ryujinx.HLE.HOS.Applets.Error
{

View File

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

View File

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

View File

@@ -1543,7 +1543,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "由 {0} 开发",
"zh_TW": ""
}
},
@@ -1843,7 +1843,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "兼容性:",
"zh_TW": ""
}
},
@@ -1868,7 +1868,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "标题 ID:",
"zh_TW": ""
}
},
@@ -1893,7 +1893,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "服务的游戏: {0}",
"zh_TW": ""
}
},
@@ -1918,7 +1918,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "在线玩家: {0}",
"zh_TW": ""
}
},
@@ -2268,7 +2268,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "清理 PPTC 缓存",
"zh_TW": ""
}
},
@@ -2293,7 +2293,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "删除应用程序的所有 PPTC 缓存",
"zh_TW": ""
}
},
@@ -2768,7 +2768,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "显示兼容性项目",
"zh_TW": ""
}
},
@@ -2793,7 +2793,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "在兼容性列表中显示选定的游戏,您通常可以通过帮助菜单访问。",
"zh_TW": ""
}
},
@@ -2818,7 +2818,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "显示游戏信息",
"zh_TW": ""
}
},
@@ -2843,7 +2843,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "显示当前选定游戏的状态与详细信息。",
"zh_TW": ""
}
},
@@ -3350,26 +3350,101 @@
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunch",
"Translations": {
"ar_SA": "التحقق من وجود تحديثات عند التشغيل",
"de_DE": "Beim Start nach Updates suchen",
"el_GR": "Έλεγχος για Ενημερώσεις στην Εκκίνηση",
"en_US": "Check for Updates on Launch",
"es_ES": "Buscar actualizaciones al iniciar",
"fr_FR": "Vérifier les mises à jour au démarrage",
"he_IL": "בדוק אם קיימים עדכונים בהפעלה",
"it_IT": "Controlla aggiornamenti all'avvio",
"ja_JP": "起動時にアップデートを確認する",
"ko_KR": "시작 시, 업데이트 확인",
"no_NO": "Se etter oppdateringer ved oppstart",
"pl_PL": "Sprawdzaj aktualizacje przy uruchomieniu",
"pt_BR": "Verificar se há atualizações ao iniciar",
"ru_RU": "Проверять наличие обновлений при запуске",
"sv_SE": "Leta efter uppdatering vid uppstart",
"th_TH": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม",
"tr_TR": "Her Açılışta Güncellemeleri Denetle",
"uk_UA": "Перевіряти наявність оновлень під час запуску",
"zh_CN": "启动时检查更新",
"zh_TW": "啟動時檢查更新"
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Check for Updates:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchOff",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Off",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchPromptAtStartup",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prompt",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabGeneralCheckUpdatesOnLaunchBackground",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Background",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
@@ -4493,7 +4568,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "与系统时间同步",
"zh_TW": ""
}
},
@@ -5747,6 +5822,31 @@
"zh_TW": "啟用客體日誌"
}
},
{
"ID": "SettingsTabLoggingEnableAvaloniaLogs",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Enable UI Logs",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabLoggingEnableFsAccessLogs",
"Translations": {
@@ -6143,7 +6243,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "重置设置",
"zh_TW": ""
}
},
@@ -6168,7 +6268,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "我要重置我的设置。",
"zh_TW": ""
}
},
@@ -8143,7 +8243,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "彩虹滚动速度",
"zh_TW": ""
}
},
@@ -13418,7 +13518,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "您正要清理 PPTC 数据:\n\n{0}\n\n您确实要继续吗?",
"zh_TW": ""
}
},
@@ -16722,6 +16822,31 @@
"zh_TW": "謹慎使用"
}
},
{
"ID": "AvaloniaLogTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prints Avalonia (UI) log messages in the console.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "OpenGlLogLevel",
"Translations": {
@@ -23568,7 +23693,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "启动和游戏时不会出现任何崩溃或任何类型的 GPU bug 且速度足够快可以在一般 PC 上尽情游玩。",
"zh_TW": ""
}
},
@@ -23593,7 +23718,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "可以成功启动并进入游戏但可能会遇到以下一种或多种问题: 崩溃、卡死、GPU bug、令人无法接受的音频,或者只是太慢。仍然可以继续进行游戏,但是可能无法达到预期。",
"zh_TW": ""
}
},
@@ -23618,7 +23743,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "可以启动并通过标题画面但是无法进入到主要的游戏流程。",
"zh_TW": ""
}
},
@@ -23643,7 +23768,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "可以启动但是无法通过标题画面。",
"zh_TW": ""
}
},
@@ -23668,7 +23793,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "无法启动或显示无任何动静。",
"zh_TW": ""
}
},
@@ -23718,7 +23843,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "Rich Presence 图像",
"zh_TW": ""
}
},
@@ -23743,7 +23868,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "动态 Rich Presence",
"zh_TW": ""
}
}

View File

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

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)")]
public string GraphicsShadersDumpPath { get; set; }
[Option("graphics-backend", Required = false, Default = GraphicsBackend.OpenGl, HelpText = "Change Graphics Backend to use.")]
[Option("graphics-backend", Required = false, Default = GraphicsBackend.Vulkan, HelpText = "Change Graphics Backend to use.")]
public GraphicsBackend GraphicsBackend { get; set; }
[Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]

View File

@@ -55,9 +55,21 @@
Tag="{Binding AppData.IdString}"
Text="{Binding AppData.LocalizedStatus}"
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
ToolTip.Tip="{Binding AppData.LocalizedStatusTooltip}"
TextAlignment="Start"
TextWrapping="Wrap" />
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>
<Style Selector="Button">
<Setter Property="MinWidth"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver;
@@ -121,6 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool RememberWindowState { get; set; }
public bool ShowTitleBar { get; set; }
public int HideCursor { get; set; }
public int UpdateCheckerType { get; set; }
public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; }
@@ -204,6 +206,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTrace { get; set; }
public bool EnableGuest { get; set; }
public bool EnableFsAccessLog { get; set; }
public bool EnableAvaloniaLog { get; set; }
public bool EnableDebug { get; set; }
public bool IsOpenAlEnabled { get; set; }
public bool IsSoundIoEnabled { get; set; }
@@ -475,6 +478,7 @@ namespace Ryujinx.Ava.UI.ViewModels
RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@@ -560,6 +564,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableGuest = config.Logger.EnableGuest;
EnableDebug = config.Logger.EnableDebug;
EnableFsAccessLog = config.Logger.EnableFsAccessLog;
EnableAvaloniaLog = config.Logger.EnableAvaloniaLog;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
@@ -580,6 +585,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
if (GameDirectoryChanged)
{
@@ -679,6 +685,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
@@ -400,10 +401,29 @@ namespace Ryujinx.Ava.UI.Windows
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
}
if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate())
if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates)
return;
switch (ConfigurationState.Instance.UpdateCheckerType.Value)
{
await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
case UpdaterType.PromptAtStartup:
await Updater.BeginUpdateAsync()
.Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"));
break;
case UpdaterType.CheckInBackground:
if ((await Updater.CheckVersionAsync()).TryGet(out (Version Current, Version Incoming) versions))
{
string newVersionString = ReleaseInformation.IsCanaryBuild
? $"Canary {versions.Current} -> Canary {versions.Incoming}"
: $"{versions.Current} -> {versions.Incoming}";
if (versions.Current < versions.Incoming)
NotificationHelper.ShowInformation(
title: "Update Available",
text: newVersionString,
onClick: () => _ = Updater.BeginUpdateAsync());
}
break;
}
}

View File

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

View File

@@ -1,3 +1,4 @@
using Gommon;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
@@ -14,6 +15,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using System;
using System.IO;
using System.Text;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary
@@ -32,33 +34,12 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
set
{
_id = value;
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
Compatibility = CompatibilityCsv.Find(Id);
}
}
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
public bool HasPlayabilityInfo => PlayabilityStatus != null;
public string LocalizedStatus =>
PlayabilityStatus.HasValue
? LocaleManager.Instance[PlayabilityStatus!.Value]
: string.Empty;
public LocaleKeys? PlayabilityStatus { get; set; }
public string LocalizedStatusTooltip =>
PlayabilityStatus.HasValue
#pragma warning disable CS8509 // It is exhaustive for any value this property can contain.
? LocaleManager.Instance[PlayabilityStatus!.Value switch
#pragma warning restore CS8509
{
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
}]
: string.Empty;
public int PlayerCount { get; set; }
public int GameCount { get; set; }
@@ -78,11 +59,39 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
public bool HasPlayedPreviously => TimePlayed.TotalSeconds > 1;
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
public Optional<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");
@@ -92,16 +101,16 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
if (!System.IO.Path.Exists(titleFilePath))
{
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
return string.Empty;
}
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();

View File

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

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 63;
public const int CurrentVersion = 65;
/// <summary>
/// Version of the configuration file format
@@ -111,6 +111,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages
/// </summary>
public bool LoggingEnableFsAccessLog { get; set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public bool LoggingEnableAvalonia { get; set; }
/// <summary>
/// Controls which log messages are written to the log targets
@@ -158,9 +163,14 @@ namespace Ryujinx.Ava.Utilities.Configuration
public bool EnableDiscordIntegration { get; set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled
/// DEPRECATED: Checks for updates when Ryujinx starts when enabled
/// </summary>
public bool CheckUpdatesOnStart { get; set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public UpdaterType UpdateCheckerType { get; set; }
/// <summary>
/// Show "Confirm Exit" Dialog

View File

@@ -45,6 +45,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
CheckUpdatesOnStart.Value = cff.CheckUpdatesOnStart;
UpdateCheckerType.Value = cff.UpdateCheckerType;
ShowConfirmExit.Value = cff.ShowConfirmExit;
RememberWindowState.Value = cff.RememberWindowState;
ShowTitleBar.Value = cff.ShowTitleBar;
@@ -430,7 +431,9 @@ namespace Ryujinx.Ava.Utilities.Configuration
}
}),
(62, static cff => cff.RainbowSpeed = 1f),
(63, static cff => cff.MatchSystemTime = false)
(63, static cff => cff.MatchSystemTime = false),
(64, static cff => cff.LoggingEnableAvalonia = false),
(65, static cff => cff.UpdateCheckerType = cff.CheckUpdatesOnStart ? UpdaterType.PromptAtStartup : UpdaterType.Off)
);
}
}

View File

@@ -1,6 +1,7 @@
using ARMeilleure;
using Gommon;
using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@@ -254,6 +255,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Enables printing FS access log messages
/// </summary>
public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
/// <summary>
/// Enables log messages from Avalonia
/// </summary>
public ReactiveObject<bool> EnableAvaloniaLog { get; private set; }
/// <summary>
/// Controls which log messages are written to the log targets
@@ -281,6 +287,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
EnableTrace = new ReactiveObject<bool>();
EnableGuest = new ReactiveObject<bool>();
EnableFsAccessLog = new ReactiveObject<bool>();
EnableAvaloniaLog = new ReactiveObject<bool>();
FilteredClasses = new ReactiveObject<LogClass[]>();
EnableFileLog = new ReactiveObject<bool>();
EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
@@ -761,6 +768,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Checks for updates when Ryujinx starts when enabled
/// </summary>
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
/// </summary>
public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
/// <summary>
/// Show "Confirm Exit" Dialog
@@ -798,6 +810,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hacks = new HacksSection();
EnableDiscordIntegration = new ReactiveObject<bool>();
CheckUpdatesOnStart = new ReactiveObject<bool>();
UpdateCheckerType = new ReactiveObject<UpdaterType>();
ShowConfirmExit = new ReactiveObject<bool>();
RememberWindowState = new ReactiveObject<bool>();
ShowTitleBar = new ReactiveObject<bool>();

View File

@@ -46,6 +46,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
LoggingEnableTrace = Logger.EnableTrace,
LoggingEnableGuest = Logger.EnableGuest,
LoggingEnableFsAccessLog = Logger.EnableFsAccessLog,
LoggingEnableAvalonia = Logger.EnableAvaloniaLog,
LoggingFilteredClasses = Logger.FilteredClasses,
LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel,
SystemLanguage = System.Language,
@@ -55,6 +56,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
DockedMode = System.EnableDockedMode,
EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart,
UpdateCheckerType = UpdateCheckerType,
ShowConfirmExit = ShowConfirmExit,
RememberWindowState = RememberWindowState,
ShowTitleBar = ShowTitleBar,
@@ -165,6 +167,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Logger.EnableTrace.Value = false;
Logger.EnableGuest.Value = true;
Logger.EnableFsAccessLog.Value = false;
Logger.EnableAvaloniaLog.Value = false;
Logger.FilteredClasses.Value = [];
Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None;
System.Language.Value = Language.AmericanEnglish;
@@ -173,7 +176,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.SystemTimeOffset.Value = 0;
System.EnableDockedMode.Value = true;
EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true;
UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
ShowConfirmExit.Value = true;
RememberWindowState.Value = true;
ShowTitleBar.Value = !OperatingSystem.IsWindows();

View File

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

View File

@@ -1,5 +1,4 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
@@ -31,8 +30,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new GameSpec { TitleIds = [titleId] }));
return this;
return AddSpec(transform(GameSpec.Create(titleId)));
}
/// <summary>
@@ -46,8 +44,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform));
return this;
return AddSpec(GameSpec.Create(titleId).Apply(transform));
}
/// <summary>
@@ -63,8 +60,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
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)}.");
_specs.Add(transform(new GameSpec { TitleIds = [..tids] }));
return this;
return AddSpec(transform(GameSpec.Create(tids)));
}
/// <summary>
@@ -79,7 +75,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport
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)}.");
_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;
}
@@ -103,56 +109,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
return FormattedValue.Unhandled;
foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority))
{
if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
if (!formatSpec.Format(appMeta, playReport, out FormattedValue value))
continue;
return formatSpec.Formatter(new SingleValue(valuePackObject)
{
Application = appMeta,
PlayReport = playReport
});
}
foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
{
List<MessagePackObject> packedObjects = [];
foreach (var reportKey in formatSpec.ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(valuePackObject);
}
if (packedObjects.Count != formatSpec.ReportKeys.Length)
return FormattedValue.Unhandled;
return formatSpec.Formatter(new MultiValue(packedObjects)
{
Application = appMeta,
PlayReport = playReport
});
}
foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
{
Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in formatSpec.ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(reportKey, valuePackObject);
}
return formatSpec.Formatter(
new SparseMultiValue(packedObjects)
{
Application = appMeta,
PlayReport = playReport
});
return value;
}
return FormattedValue.Unhandled;

View File

@@ -10,7 +10,7 @@
/// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary>
public delegate FormattedValue ValueFormatter(SingleValue value);
public delegate FormattedValue SingleValueFormatter(SingleValue value);
/// <summary>
/// The delegate type that powers multiple value formatters.<br/>

View File

@@ -1,13 +1,12 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public abstract class MatchedValue<T>
{
public MatchedValue(T matched)
protected MatchedValue(T matched)
{
Matched = matched;
}
@@ -29,7 +28,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
}
/// <summary>
/// The input data to a <see cref="ValueFormatter"/>,
/// The input data to a <see cref="SingleValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary>
@@ -38,8 +37,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public SingleValue(Value matched) : base(matched)
{
}
public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo);
}
/// <summary>
@@ -56,9 +53,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public MultiValue(IEnumerable<MessagePackObject> matched) : base(Value.ConvertPackedObjects(matched))
{
}
public static implicit operator MultiValue(List<MessagePackObject> matched)
=> new(matched.Select(x => new Value(x)).ToArray());
}
/// <summary>
@@ -75,13 +69,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public SparseMultiValue(Dictionary<string, MessagePackObject> matched) : base(Value.ConvertPackedObjectMap(matched))
{
}
public static implicit operator SparseMultiValue(Dictionary<string, MessagePackObject> matched)
=> new(matched
.ToDictionary(
x => x.Key,
x => new Value(x.Value)
)
);
}
}

View File

@@ -0,0 +1,632 @@
using Gommon;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public partial class PlayReports
{
private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) =>
value.Matched.DoubleValue switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
=> value.Matched.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => FormattedValue.ForceReset
};
private static FormattedValue PokemonSVUnionCircle(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
private static FormattedValue PokemonSVArea(SingleValue value)
=> value.Matched.StringValue switch
{
// Base Game Locations
"a_w01" => "South Area One",
"a_w02" => "Mesagoza",
"a_w03" => "The Pokemon League",
"a_w04" => "South Area Two",
"a_w05" => "South Area Four",
"a_w06" => "South Area Six",
"a_w07" => "South Area Five",
"a_w08" => "South Area Three",
"a_w09" => "West Area One",
"a_w10" => "Asado Desert",
"a_w11" => "West Area Two",
"a_w12" => "Medali",
"a_w13" => "Tagtree Thicket",
"a_w14" => "East Area Three",
"a_w15" => "Artazon",
"a_w16" => "East Area Two",
"a_w18" => "Casseroya Lake",
"a_w19" => "Glaseado Mountain",
"a_w20" => "North Area Three",
"a_w21" => "North Area One",
"a_w22" => "North Area Two",
"a_w23" => "The Great Crater of Paldea",
"a_w24" => "South Paldean Sea",
"a_w25" => "West Paldean Sea",
"a_w26" => "East Paldean Sea",
"a_w27" => "Nouth Paldean Sea",
//TODO DLC Locations
_ => FormattedValue.ForceReset
};
private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values)
{
// Check if the PlayReport is for a challenger approach or an achievement.
if (values.Matched.TryGetValue("fighter", out Value fighter) && values.Matched.ContainsKey("reason"))
{
return $"Challenger Approaches - {SuperSmashBrosUltimate_Character(fighter)}";
}
if (values.Matched.TryGetValue("fighter", out fighter) && values.Matched.ContainsKey("challenge_count"))
{
return $"Fighter Unlocked - {SuperSmashBrosUltimate_Character(fighter)}";
}
if (values.Matched.TryGetValue("anniversary", out Value anniversary))
{
return $"Achievement Unlocked - ID: {anniversary}";
}
if (values.Matched.ContainsKey("adv_slot"))
{
return
"Playing Adventure Mode"; // Doing this as it can be a placeholder until we can grab the character.
}
// Check if we have a match_mode at this point, if not, go to default.
if (!values.Matched.TryGetValue("match_mode", out Value matchMode))
{
return "Smashing";
}
return matchMode.BoxedValue switch
{
0 when values.Matched.TryGetValue("player_1_fighter", out Value player) &&
values.Matched.TryGetValue("player_2_fighter", out Value challenger)
=> $"Last Smashed: {SuperSmashBrosUltimate_Character(challenger)}'s Fighter Challenge - {SuperSmashBrosUltimate_Character(player)}",
1 => $"Last Smashed: Normal Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
2 when values.Matched.TryGetValue("player_1_rank", out Value team)
=> team.BoxedValue is 0
? "Last Smashed: Squad Strike - Red Team Wins"
: "Last Smashed: Squad Strike - Blue Team Wins",
3 => $"Last Smashed: Custom Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
4 => $"Last Smashed: Super Sudden Death - {SuperSmashBrosUltimate_PlayerListing(values)}",
5 => $"Last Smashed: Smashdown - {SuperSmashBrosUltimate_PlayerListing(values)}",
6 => $"Last Smashed: Tourney Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
7 when values.Matched.TryGetValue("player_1_fighter", out Value player)
=> $"Last Smashed: Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}",
8 when values.Matched.TryGetValue("player_1_fighter", out Value player)
=> $"Playing Adventure Mode as {SuperSmashBrosUltimate_Character(player)}",
10 when values.Matched.TryGetValue("match_submode", out Value battle) &&
values.Matched.TryGetValue("player_1_fighter", out Value player)
=> $"Last Smashed: Classic Mode, Battle {(int)battle.BoxedValue + 1}/8 as {SuperSmashBrosUltimate_Character(player)}",
12 => $"Last Smashed: Century Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
13 => $"Last Smashed: All-Star Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
14 => $"Last Smashed: Cruel Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
15 when values.Matched.TryGetValue("player_1_fighter", out Value player)
=> $"Last Smashed: Home-Run Contest - {SuperSmashBrosUltimate_Character(player)}",
16 when values.Matched.TryGetValue("player_1_fighter", out Value player1) &&
values.Matched.TryGetValue("player_2_fighter", out Value player2)
=> $"Last Smashed: Home-Run Content (Co-op) - {SuperSmashBrosUltimate_Character(player1)} and {SuperSmashBrosUltimate_Character(player2)}",
17 => $"Last Smashed: Home-Run Contest (Versus) - {SuperSmashBrosUltimate_PlayerListing(values)}",
18 when values.Matched.TryGetValue("player_1_fighter", out Value player1) &&
values.Matched.TryGetValue("player_2_fighter", out Value player2)
=> $"Fresh out of Training mode - {SuperSmashBrosUltimate_Character(player1)} with {SuperSmashBrosUltimate_Character(player2)}",
58 => $"Last Smashed: LDN Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
63 when values.Matched.TryGetValue("player_1_fighter", out Value player)
=> $"Last Smashed: DLC Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}",
_ => "Smashing"
};
}
private static string SuperSmashBrosUltimate_Character(Value value) =>
BinaryPrimitives.ReverseEndianness(
BitConverter.ToInt64(((MsgPack.MessagePackExtendedTypeObject)value.BoxedValue).GetBody(), 0)) switch
{
0x0 => "Mario",
0x1 => "Donkey Kong",
0x2 => "Link",
0x3 => "Samus",
0x4 => "Dark Samus",
0x5 => "Yoshi",
0x6 => "Kirby",
0x7 => "Fox",
0x8 => "Pikachu",
0x9 => "Luigi",
0xA => "Ness",
0xB => "Captain Falcon",
0xC => "Jigglypuff",
0xD => "Peach",
0xE => "Daisy",
0xF => "Bowser",
0x10 => "Ice Climbers",
0x11 => "Sheik",
0x12 => "Zelda",
0x13 => "Dr. Mario",
0x14 => "Pichu",
0x15 => "Falco",
0x16 => "Marth",
0x17 => "Lucina",
0x18 => "Young Link",
0x19 => "Ganondorf",
0x1A => "Mewtwo",
0x1B => "Roy",
0x1C => "Chrom",
0x1D => "Mr Game & Watch",
0x1E => "Meta Knight",
0x1F => "Pit",
0x20 => "Dark Pit",
0x21 => "Zero Suit Samus",
0x22 => "Wario",
0x23 => "Snake",
0x24 => "Ike",
0x25 => "Pokémon Trainer",
0x26 => "Diddy Kong",
0x27 => "Lucas",
0x28 => "Sonic",
0x29 => "King Dedede",
0x2A => "Olimar",
0x2B => "Lucario",
0x2C => "R.O.B.",
0x2D => "Toon Link",
0x2E => "Wolf",
0x2F => "Villager",
0x30 => "Mega Man",
0x31 => "Wii Fit Trainer",
0x32 => "Rosalina & Luma",
0x33 => "Little Mac",
0x34 => "Greninja",
0x35 => "Palutena",
0x36 => "Pac-Man",
0x37 => "Robin",
0x38 => "Shulk",
0x39 => "Bowser Jr.",
0x3A => "Duck Hunt",
0x3B => "Ryu",
0x3C => "Ken",
0x3D => "Cloud",
0x3E => "Corrin",
0x3F => "Bayonetta",
0x40 => "Richter",
0x41 => "Inkling",
0x42 => "Ridley",
0x43 => "King K. Rool",
0x44 => "Simon",
0x45 => "Isabelle",
0x46 => "Incineroar",
0x47 => "Mii Brawler",
0x48 => "Mii Swordfighter",
0x49 => "Mii Gunner",
0x4A => "Piranha Plant",
0x4B => "Joker",
0x4C => "Hero",
0x4D => "Banjo",
0x4E => "Terry",
0x4F => "Byleth",
0x50 => "Min Min",
0x51 => "Steve",
0x52 => "Sephiroth",
0x53 => "Pyra/Mythra",
0x54 => "Kazuya",
0x55 => "Sora",
0xFE => "Random",
0xFF => "Scripted Entity",
_ => "Unknown"
};
private static string SuperSmashBrosUltimate_PlayerListing(SparseMultiValue values)
{
List<(string Character, int PlayerNumber, int? Rank)> players = [];
foreach (KeyValuePair<string, Value> player in values.Matched)
{
if (player.Key.StartsWith("player_") && player.Key.EndsWith("_fighter") &&
player.Value.BoxedValue is not null)
{
if (!int.TryParse(player.Key.Split('_')[1], out int playerNumber))
continue;
string character = SuperSmashBrosUltimate_Character(player.Value);
int? rank = values.Matched.TryGetValue($"player_{playerNumber}_rank", out Value rankValue)
? rankValue.IntValue
: null;
players.Add((character, playerNumber, rank));
}
}
players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList();
return players.Count > 4
? $"{players.Count} Players - {
players.Take(3)
.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
.JoinToString(", ")
}"
: players
.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
.JoinToString(", ");
string RankMedal(int? rank) => rank switch
{
0 => "🥇",
1 => "🥈",
2 => "🥉",
_ => ""
};
}
private static FormattedValue NsoEmulator_LaunchedGame(SingleValue value) => value.Matched.StringValue switch
{
#region SEGA Genesis
"m_0054_e" => Playing("Alien Soldier"),
"m_3978_e" => Playing("Alien Storm"),
"m_5234_e" => Playing("ALISIA DRAGOON"),
"m_5003_e" => Playing("Streets of Rage 2"),
"m_4843_e" => Playing("Kid Chameleon"),
"m_2874_e" => Playing("Columns"),
"m_3167_e" => Playing("Comix Zone"),
"m_5007_e" => Playing("Contra: Hard Corps"),
"m_0865_e" => Playing("Ghouls 'n Ghosts"),
"m_0935_e" => Playing("Dynamite Headdy"),
"m_8314_e" => Playing("Earthworm Jim"),
"m_5012_e" => Playing("Ecco the Dolphin"),
"m_2207_e" => Playing("Flicky"),
"m_9432_e" => Playing("Golden Axe II"),
"m_5015_e" => Playing("Golden Axe"),
"m_5017_e" => Playing("Gunstar Heroes"),
"m_0732_e" => Playing("Altered Beast"),
"m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"),
"m_1654_e" => Playing("Target Earth"),
"m_7050_e" => Playing("Light Crusader"),
"m_5027_e" => Playing("M.U.S.H.A."),
"m_5028_e" => Playing("Phantasy Star IV"),
"m_9155_e" => Playing("Pulseman"),
"m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"),
"m_0098_e" => Playing("Crusader of Centy"),
"m_0098_k" => Playing("신창세기 라그나센티"),
"m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"),
"m_5033_e" => Playing("Ristar"),
"m_1987_e" => Playing("MEGA MAN: THE WILY WARS"),
"m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"),
"m_3353_e" => Playing("Shining Force II"),
"m_5036_e" => Playing("Shining Force"),
"m_9866_e" => Playing("Sonic The Hedgehog Spinball"),
"m_5041_e" => Playing("Sonic The Hedgehog 2"),
"m_5523_e" => Playing("Space Harrier II"),
"m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"),
"m_5044_e" => Playing("STRIDER"),
"m_6353_e" => Playing("Super Fantasy Zone"),
"m_9569_e" => Playing("Beyond Oasis"),
"m_9569_k" => Playing("스토리 오브 도어"),
"m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"),
"m_9569_pf" => Playing("La Légende de Thor"),
"m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"),
"m_6811_e" => Playing("The Revenge of Shinobi"),
"m_4372_e" => Playing("Thunder Force II"),
"m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"),
"m_0432_e" => Playing("ToeJam & Earl"),
"m_5052_e" => Playing("Castlevania: BLOODLINES"),
"m_3626_e" => Playing("VectorMan"),
"m_7955_e" => Playing("Sword of Vermilion"),
"m_0394_e" => Playing("Virtua Fighter 2"),
"m_9417_e" => Playing("Zero Wing"),
#endregion
#region Nintendo 64
"n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"),
"n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"),
"n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"),
"n_3083_e" or "n_3083_p" => Playing("Blast Corps"),
"n_3007_e" => Playing("Dr. Mario™ 64"),
"n_4238_e" => Playing("Excitebike™ 64"),
"n_1870_e" => Playing("Extreme G"),
"n_2456_e" => Playing("F-Zero™ X"),
"n_4631_e" => Playing("GoldenEye 007"),
"n_1635_e" => Playing("Harvest Moon 64"),
"n_2225_e" => Playing("Iggys Reckin Balls"),
"n_1625_e" or "n_1625_p" => Playing("JET FORCE GEMINI™"),
"n_3052_e" => Playing("Kirby 64™: The Crystal Shards"),
"n_4371_e" => Playing("Mario Golf™"),
"n_3013_e" => Playing("Mario Kart™ 64"),
"n_1053_e" or "n_1053_p" => Playing("Mario Party™ 2"),
"n_2965_e" or "n_2965_p" => Playing("Mario Party™ 3"),
"n_4737_e" or "n_4737_p" => Playing("Mario Party™"),
"n_3017_e" => Playing("Mario Tennis™"),
"n_2992_e" or "n_2992_p" => Playing("Paper Mario™"),
"n_3783_e" or "n_3783_p" => Playing("Pilotwings™ 64"),
"n_1848_e" or "n_1848_pd" or "n_1848_pf" => Playing("Pokémon™ Puzzle League"),
"n_3240_e" or "n_3240_pd" or "n_3240_pf" or "n_3240_pi" or "n_3240_ps" => Playing("Pokémon Snap™"),
"n_4590_e" or "n_4590_pd" or "n_4590_pf" or "n_4590_pi" or "n_4590_ps" => Playing("Pokémon Stadium™"),
"n_3309_e" or "n_3309_pd" or "n_3309_pf" or "n_3309_pi" or "n_3309_ps" => Playing("Pokémon Stadium 2™"),
"n_3029_e" => Playing("Sin & Punishment™"),
"n_3030_e" => Playing("Star Fox™ 64"),
"n_3030_p" => Playing("Lylat Wars™"),
"n_3031_e" or "n_3031_p" => Playing("Super Mario 64™"),
"n_4813_e" or "n_4813_p" => Playing("Wave Race™ 64"),
"n_3034_e" => Playing("WIN BACK: COVERT OPERATIONS"),
"n_3034_p" => Playing("OPERATION: WIN BACK"),
"n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"),
"n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"),
"n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"),
#endregion
#region NES
"clv_p_naaae" => Playing("Super Mario Bros.™"),
"clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"),
"clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"),
"clv_p_naade" => Playing("Super Mario Bros.™ 2"),
"clv_p_naaee" => Playing("Donkey Kong™"),
"clv_p_naafe" => Playing("Donkey Kong Jr.™"),
"clv_p_naage" => Playing("Donkey Kong™ 3"),
"clv_p_naahe" => Playing("Excitebike™"),
"clv_p_naaje" => Playing("EarthBound Beginnings"),
"clv_p_naame" => Playing("NES™ Open Tournament Golf"),
"clv_p_naane" or "clv_p_naane_sp1" => Playing("The Legend of Zelda™"),
"clv_p_naape" or "clv_p_naape_sp1" => Playing("Kirby's Adventure™"),
"clv_p_naaqe" or "clv_p_naaqe_sp1" or "clv_p_naaqe_sp2" => Playing("Metroid™"),
"clv_p_naare" => Playing("Balloon Fight™"),
"clv_p_naase" or "clv_p_naase_sp1" => Playing("Zelda II - The Adventure of Link™"),
"clv_p_naate" => Playing("Punch-Out!!™ Featuring Mr. Dream"),
"clv_p_naaue" => Playing("Ice Climber™"),
"clv_p_naave" or "clv_p_naave_sp1" => Playing("Kid Icarus™"),
"clv_p_naawe" => Playing("Mario Bros.™"),
"clv_p_naaxe" or "clv_p_naaxe_sp1" => Playing("Dr. Mario™"),
"clv_p_naaye" => Playing("Yoshi™"),
"clv_p_naaze" => Playing("StarTropics™"),
"clv_p_nabce" or "clv_p_nabce_sp1" => Playing("Ghosts'n Goblins™"),
"clv_p_nabre" or "clv_p_nabre_sp1" or "clv_p_nabre_sp2" => Playing("Gradius"),
"clv_p_nacbe" or "clv_p_nacbe_sp1" => Playing("Ninja Gaiden"),
"clv_p_nacce" => Playing("Solomon's Key"),
"clv_p_nacde" => Playing("Tecmo Bowl"),
"clv_p_nacfe" => Playing("Double Dragon"),
"clv_p_nache" => Playing("Double Dragon II: The Revenge"),
"clv_p_nacje" => Playing("River City Ransom"),
"clv_p_nacke" => Playing("Super Dodge Ball"),
"clv_p_nacle" => Playing("Downtown Nekketsu March Super-Awesome Field Day!"),
"clv_p_nacpe" => Playing("The Mystery of Atlantis"),
"clv_p_nacre" => Playing("Soccer"),
"clv_p_nacse" or "clv_p_nacse_sp1" => Playing("Ninja JaJaMaru-kun"),
"clv_p_nacte" => Playing("Ice Hockey"),
"clv_p_nacue" or "clv_p_nacue_sp1" => Playing("Blaster Master"),
"clv_p_nacwe" => Playing("ADVENTURES OF LOLO"),
"clv_p_nacxe" => Playing("Wario's Woods™"),
"clv_p_nacye" => Playing("Tennis"),
"clv_p_nacze" => Playing("Wrecking Crew™"),
"clv_p_nadbe" => Playing("Joy Mech Fight™"),
"clv_p_nadde" or "clv_p_nadde_sp1" => Playing("Star Soldier"),
"clv_p_nadke" => Playing("Tetris®"),
"clv_p_nadle" => Playing("Pro Wrestling"),
"clv_p_nadpe" => Playing("Baseball"),
"clv_p_nadte" or "clv_p_nadte_sp1" => Playing("TwinBee"),
"clv_p_nadue" or "clv_p_nadue_sp1" => Playing("Mighty Bomb Jack"),
"clv_p_nadve" => Playing("Kung-Fu Heroes"),
"clv_p_nadxe" => Playing("City Connection"),
"clv_p_nadye" => Playing("Rygar"),
"clv_p_naeae" => Playing("Crystalis"),
"clv_p_naece" => Playing("Vice: Project Doom"),
"clv_p_naehe" => Playing("Clu Clu Land™"),
"clv_p_naeie" => Playing("VS. Excitebike™"),
"clv_p_naeje" => Playing("Volleyball™"),
"clv_p_naeke" => Playing("JOURNEY TO SILIUS"),
"clv_p_naele" => Playing("S.C.A.T.: Special Cybernetic Attack Team"),
"clv_p_naeme" => Playing("Shadow of the Ninja"),
"clv_p_naene" => Playing("Nightshade"),
"clv_p_naepe" => Playing("The Immortal"),
"clv_p_naeqe" => Playing("Eliminator Boat Duel"),
"clv_p_naere" => Playing("Fire 'n Ice"),
"clv_p_nafce" => Playing("XEVIOUS"),
"clv_p_nagpe" => Playing("DAIVA STORY 6 IMPERIAL OF NIRSARTIA"),
"clv_p_nagqe" => Playing("DIG DUGⅡ"),
"clv_p_nague" => Playing("MAPPY-LAND"),
"clv_p_nahhe" => Playing("Mach Rider™"),
"clv_p_nahje" => Playing("Pinball"),
"clv_p_nahre" => Playing("Mystery Tower"),
"clv_p_nahte" => Playing("Urban Champion™"),
"clv_p_nahue" => Playing("Donkey Kong Jr.™ Math"),
"clv_p_nahve" => Playing("The Mysterious Murasame Castle"),
"clv_p_najae" => Playing("DEVIL WORLD™"),
"clv_p_najbe" => Playing("Golf"),
"clv_p_najpe" => Playing("R.C. PRO-AM™"),
"clv_p_najre" => Playing("COBRA TRIANGLE™"),
"clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"),
"clv_p_najte" => Playing("SOLAR® JETMAN"),
#endregion
#region SNES
"s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"),
"s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"),
"s_2182_e" => Playing("BIG RUN"),
"s_2156_e" => Playing("Bombuzal"),
"s_2002_e" => Playing("BRAWL BROTHERS"),
"s_2025_e" => Playing("Breath of Fire II"),
"s_2003_e" => Playing("Breath Of Fire"),
"s_2163_e" => Playing("Claymates"),
"s_2150_e" => Playing("Congo's Caper"),
"s_2171_e" => Playing("COSMO GANG THE PUZZLE"),
"s_2004_e" => Playing("Demon's Crest"),
"s_2026_e" => Playing("Kunio-kun no Dodgeball da yo Zen'in Shūgō!"),
"s_2060_e" => Playing("Donkey Kong Country 2: Diddy's Kong Quest"),
"s_2061_e" => Playing("Donkey Kong Country 3: Dixie Kong's Double Trouble!"),
"s_2055_e" => Playing("Donkey Kong Country"),
"s_2139_e" => Playing("DOOMSDAY WARRIOR"),
"s_2051_e" => Playing("EarthBound"),
"s_2162_e" => Playing("Earthworm Jim™ 2"),
"s_2005_e" => Playing("F-ZERO™"),
"s_2183_e" => Playing("FATAL FURY 2"),
"s_2174_e" => Playing("Fighter's History"),
"s_2037_e" => Playing("Harvest Moon"),
"s_2161_e" => Playing("Jelly Boy"),
"s_2006_e" => Playing("Joe & Mac 2: Lost in the Tropics"),
"s_2169_e" => Playing("Caveman Ninja"),
"s_2181_e" => Playing("KILLER INSTINCT™"),
"s_2029_e" or "s_2029_e_sp1" => Playing("Kirby Super Star™"),
"s_2121_e" => Playing("Kirby's Avalanche™"),
"s_2007_e" or "s_2007_e_sp1" => Playing("Kirby's Dream Course™"),
"s_2008_e" or "s_2008_e_sp1" => Playing("Kirby's Dream Land™ 3"),
"s_2172_e" => Playing("Kirbys Star Stacker™"),
"s_2151_e" => Playing("Magical Drop2"),
"s_2044_e" => Playing("Mario's Super Picross"),
"s_2038_e" => Playing("Natsume Championship Wrestling"),
"s_2140_e" => Playing("Operation Logic Bomb"),
"s_2034_e" => Playing("Panel de Pon"),
"s_2009_e" => Playing("Pilotwings™"),
"s_2010_e" => Playing("Pop'n TwinBee"),
"s_2157_e" => Playing("Prehistorik Man"),
"s_2145_e" => Playing("Psycho Dream"),
"s_2141_e" => Playing("Rival Turf!"),
"s_2152_e" => Playing("SIDE POCKET"),
"s_2158_e" => Playing("Spankys™ Quest"),
"s_2031_e" => Playing("Star Fox™ 2"),
"s_2011_e" => Playing("Star Fox™"),
"s_2012_e" => Playing("Stunt Race FX™"),
"s_2032_e" => Playing("Amazing Hebereke"),
"s_2159_e" => Playing("Super Baseball Simulator 1.000"),
"s_2013_e" => Playing("SUPER E.D.F. EARTH DEFENSE FORCE"),
"s_2014_e" => Playing("Smash Tennis"),
"s_2015_e" => Playing("Super Ghouls'n Ghosts™"),
"s_2033_e" => Playing("Super Mario All-Stars™"),
"s_2016_e" or "s_2016_e_sp1" => Playing("Super Mario Kart™"),
"s_2017_e" or "s_2017_e_sp1" => Playing("Super Mario World™"),
"s_2018_e" or "s_2018_e_sp1" => Playing("Super Metroid™"),
"s_2184_e" => Playing("Super Ninja Boy"),
"s_2019_e" or "s_2019_e_sp1" => Playing("Super Punch-Out!!™"),
"s_2020_e" => Playing("Super Puyo Puyo 2"),
"s_2133_e" => Playing("SUPER R-TYPE"),
"s_2021_e" => Playing("Super Soccer"),
"s_2022_e" => Playing("Super Tennis"),
"s_2136_e" => Playing("Sutte Hakkun"),
"s_2142_e" => Playing("The Ignition Factor"),
"s_2143_e" => Playing("The Peace Keepers"),
"s_2146_e" => Playing("Tuff E Nuff"),
"s_2144_e" => Playing("SUPER VALIS Ⅳ"),
"s_2049_e" => Playing("Wild Guns"),
"s_2096_e" => Playing("Wrecking Crew™ '98"),
"s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"),
"s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"),
#endregion
#region GameBoy
"c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"),
"c_5022_e" => Playing("Blaster Master: Enemy Below"),
"c_3381_e" => Playing("Game & Watch™ Gallery 3"),
"c_0282_e" => Playing("Kirby Tilt n Tumble™"),
"c_4471_e" or "c_4471_p" => Playing("Mario Golf™"),
"c_9947_e" => Playing("Mario Tennis™"),
"c_3191_e" or "c_3191_p" or "c_3191_x" => Playing("Pokémon™ Trading Card Game"),
"c_8914_e" or "c_8914_p" => Playing("Quest for Camelot™"),
"c_2648_e" => Playing("Tetris® DX"),
"c_5928_e" => Playing("Wario Land™ 3"),
"c_3996_e" or "c_3996_pd" or "c_3996_pf" => Playing("The Legend of Zelda™: Link's Awakening DX™"),
"c_8852_e" or "c_8852_p" => Playing("The Legend of Zelda™: Oracle of Ages™"),
"c_9130_e" or "c_9130_p" => Playing("The Legend of Zelda™: Oracle of Seasons™"),
"d_6879_e" => Playing("Alleyway™"),
"d_7618_e" => Playing("Baseball"),
"d_6005_e" => Playing("BurgerTime Deluxe"),
"d_7120_e" => Playing("Castlevania Legends"),
"d_2744_e" => Playing("Dr. Mario™"),
"d_1593_e" => Playing("Donkey Kong Land 2™"),
"d_7216_e" => Playing("Donkey Kong Land III™"),
"d_4971_e" => Playing("Donkey Kong Land™"),
"d_7984_e" => Playing("GARGOYLE'S QUEST"),
"d_8212_e" => Playing("Kirby's Dream Land™ 2"),
"d_5661_e" => Playing("Kirby's Dream Land™"),
"d_3837_e" => Playing("MEGA MAN II"),
"d_1965_e" => Playing("MEGA MAN III"),
"d_0194_e" => Playing("MEGA MAN IV"),
"d_1425_e" => Playing("MEGA MAN V"),
"d_9324_e" => Playing("MEGA MAN: DR. WILY'S REVENGE"),
"d_1577_e" => Playing("Metroid™ II - Return of Samus™"),
"d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"),
"d_7970_e" => Playing("Super Mario Land™"),
"d_8484_e" => Playing("Tetris®"),
#endregion
#region GameBoy Advance
"a_9694_e" => Playing("Densetsu no Starfy 1"),
"a_5600_e" => Playing("Densetsu no Starfy 2"),
"a_7565_e" => Playing("Densetsu no Starfy 3"),
"a_6553_e" => Playing("F-ZERO CLIMAX"),
"a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"),
"a_9283_e" => Playing("F-Zero™ Maximum Velocity"),
"a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"),
"a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"),
"a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"),
"a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"),
"a_7279_p" => Playing("Kuru Kuru Kururin™"),
"a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"),
"a_6845_e" => Playing("Mario Kart™: Super Circuit™"),
"a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"),
"a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"),
"a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"),
"a_9444_e" => Playing("Super Mario™ Advance"),
"a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"),
"a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"),
"a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"),
"a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"),
"a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."),
"a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"),
"a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"),
"a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"),
#endregion
_ => FormattedValue.ForceReset
};
}
}

View File

@@ -1,6 +1,6 @@
namespace Ryujinx.Ava.Utilities.PlayReport
{
public static class PlayReports
public static partial class PlayReports
{
public static Analyzer Analyzer { get; } = new Analyzer()
.AddSpec(
@@ -37,91 +37,32 @@
spec => spec
.AddValueFormatter("area_no", PokemonSVArea)
.AddValueFormatter("team_circle", PokemonSVUnionCircle)
)
.AddSpec(
"01006a800016e000",
spec => spec
.AddSparseMultiValueFormatter(
[
// Metadata to figure out what PlayReport we have.
"match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count",
"adv_slot",
// List of Fighters
"player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter",
"player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter",
// List of rankings/placements
"player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank",
"player_6_rank", "player_7_rank", "player_8_rank"
],
SuperSmashBrosUltimate_Mode
)
)
.AddSpec(
[
"0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
"010012f017576000", "0100c62011050000", "0100b3c014bda000"],
spec => spec.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
);
private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) =>
value.Matched.DoubleValue switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
=> value.Matched.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => FormattedValue.ForceReset
};
private static FormattedValue PokemonSVUnionCircle(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
private static FormattedValue PokemonSVArea(SingleValue value)
=> value.Matched.StringValue switch
{
// Base Game Locations
"a_w01" => "South Area One",
"a_w02" => "Mesagoza",
"a_w03" => "The Pokemon League",
"a_w04" => "South Area Two",
"a_w05" => "South Area Four",
"a_w06" => "South Area Six",
"a_w07" => "South Area Five",
"a_w08" => "South Area Three",
"a_w09" => "West Area One",
"a_w10" => "Asado Desert",
"a_w11" => "West Area Two",
"a_w12" => "Medali",
"a_w13" => "Tagtree Thicket",
"a_w14" => "East Area Three",
"a_w15" => "Artazon",
"a_w16" => "East Area Two",
"a_w18" => "Casseroya Lake",
"a_w19" => "Glaseado Mountain",
"a_w20" => "North Area Three",
"a_w21" => "North Area One",
"a_w22" => "North Area Two",
"a_w23" => "The Great Crater of Paldea",
"a_w24" => "South Paldean Sea",
"a_w25" => "West Paldean Sea",
"a_w26" => "East Paldean Sea",
"a_w27" => "Nouth Paldean Sea",
//TODO DLC Locations
_ => FormattedValue.ForceReset
};
private static string Playing(string game) => $"Playing {game}";
}
}

View File

@@ -1,4 +1,6 @@
using FluentAvalonia.Core;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,10 +13,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// </summary>
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;
public required string[] TitleIds { get; init; }
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
public List<SparseMultiFormatterSpec> SparseMultiValueFormatters { get; } = [];
public List<FormatterSpecBase> ValueFormatters { get; } = [];
/// <summary>
@@ -24,8 +33,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
=> AddValueFormatter(SimpleValueFormatters.Count, reportKey, valueFormatter);
public GameSpec AddValueFormatter(
string reportKey,
SingleValueFormatter valueFormatter
) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter);
/// <summary>
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -35,15 +46,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(int priority, string reportKey,
ValueFormatter valueFormatter)
public GameSpec AddValueFormatter(
int priority,
string reportKey,
SingleValueFormatter valueFormatter
) => AddValueFormatter(new FormatterSpec
{
SimpleValueFormatters.Add(new FormatterSpec
{
Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
});
return this;
}
Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter
});
/// <summary>
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
@@ -52,8 +62,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
=> AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter);
public GameSpec AddMultiValueFormatter(
string[] reportKeys,
MultiValueFormatter valueFormatter
) => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
/// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -63,15 +75,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
MultiValueFormatter valueFormatter)
public GameSpec AddMultiValueFormatter(
int priority,
string[] reportKeys,
MultiValueFormatter valueFormatter
) => AddValueFormatter(new MultiFormatterSpec
{
MultiValueFormatters.Add(new MultiFormatterSpec
{
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
});
return this;
}
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
});
/// <summary>
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
@@ -83,8 +94,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter)
=> AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter);
public GameSpec AddSparseMultiValueFormatter(
string[] reportKeys,
SparseMultiValueFormatter valueFormatter
) => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
/// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -97,13 +110,18 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKeys">The key names to match.</param>
/// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
SparseMultiValueFormatter valueFormatter)
public GameSpec AddSparseMultiValueFormatter(
int priority,
string[] reportKeys,
SparseMultiValueFormatter valueFormatter
) => AddValueFormatter(new SparseMultiFormatterSpec
{
SparseMultiValueFormatters.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;
}
}
@@ -111,30 +129,107 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <summary>
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
/// </summary>
public struct FormatterSpec
public class FormatterSpec : FormatterSpecBase
{
public required int Priority { get; init; }
public required string ReportKey { get; init; }
public ValueFormatter Formatter { get; init; }
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(ReportKeys[0], out MessagePackObject valuePackObject))
{
result = null;
return false;
}
result = valuePackObject;
return true;
}
}
/// <summary>
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
/// </summary>
public struct MultiFormatterSpec
public class MultiFormatterSpec : FormatterSpecBase
{
public required int Priority { get; init; }
public required string[] ReportKeys { get; init; }
public MultiValueFormatter Formatter { get; init; }
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
List<MessagePackObject> packedObjects = [];
foreach (string reportKey in ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
{
result = null;
return false;
}
packedObjects.Add(valuePackObject);
}
result = packedObjects;
return true;
}
}
/// <summary>
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
/// </summary>
public struct SparseMultiFormatterSpec
public class SparseMultiFormatterSpec : FormatterSpecBase
{
public required int Priority { get; init; }
public required string[] ReportKeys { get; init; }
public SparseMultiValueFormatter Formatter { get; init; }
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
Dictionary<string, MessagePackObject> packedObjects = [];
foreach (string reportKey in ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(reportKey, valuePackObject);
}
result = packedObjects;
return true;
}
}
public abstract class FormatterSpecBase
{
public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data);
public int Priority { get; init; }
public string[] ReportKeys { get; init; }
public Delegate Formatter { get; init; }
public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
out FormattedValue formattedValue)
{
formattedValue = default;
if (!GetData(playReport, out object data))
return false;
if (data is FormattedValue fv)
{
formattedValue = fv;
return true;
}
switch (Formatter)
{
case SingleValueFormatter svf when data is MessagePackObject match:
formattedValue = svf(
new SingleValue(match) { Application = appMeta, PlayReport = playReport }
);
return true;
case MultiValueFormatter mvf when data is List<MessagePackObject> matches:
formattedValue = mvf(
new MultiValue(matches) { Application = appMeta, PlayReport = playReport }
);
return true;
case SparseMultiValueFormatter smvf when data is Dictionary<string, MessagePackObject> sparseMatches:
formattedValue = smvf(
new SparseMultiValue(sparseMatches) { Application = appMeta, PlayReport = playReport }
);
return true;
default:
throw new InvalidOperationException("Formatter delegate is not of a known type!");
}
}
}
}

View File

@@ -1,5 +1,4 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,8 +6,7 @@ using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
/// <summary>
/// The input data to a <see cref="ValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// The base input data to a ValueFormatter delegate,
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary>
public readonly struct Value
@@ -70,7 +68,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
}
/// <summary>
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
/// A potential formatted value returned by a ValueFormatter delegate.
/// </summary>
public readonly struct FormattedValue
{
@@ -116,28 +114,47 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <summary>
/// Return this to tell the caller there is no value to return.
/// </summary>
public static FormattedValue Unhandled => default;
public static readonly FormattedValue Unhandled = default;
/// <summary>
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
/// </summary>
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
public static readonly FormattedValue ForceReset = new() { Handled = true, Reset = true };
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="SingleValueFormatter"/>.
/// </summary>
public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset;
public static readonly SingleValueFormatter SingleAlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="MultiValueFormatter"/>.
/// </summary>
public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="SparseMultiValueFormatter"/>.
/// </summary>
public static readonly SparseMultiValueFormatter SparseMultiAlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
/// <paramref name="formattedValue"/> in a <see cref="SingleValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
public static SingleValueFormatter SingleAlwaysReturns(string formattedValue) => _ => formattedValue;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="MultiValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static MultiValueFormatter MultiAlwaysReturns(string formattedValue) => _ => formattedValue;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="SparseMultiValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static SparseMultiValueFormatter SparseMultiAlwaysReturns(string formattedValue) => _ => formattedValue;
}
}