Compare commits
29 Commits
Canary-1.2
...
Canary-1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479b38f035 | ||
|
|
3ecc7819cc | ||
|
|
4b1d94ccd8 | ||
|
|
4ae9f1c0d2 | ||
|
|
717851985e | ||
|
|
bd08a111a8 | ||
|
|
1972a47f39 | ||
|
|
222ceb818b | ||
|
|
b0fcc5bee1 | ||
|
|
820e8f7375 | ||
|
|
e8a7d5b0b7 | ||
|
|
fafb99c702 | ||
|
|
df9e6e4812 | ||
|
|
566f3d079a | ||
|
|
d7707d4176 | ||
|
|
7a9b62884a | ||
|
|
de9faf183a | ||
|
|
0bf7c5dfa2 | ||
|
|
11bc32d98e | ||
|
|
063430ea16 | ||
|
|
65f08caaa3 | ||
|
|
f225b18c05 | ||
|
|
d8549f687b | ||
|
|
5ab50680b4 | ||
|
|
a0edc5c2b0 | ||
|
|
158ea7b4d6 | ||
|
|
8bc3de8303 | ||
|
|
c812106611 | ||
|
|
11e4d8f970 |
@@ -47,7 +47,8 @@ namespace Ryujinx.Common
|
||||
"01006f8002326000", // Animal Crossings: New Horizons
|
||||
"01009bf0072d4000", // Captain Toad: Treasure Tracker
|
||||
"01009510001ca000", // Fast RMX
|
||||
"01005CA01580E000", // Persona 5 Royale
|
||||
"01005CA01580E000", // Persona 5 Royal
|
||||
"0100b880154fc000", // Persona 5 The Royal (Japan)
|
||||
"010015100b514000", // Super Mario Bros. Wonder
|
||||
"0100000000010000", // Super Mario Odyssey
|
||||
|
||||
|
||||
@@ -2,35 +2,36 @@ using MsgPack;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Horizon
|
||||
{
|
||||
public static class HorizonStatic
|
||||
{
|
||||
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
|
||||
internal static void HandlePlayReport(MessagePackObject report) =>
|
||||
new Thread(() => PlayReport?.Invoke(report))
|
||||
{
|
||||
Name = "HLE.PlayReportEvent",
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.AboveNormal
|
||||
}.Start();
|
||||
|
||||
public static event Action<MessagePackObject> PlayReportPrinted;
|
||||
|
||||
[ThreadStatic]
|
||||
private static HorizonOptions _options;
|
||||
public static event Action<MessagePackObject> PlayReport;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ISyscallApi _syscall;
|
||||
[field: ThreadStatic]
|
||||
public static HorizonOptions Options { get; private set; }
|
||||
|
||||
[ThreadStatic]
|
||||
private static IVirtualMemoryManager _addressSpace;
|
||||
[field: ThreadStatic]
|
||||
public static ISyscallApi Syscall { get; private set; }
|
||||
|
||||
[ThreadStatic]
|
||||
private static IThreadContext _threadContext;
|
||||
[field: ThreadStatic]
|
||||
public static IVirtualMemoryManager AddressSpace { get; private set; }
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _threadHandle;
|
||||
[field: ThreadStatic]
|
||||
public static IThreadContext ThreadContext { get; private set; }
|
||||
|
||||
public static HorizonOptions Options => _options;
|
||||
public static ISyscallApi Syscall => _syscall;
|
||||
public static IVirtualMemoryManager AddressSpace => _addressSpace;
|
||||
public static IThreadContext ThreadContext => _threadContext;
|
||||
public static int CurrentThreadHandle => _threadHandle;
|
||||
[field: ThreadStatic]
|
||||
public static int CurrentThreadHandle { get; private set; }
|
||||
|
||||
public static void Register(
|
||||
HorizonOptions options,
|
||||
@@ -39,11 +40,11 @@ namespace Ryujinx.Horizon
|
||||
IThreadContext threadContext,
|
||||
int threadHandle)
|
||||
{
|
||||
_options = options;
|
||||
_syscall = syscallApi;
|
||||
_addressSpace = addressSpace;
|
||||
_threadContext = threadContext;
|
||||
_threadHandle = threadHandle;
|
||||
Options = options;
|
||||
Syscall = syscallApi;
|
||||
AddressSpace = addressSpace;
|
||||
ThreadContext = threadContext;
|
||||
CurrentThreadHandle = threadHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Gommon;
|
||||
using MsgPack;
|
||||
using MsgPack.Serialization;
|
||||
using Ryujinx.Common.Logging;
|
||||
@@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf;
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
|
||||
|
||||
namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
|
||||
@@ -1524,6 +1524,156 @@
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderDeveloper",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Developed by {0}",
|
||||
"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": "GameListHeaderVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "Έκδοση: {0}",
|
||||
"en_US": "Version: {0}",
|
||||
"es_ES": "Versión: {0}",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "Versione: {0}",
|
||||
"ja_JP": "バージョン: {0}",
|
||||
"ko_KR": "버전: {0}",
|
||||
"no_NO": "Versjon: {0}",
|
||||
"pl_PL": "Wersja: {0}",
|
||||
"pt_BR": "Versão: {0}",
|
||||
"ru_RU": "Версия: {0}",
|
||||
"sv_SE": "",
|
||||
"th_TH": "เวอร์ชั่น: {0}",
|
||||
"tr_TR": "Sürüm: {0}",
|
||||
"uk_UA": "Версія: {0}",
|
||||
"zh_CN": "版本: {0}",
|
||||
"zh_TW": "版本: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTimePlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Spielzeit: {0}",
|
||||
"el_GR": "Χρόνος: {0}",
|
||||
"en_US": "Play Time: {0}",
|
||||
"es_ES": "Tiempo jugado: {0}",
|
||||
"fr_FR": "Temps de jeu: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Tempo di gioco: {0}",
|
||||
"ja_JP": "プレイ時間: {0}",
|
||||
"ko_KR": "플레이 타임: {0}",
|
||||
"no_NO": "Spilletid: {0}",
|
||||
"pl_PL": "Czas w grze: {0}",
|
||||
"pt_BR": "Tempo de jogo: {0}",
|
||||
"ru_RU": "Время в игре: {0}",
|
||||
"sv_SE": "Speltid: {0}",
|
||||
"th_TH": "เล่นไปแล้ว: {0}",
|
||||
"tr_TR": "Oynama Süresi: {0}",
|
||||
"uk_UA": "Зіграно часу: {0}",
|
||||
"zh_CN": "游玩时长: {0}",
|
||||
"zh_TW": "遊玩時數: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderLastPlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Zuletzt gespielt: {0}",
|
||||
"el_GR": "Παίχτηκε: {0}",
|
||||
"en_US": "Last Played: {0}",
|
||||
"es_ES": "Jugado por última vez: {0}",
|
||||
"fr_FR": "Dernière partie jouée: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Ultima partita: {0}",
|
||||
"ja_JP": "最終プレイ日時: {0}",
|
||||
"ko_KR": "마지막 플레이: {0}",
|
||||
"no_NO": "Sist Spilt: {0}",
|
||||
"pl_PL": "Ostatnio grane: {0}",
|
||||
"pt_BR": "Último jogo: {0}",
|
||||
"ru_RU": "Последний запуск: {0}",
|
||||
"sv_SE": "Senast spelad: {0}",
|
||||
"th_TH": "เล่นล่าสุด: {0}",
|
||||
"tr_TR": "Son Oynama Tarihi: {0}",
|
||||
"uk_UA": "Востаннє зіграно: {0}",
|
||||
"zh_CN": "最近游玩: {0}",
|
||||
"zh_TW": "最近遊玩: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileExtension",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Dateiformat: {0}",
|
||||
"el_GR": "Κατάληξη: {0}",
|
||||
"en_US": "Extension: {0}",
|
||||
"es_ES": "Extensión: {0}",
|
||||
"fr_FR": "Extension du Fichier: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Estensione: {0}",
|
||||
"ja_JP": "ファイル拡張子: {0}",
|
||||
"ko_KR": "파일 확장자: {0}",
|
||||
"no_NO": "Fil Eks.: {0}",
|
||||
"pl_PL": "Rozszerzenie pliku: {0}",
|
||||
"pt_BR": "Extensão: {0}",
|
||||
"ru_RU": "Расширение файла: {0}",
|
||||
"sv_SE": "Filänd: {0}",
|
||||
"th_TH": "นามสกุลไฟล์: {0}",
|
||||
"tr_TR": "Dosya Uzantısı: {0}",
|
||||
"uk_UA": "Розширення файлу: {0}",
|
||||
"zh_CN": "扩展名: {0}",
|
||||
"zh_TW": "副檔名: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileSize",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Dateigröße: {0}",
|
||||
"el_GR": "Μέγεθος Αρχείου: {0}",
|
||||
"en_US": "File Size: {0}",
|
||||
"es_ES": "Tamaño del archivo: {0}",
|
||||
"fr_FR": "Taille du Fichier: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Dimensione file: {0}",
|
||||
"ja_JP": "ファイルサイズ: {0}",
|
||||
"ko_KR": "파일 크기: {0}",
|
||||
"no_NO": "Fil Størrelse: {0}",
|
||||
"pl_PL": "Rozmiar pliku: {0}",
|
||||
"pt_BR": "Tamanho: {0}",
|
||||
"ru_RU": "Размер файла: {0}",
|
||||
"sv_SE": "Filstorlek: {0}",
|
||||
"th_TH": "ขนาดไฟล์: {0}",
|
||||
"tr_TR": "Dosya Boyutu: {0}",
|
||||
"uk_UA": "Розмір файлу: {0}",
|
||||
"zh_CN": "大小: {0}",
|
||||
"zh_TW": "檔案大小: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListSortDeveloper",
|
||||
"Translations": {
|
||||
"ar_SA": "المطور",
|
||||
"de_DE": "Entwickler",
|
||||
@@ -1548,32 +1698,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "الإصدار",
|
||||
"de_DE": "",
|
||||
"el_GR": "Έκδοση",
|
||||
"en_US": "Version",
|
||||
"es_ES": "Versión",
|
||||
"fr_FR": "",
|
||||
"he_IL": "גרסה",
|
||||
"it_IT": "Versione",
|
||||
"ja_JP": "バージョン",
|
||||
"ko_KR": "버전",
|
||||
"no_NO": "Versjon",
|
||||
"pl_PL": "Wersja",
|
||||
"pt_BR": "Versão",
|
||||
"ru_RU": "Версия",
|
||||
"sv_SE": "",
|
||||
"th_TH": "เวอร์ชั่น",
|
||||
"tr_TR": "Sürüm",
|
||||
"uk_UA": "Версія",
|
||||
"zh_CN": "版本",
|
||||
"zh_TW": "版本"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTimePlayed",
|
||||
"ID": "GameListSortTimePlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "وقت اللعب",
|
||||
"de_DE": "Spielzeit",
|
||||
@@ -1598,7 +1723,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderLastPlayed",
|
||||
"ID": "GameListSortLastPlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "آخر مرة لُعبت",
|
||||
"de_DE": "Zuletzt gespielt",
|
||||
@@ -1623,7 +1748,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileExtension",
|
||||
"ID": "GameListSortFileExtension",
|
||||
"Translations": {
|
||||
"ar_SA": "صيغة الملف",
|
||||
"de_DE": "Dateiformat",
|
||||
@@ -1648,7 +1773,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileSize",
|
||||
"ID": "GameListSortFileSize",
|
||||
"Translations": {
|
||||
"ar_SA": "حجم الملف",
|
||||
"de_DE": "Dateigröße",
|
||||
@@ -1673,7 +1798,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderPath",
|
||||
"ID": "GameListSortPath",
|
||||
"Translations": {
|
||||
"ar_SA": "المسار",
|
||||
"de_DE": "Pfad",
|
||||
@@ -1697,6 +1822,106 @@
|
||||
"zh_TW": "路徑"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderCompatibilityStatus",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Compatibility:",
|
||||
"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": "GameListHeaderTitleId",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Title ID:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderHostedGames",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Hosted Games: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderPlayerCount",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Online Players: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuOpenUserSaveDirectory",
|
||||
"Translations": {
|
||||
@@ -2522,6 +2747,106 @@
|
||||
"zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowCompatEntry",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show Compatibility Entry",
|
||||
"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": "GameListContextMenuShowCompatEntryToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show the selected game in the Compatibility List you can normally access via the Help menu.",
|
||||
"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": "GameListContextMenuShowGameData",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show Game Info",
|
||||
"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": "GameListContextMenuShowGameDataToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show stats & details about the currently selected game.",
|
||||
"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": "GameListContextMenuOpenModsDirectory",
|
||||
"Translations": {
|
||||
@@ -23069,7 +23394,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "可游玩",
|
||||
"zh_TW": "可暢順遊玩 (Playable)"
|
||||
"zh_TW": "可暢順遊玩"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23094,7 +23419,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "进入游戏",
|
||||
"zh_TW": "大致可遊玩 (Ingame)"
|
||||
"zh_TW": "大致可遊玩"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23119,7 +23444,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "菜单",
|
||||
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
|
||||
"zh_TW": "只開啟至遊戲開始功能表"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23144,7 +23469,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "启动",
|
||||
"zh_TW": "只能啟動 (Boots)"
|
||||
"zh_TW": "只能啟動"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23169,7 +23494,132 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "什么都没有",
|
||||
"zh_TW": "無法啟動 (Nothing)"
|
||||
"zh_TW": "無法啟動"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListPlayableTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListIngameTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListMenusTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots and goes past the title screen but does not make it into main gameplay.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListBootsTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Boots but does not make it past the title screen.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListNothingTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Does not boot or shows no signs of activity.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
|
||||
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
|
||||
HorizonStatic.PlayReportPrinted += HandlePlayReport;
|
||||
HorizonStatic.PlayReport += HandlePlayReport;
|
||||
}
|
||||
|
||||
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
|
||||
@@ -117,11 +117,6 @@ namespace Ryujinx.Ava
|
||||
_currentApp = appMeta;
|
||||
}
|
||||
|
||||
private static void UpdatePlayingState()
|
||||
{
|
||||
_discordClient?.SetPresence(_discordPresencePlaying);
|
||||
}
|
||||
|
||||
private static void SwitchToMainState()
|
||||
{
|
||||
_discordClient?.SetPresence(_discordPresenceMain);
|
||||
@@ -135,21 +130,20 @@ namespace Ryujinx.Ava
|
||||
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
||||
if (_discordPresencePlaying is null) return;
|
||||
|
||||
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||
PlayReportAnalyzer.FormattedValue formattedValue =
|
||||
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||
|
||||
if (!value.Handled) return;
|
||||
if (!formattedValue.Handled) return;
|
||||
|
||||
if (value.Reset)
|
||||
{
|
||||
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
|
||||
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_discordPresencePlaying.Details = value.FormattedString;
|
||||
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||
}
|
||||
UpdatePlayingState();
|
||||
_discordPresencePlaying.Details = formattedValue.Reset
|
||||
? $"Playing {_currentApp.Title}"
|
||||
: formattedValue.FormattedString;
|
||||
|
||||
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
||||
return; //don't trigger an update if the set presence Details are identical to current
|
||||
|
||||
_discordClient.SetPresence(_discordPresencePlaying);
|
||||
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||
}
|
||||
|
||||
private static string TruncateToByteLength(string input)
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace Ryujinx.Ava
|
||||
public static MainWindow MainWindow => Current!
|
||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
|
||||
.MainWindow.Cast<MainWindow>();
|
||||
|
||||
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
|
||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
|
||||
|
||||
public static bool IsClipboardAvailable(out IClipboard clipboard)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,17 @@
|
||||
Header="{ext:Locale GameListContextMenuCreateShortcut}"
|
||||
Icon="{ext:Icon fa-solid fa-bookmark}"
|
||||
ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasCompatibilityEntry}"
|
||||
Click="OpenApplicationCompatibility_Click"
|
||||
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
|
||||
Icon="{ext:Icon mdi-gamepad}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
|
||||
<MenuItem
|
||||
Click="OpenApplicationData_Click"
|
||||
Header="{ext:Locale GameListContextMenuShowGameData}"
|
||||
Icon="{ext:Icon mdi-chart-line}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenUserSaveDirectory_Click"
|
||||
@@ -74,7 +85,6 @@
|
||||
Header="{ext:Locale GameListContextMenuTrimXCI}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon mdi-cached}">
|
||||
<MenuItem
|
||||
Click="PurgePtcCache_Click"
|
||||
@@ -112,6 +122,7 @@
|
||||
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Click="ExtractAocRomFs_Click"
|
||||
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
|
||||
|
||||
@@ -12,6 +12,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.HLE.HOS;
|
||||
@@ -333,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
return;
|
||||
|
||||
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
|
||||
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
|
||||
|
||||
if (selectedDlc is not null)
|
||||
{
|
||||
@@ -385,6 +386,18 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
viewModel.SelectedApplication.Icon
|
||||
);
|
||||
}
|
||||
|
||||
public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
|
||||
}
|
||||
|
||||
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await ApplicationDataView.Show(viewModel.SelectedApplication);
|
||||
}
|
||||
|
||||
public async void RunApplication_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
@@ -394,12 +407,8 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
src/Ryujinx/UI/Controls/ApplicationDataView.axaml
Normal file
114
src/Ryujinx/UI/Controls/ApplicationDataView.axaml
Normal file
@@ -0,0 +1,114 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
|
||||
x:DataType="viewModels:ApplicationDataViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Margin="0"
|
||||
MaxWidth="256"
|
||||
MinWidth="256"
|
||||
Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<Border Margin="5, 0" Width="1" Height="256" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnDefinitions="*">
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock HorizontalAlignment="Left"
|
||||
Text="{Binding FormattedVersion}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Left"
|
||||
Text="{Binding FormattedDeveloper}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedFileExtension}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedFileSize}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel Grid.Row="2"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding AppData.HasPlayabilityInfo}">
|
||||
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderCompatibilityStatus}" />
|
||||
<Button
|
||||
Click="PlayabilityStatus_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Padding="0">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
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" />
|
||||
<Button.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinWidth"
|
||||
Value="0" />
|
||||
<!-- avoids very wide buttons from the overall project avalonia style -->
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderTitleId}" />
|
||||
<Button
|
||||
Click="IdString_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Padding="0">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding AppData.IdString}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding AppData.HasLdnGames}"
|
||||
Text="{Binding FormattedLdnInfo}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedLastPlayed}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedPlayTime}"
|
||||
IsVisible="{Binding AppData.HasPlayedPreviously}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
86
src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
Normal file
86
src/Ryujinx/UI/Controls/ApplicationDataView.axaml.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class ApplicationDataView : UserControl
|
||||
{
|
||||
public static async Task Show(ApplicationData appData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = appData.Name,
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
MinWidth = 256,
|
||||
Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) }
|
||||
};
|
||||
|
||||
Style closeButton = new(x => x.Name("CloseButton"));
|
||||
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
|
||||
|
||||
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
|
||||
Avalonia.Layout.HorizontalAlignment.Center));
|
||||
|
||||
contentDialog.Styles.Add(closeButton);
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
public ApplicationDataView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Content: TextBlock playabilityLabel })
|
||||
return;
|
||||
|
||||
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
|
||||
window.Close(ContentDialogResult.None);
|
||||
|
||||
await CompatibilityList.Show((string)playabilityLabel.Tag);
|
||||
}
|
||||
|
||||
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel mwvm)
|
||||
return;
|
||||
|
||||
if (sender is not Button { Content: TextBlock idText })
|
||||
return;
|
||||
|
||||
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
|
||||
return;
|
||||
|
||||
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
|
||||
if (appData is null)
|
||||
return;
|
||||
|
||||
await clipboard.SetTextAsync(appData.IdString);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
"Copied Title ID",
|
||||
$"{appData.Name} ({appData.IdString})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,30 @@
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
Click="PlayabilityStatus_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding HasPlayabilityInfo}"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Margin="-1, 0, 0, 0"
|
||||
Padding="0"
|
||||
ToolTip.Tip="{Binding LocalizedStatusTooltip}">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
Tag="{Binding IdString}"
|
||||
Text="{Binding LocalizedStatus}"
|
||||
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinWidth"
|
||||
Value="0" />
|
||||
<!-- avoids very wide buttons from the overall project avalonia style -->
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel
|
||||
@@ -117,7 +141,8 @@
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
|
||||
IsVisible="{Binding HasLdnGames}"
|
||||
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -5,7 +5,9 @@ using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
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
|
||||
@@ -28,6 +30,17 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (sender is ListBox { SelectedItem: ApplicationData selected })
|
||||
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
|
||||
}
|
||||
|
||||
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel mwvm)
|
||||
return;
|
||||
|
||||
if (sender is not Button { Content: TextBlock playabilityLabel })
|
||||
return;
|
||||
|
||||
await CompatibilityList.Show((string)playabilityLabel.Tag);
|
||||
}
|
||||
|
||||
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly MultiplayerInfoConverter _instance = new();
|
||||
public static readonly MultiplayerInfoConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ApplicationData applicationData)
|
||||
{
|
||||
if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
|
||||
{
|
||||
return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
if (value is not ApplicationData { HasLdnGames: true } applicationData)
|
||||
return "";
|
||||
|
||||
return new StringBuilder()
|
||||
.AppendLine(
|
||||
LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames]
|
||||
.Format(applicationData.GameCount))
|
||||
.Append(
|
||||
LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount]
|
||||
.Format(applicationData.PlayerCount))
|
||||
.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
@@ -31,7 +35,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
return Instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
Normal file
26
src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class ApplicationDataViewModel : BaseModel
|
||||
{
|
||||
public ApplicationData AppData { get; }
|
||||
|
||||
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
|
||||
|
||||
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
|
||||
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
|
||||
|
||||
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
|
||||
public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
|
||||
public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
|
||||
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
|
||||
|
||||
public string FormattedLdnInfo =>
|
||||
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" +
|
||||
$"\n" +
|
||||
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}";
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
|
||||
{
|
||||
_dlcs = appLibrary.DownloadableContents.Items
|
||||
.Where(x => x.Dlc.TitleIdBase == titleId)
|
||||
.Select(x => x.Dlc)
|
||||
_dlcs = appLibrary.FindDlcsFor(titleId)
|
||||
.OrderBy(it => it.IsBundled ? 0 : 1)
|
||||
.ThenBy(it => it.TitleId)
|
||||
.ToArray();
|
||||
|
||||
@@ -69,8 +69,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
|
||||
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
|
||||
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
|
||||
|
||||
@@ -349,6 +349,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
|
||||
|
||||
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
|
||||
|
||||
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
|
||||
public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||
@@ -629,15 +633,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
return SortMode switch
|
||||
{
|
||||
ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
|
||||
ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper],
|
||||
ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed],
|
||||
ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed],
|
||||
ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension],
|
||||
ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize],
|
||||
ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath],
|
||||
ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite],
|
||||
ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel],
|
||||
ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
|
||||
ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper],
|
||||
ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed],
|
||||
ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed],
|
||||
ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListSortFileExtension],
|
||||
ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListSortFileSize],
|
||||
ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListSortPath],
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
|
||||
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
|
||||
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
SelectedUpdate = new TitleUpdateViewModelNoUpdate();
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
|
||||
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
|
||||
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
|
||||
CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show);
|
||||
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show());
|
||||
|
||||
UpdateMenuItem.Command = Commands.Create(async () =>
|
||||
{
|
||||
|
||||
@@ -113,37 +113,37 @@
|
||||
Tag="TitleId" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderDeveloper}"
|
||||
Content="{ext:Locale GameListSortDeveloper}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
|
||||
Tag="Developer" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderTimePlayed}"
|
||||
Content="{ext:Locale GameListSortTimePlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
|
||||
Tag="TotalTimePlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderLastPlayed}"
|
||||
Content="{ext:Locale GameListSortLastPlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
|
||||
Tag="LastPlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderFileExtension}"
|
||||
Content="{ext:Locale GameListSortFileExtension}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
|
||||
Tag="FileType" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderFileSize}"
|
||||
Content="{ext:Locale GameListSortFileSize}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
|
||||
Tag="FileSize" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{ext:Locale GameListHeaderPath}"
|
||||
Content="{ext:Locale GameListSortPath}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
|
||||
Tag="Path" />
|
||||
|
||||
@@ -7,6 +7,8 @@ using LibHac.Ns;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
@@ -21,11 +23,47 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
public bool Favorite { get; set; }
|
||||
public byte[] Icon { get; set; }
|
||||
public string Name { get; set; } = "Unknown";
|
||||
public ulong Id { get; set; }
|
||||
|
||||
private ulong _id;
|
||||
|
||||
public ulong Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
_id = value;
|
||||
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
|
||||
}
|
||||
}
|
||||
public string Developer { get; set; } = "Unknown";
|
||||
public string Version { get; set; } = "0";
|
||||
|
||||
public bool HasPlayabilityInfo => PlayabilityStatus != null;
|
||||
|
||||
public string LocalizedStatus =>
|
||||
PlayabilityStatus.HasValue
|
||||
? LocaleManager.Instance[PlayabilityStatus!.Value]
|
||||
: string.Empty;
|
||||
|
||||
public LocaleKeys? PlayabilityStatus { get; set; }
|
||||
public string LocalizedStatusTooltip =>
|
||||
PlayabilityStatus.HasValue
|
||||
#pragma warning disable CS8509 // It is exhaustive for any value this property can contain.
|
||||
? LocaleManager.Instance[PlayabilityStatus!.Value switch
|
||||
#pragma warning restore CS8509
|
||||
{
|
||||
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
|
||||
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
|
||||
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
|
||||
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
|
||||
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
|
||||
}]
|
||||
: string.Empty;
|
||||
public int PlayerCount { get; set; }
|
||||
public int GameCount { get; set; }
|
||||
|
||||
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
||||
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
|
||||
@@ -128,13 +128,50 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
|
||||
if (appData.HasValue)
|
||||
return appData.Value.Name;
|
||||
|
||||
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
|
||||
return Path.GetFileNameWithoutExtension(dlcData.FileName);
|
||||
|
||||
return id.ToString("X16");
|
||||
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
|
||||
return id.ToString("X16");
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
|
||||
int idx = name.IndexOf('[');
|
||||
if (idx != -1)
|
||||
name = name[..idx];
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public bool FindApplication(ulong id, out ApplicationData foundData)
|
||||
{
|
||||
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
|
||||
foundData = appData.HasValue ? appData.Value : null;
|
||||
|
||||
return appData.HasValue;
|
||||
}
|
||||
|
||||
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
|
||||
{
|
||||
Gommon.Optional<TitleUpdateModel> appData =
|
||||
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
|
||||
foundData = appData.HasValue ? appData.Value : null;
|
||||
|
||||
return appData.HasValue;
|
||||
}
|
||||
|
||||
public TitleUpdateModel[] FindUpdatesFor(ulong id)
|
||||
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
|
||||
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public DownloadableContentModel[] FindDlcsFor(ulong id)
|
||||
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
|
||||
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public bool HasDlcs(ulong id)
|
||||
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
|
||||
|
||||
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
|
||||
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
|
||||
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception>
|
||||
|
||||
@@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
||||
}
|
||||
|
||||
public static void Unload()
|
||||
{
|
||||
_entries = null;
|
||||
}
|
||||
|
||||
private static CompatibilityEntry[] _entries;
|
||||
|
||||
public static CompatibilityEntry[] Entries
|
||||
@@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
return _entries;
|
||||
}
|
||||
}
|
||||
|
||||
public static LocaleKeys? GetStatus(string titleId)
|
||||
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status;
|
||||
|
||||
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
public class CompatibilityEntry
|
||||
@@ -100,12 +100,25 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
public Optional<string> TitleId { get; }
|
||||
public string[] Labels { get; }
|
||||
public LocaleKeys? Status { get; }
|
||||
|
||||
public LocaleKeys? StatusDescription
|
||||
=> Status switch
|
||||
{
|
||||
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
|
||||
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
|
||||
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
|
||||
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
|
||||
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public DateTime LastUpdated { get; }
|
||||
|
||||
public string LocalizedLastUpdated =>
|
||||
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
|
||||
|
||||
|
||||
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
|
||||
public string LocalizedStatusDescription => LocaleManager.Instance[StatusDescription!.Value];
|
||||
public string FormattedTitleId => TitleId
|
||||
.OrElse(new string(' ', 16));
|
||||
|
||||
@@ -113,20 +126,17 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
.Select(FormatLabelName)
|
||||
.JoinToString(", ");
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new("CompatibilityEntry: {");
|
||||
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
|
||||
sb.Append($"{nameof(TitleId)}={TitleId}, ");
|
||||
sb.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ");
|
||||
sb.Append($"{nameof(Status)}=\"{Status}\", ");
|
||||
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
|
||||
sb.Append('}');
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
public override string ToString() =>
|
||||
new StringBuilder("CompatibilityEntry: {")
|
||||
.Append($"{nameof(GameName)}=\"{GameName}\", ")
|
||||
.Append($"{nameof(TitleId)}={TitleId}, ")
|
||||
.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ")
|
||||
.Append($"{nameof(Status)}=\"{Status}\", ")
|
||||
.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"")
|
||||
.Append('}')
|
||||
.ToString();
|
||||
|
||||
public static string FormatLabelName(string labelName) => labelName.ToLower() switch
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
Text="{ext:Locale CompatibilityListWarning}" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
|
||||
<TextBox Name="SearchBox" Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
|
||||
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
|
||||
<TextBlock Grid.Column="2" Margin="-10, 0, 0, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
|
||||
</Grid>
|
||||
@@ -64,6 +64,8 @@
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding LocalizedStatus}"
|
||||
Width="85"
|
||||
Background="Transparent"
|
||||
ToolTip.Tip="{Binding LocalizedStatusDescription}"
|
||||
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Column="3"
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public partial class CompatibilityList : UserControl
|
||||
{
|
||||
public static async Task Show()
|
||||
public static async Task Show(string titleId = null)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
@@ -18,7 +18,10 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
Content = new CompatibilityList
|
||||
{
|
||||
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
|
||||
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary),
|
||||
SearchBox = {
|
||||
Text = titleId ?? ""
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,8 +35,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
|
||||
CompatibilityCsv.Unload();
|
||||
}
|
||||
|
||||
public CompatibilityList()
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
using Gommon;
|
||||
using MsgPack;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Common.Helper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities
|
||||
{
|
||||
@@ -13,41 +7,55 @@ namespace Ryujinx.Ava.Utilities
|
||||
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
|
||||
.AddSpec(
|
||||
"01007ef00011e000",
|
||||
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||
spec => spec
|
||||
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||
// reset to normal status when switching between normal & master mode in title screen
|
||||
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
|
||||
)
|
||||
.AddSpec( // Super Mario Odyssey
|
||||
.AddSpec(
|
||||
"0100f2c0115b6000",
|
||||
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
|
||||
.AddSpec(
|
||||
"0100000000010000",
|
||||
spec =>
|
||||
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
||||
)
|
||||
.AddSpec( // Super Mario Odyssey (China)
|
||||
"010075000ECBE000",
|
||||
.AddSpec(
|
||||
"010075000ecbe000",
|
||||
spec =>
|
||||
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
||||
)
|
||||
.AddSpec( // Super Mario 3D World + Bowser's Fury
|
||||
"010028600EBDA000",
|
||||
.AddSpec(
|
||||
"010028600ebda000",
|
||||
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
||||
)
|
||||
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China)
|
||||
["0100152000022000", "010075100E8EC000"],
|
||||
.AddSpec( // Global & China IDs
|
||||
["0100152000022000", "010075100e8ec000"],
|
||||
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||
);
|
||||
|
||||
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
|
||||
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
|
||||
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
|
||||
|
||||
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
|
||||
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
|
||||
value.DoubleValue switch
|
||||
{
|
||||
> 800d => "Exploring the Sky Islands",
|
||||
< -201d => "Exploring the Depths",
|
||||
_ => "Roaming Hyrule"
|
||||
};
|
||||
|
||||
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
|
||||
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
||||
|
||||
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
|
||||
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
|
||||
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
||||
|
||||
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
|
||||
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
|
||||
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
||||
|
||||
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value)
|
||||
=> value.BoxedValue switch
|
||||
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
|
||||
=> value.StringValue switch
|
||||
{
|
||||
// Single Player
|
||||
"Single" => "Single Player",
|
||||
@@ -74,125 +82,4 @@ namespace Ryujinx.Ava.Utilities
|
||||
_ => PlayReportFormattedValue.ForceReset
|
||||
};
|
||||
}
|
||||
|
||||
#region Analyzer implementation
|
||||
|
||||
public class PlayReportAnalyzer
|
||||
{
|
||||
private readonly List<PlayReportGameSpec> _specs = [];
|
||||
|
||||
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||
{
|
||||
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
||||
{
|
||||
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||
{
|
||||
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
||||
{
|
||||
_specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
|
||||
{
|
||||
if (!playReport.IsDictionary)
|
||||
return PlayReportFormattedValue.Unhandled;
|
||||
|
||||
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
||||
return PlayReportFormattedValue.Unhandled;
|
||||
|
||||
foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
|
||||
{
|
||||
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||
continue;
|
||||
|
||||
PlayReportValue value = new()
|
||||
{
|
||||
Application = appMeta,
|
||||
BoxedValue = valuePackObject.ToObject()
|
||||
};
|
||||
|
||||
return formatSpec.ValueFormatter(ref value);
|
||||
}
|
||||
|
||||
return PlayReportFormattedValue.Unhandled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PlayReportGameSpec
|
||||
{
|
||||
public required string[] TitleIds { get; init; }
|
||||
public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
|
||||
|
||||
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
|
||||
{
|
||||
Analyses.Add(new PlayReportValueFormatterSpec
|
||||
{
|
||||
Priority = Analyses.Count,
|
||||
ReportKey = reportKey,
|
||||
ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter)
|
||||
{
|
||||
Analyses.Add(new PlayReportValueFormatterSpec
|
||||
{
|
||||
Priority = priority,
|
||||
ReportKey = reportKey,
|
||||
ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public struct PlayReportValue
|
||||
{
|
||||
public ApplicationMetadata Application { get; init; }
|
||||
public object BoxedValue { get; init; }
|
||||
}
|
||||
|
||||
public struct PlayReportFormattedValue
|
||||
{
|
||||
public bool Handled { get; private init; }
|
||||
|
||||
public bool Reset { get; private init; }
|
||||
|
||||
public string FormattedString { get; private init; }
|
||||
|
||||
public static implicit operator PlayReportFormattedValue(string formattedValue)
|
||||
=> new() { Handled = true, FormattedString = formattedValue };
|
||||
|
||||
public static PlayReportFormattedValue Unhandled => default;
|
||||
public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true };
|
||||
|
||||
public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl;
|
||||
|
||||
private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset;
|
||||
}
|
||||
|
||||
public struct PlayReportValueFormatterSpec
|
||||
{
|
||||
public required int Priority { get; init; }
|
||||
public required string ReportKey { get; init; }
|
||||
public PlayReportValueFormatter ValueFormatter { get; init; }
|
||||
}
|
||||
|
||||
public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
282
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
282
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using Gommon;
|
||||
using MsgPack;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// The entrypoint for the Play Report analysis system.
|
||||
/// </summary>
|
||||
public class PlayReportAnalyzer
|
||||
{
|
||||
private readonly List<PlayReportGameSpec> _specs = [];
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||
{
|
||||
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
|
||||
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||
|
||||
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
||||
{
|
||||
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
|
||||
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||
|
||||
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds,
|
||||
Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||
{
|
||||
string[] tids = titleIds.ToArray();
|
||||
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
|
||||
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||
|
||||
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
||||
{
|
||||
string[] tids = titleIds.ToArray();
|
||||
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
|
||||
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||
|
||||
_specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID.
|
||||
/// </summary>
|
||||
/// <param name="runningGameId">The game currently running.</param>
|
||||
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
|
||||
/// <param name="playReport">The Play Report received from HLE.</param>
|
||||
/// <returns>A struct representing a possible formatted value.</returns>
|
||||
public FormattedValue Format(
|
||||
string runningGameId,
|
||||
ApplicationMetadata appMeta,
|
||||
MessagePackObject playReport
|
||||
)
|
||||
{
|
||||
if (!playReport.IsDictionary)
|
||||
return FormattedValue.Unhandled;
|
||||
|
||||
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
||||
return FormattedValue.Unhandled;
|
||||
|
||||
foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
||||
{
|
||||
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||
continue;
|
||||
|
||||
return formatSpec.ValueFormatter(new PlayReportValue
|
||||
{
|
||||
Application = appMeta, PackedValue = valuePackObject
|
||||
});
|
||||
}
|
||||
|
||||
return FormattedValue.Unhandled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>.
|
||||
/// </summary>
|
||||
public readonly struct FormattedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Was any handler able to match anything in the Play Report?
|
||||
/// </summary>
|
||||
public bool Handled { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Did the handler request the caller of the <see cref="PlayReportAnalyzer"/> to reset the existing value?
|
||||
/// </summary>
|
||||
public bool Reset { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||
/// </summary>
|
||||
public string FormattedString { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The intended path of execution for having a string to return: simply return the string.
|
||||
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||
///
|
||||
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The formatted string value.</param>
|
||||
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||
public static implicit operator FormattedValue(string formattedValue)
|
||||
=> formattedValue is not null
|
||||
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
||||
: Unhandled;
|
||||
|
||||
/// <summary>
|
||||
/// Return this to tell the caller there is no value to return.
|
||||
/// </summary>
|
||||
public static FormattedValue Unhandled => default;
|
||||
|
||||
/// <summary>
|
||||
/// Return this to suggest the caller reset the value it's using the <see cref="PlayReportAnalyzer"/> for.
|
||||
/// </summary>
|
||||
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
|
||||
|
||||
/// <summary>
|
||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="PlayReportValueFormatter"/>.
|
||||
/// </summary>
|
||||
public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="PlayReportValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mapping of title IDs to value formatter specs.
|
||||
///
|
||||
/// <remarks>Generally speaking, use the <see cref="PlayReportAnalyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||
/// </summary>
|
||||
public class PlayReportGameSpec
|
||||
{
|
||||
public required string[] TitleIds { get; init; }
|
||||
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter to the current <see cref="PlayReportGameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <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="PlayReportGameSpec"/>, for chaining convenience.</returns>
|
||||
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
|
||||
{
|
||||
SimpleValueFormatters.Add(new FormatterSpec
|
||||
{
|
||||
Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter at a specific priority to the current <see cref="PlayReportGameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <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="PlayReportGameSpec"/>, for chaining convenience.</returns>
|
||||
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
|
||||
PlayReportValueFormatter valueFormatter)
|
||||
{
|
||||
SimpleValueFormatters.Add(new FormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <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 required int Priority { get; init; }
|
||||
public required string ReportKey { get; init; }
|
||||
public PlayReportValueFormatter ValueFormatter { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input data to a <see cref="PlayReportValueFormatter"/>,
|
||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||
/// </summary>
|
||||
public class PlayReportValue
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||
/// </summary>
|
||||
public ApplicationMetadata Application { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The matched value from the Play Report.
|
||||
/// </summary>
|
||||
public MessagePackObject PackedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||
///
|
||||
/// Does not seem to work well with comparing numeric types,
|
||||
/// so use <see cref="PackedValue"/> and the AsX (where X is a numerical type name i.e. Int32) methods for that.
|
||||
/// </summary>
|
||||
public object BoxedValue => PackedValue.ToObject();
|
||||
|
||||
#region AsX accessors
|
||||
|
||||
public bool BooleanValue => PackedValue.AsBoolean();
|
||||
public byte ByteValye => PackedValue.AsByte();
|
||||
public sbyte SByteValye => PackedValue.AsSByte();
|
||||
public short ShortValye => PackedValue.AsInt16();
|
||||
public ushort UShortValye => PackedValue.AsUInt16();
|
||||
public int IntValye => PackedValue.AsInt32();
|
||||
public uint UIntValye => PackedValue.AsUInt32();
|
||||
public long LongValye => PackedValue.AsInt64();
|
||||
public ulong ULongValye => PackedValue.AsUInt64();
|
||||
public float FloatValue => PackedValue.AsSingle();
|
||||
public double DoubleValue => PackedValue.AsDouble();
|
||||
public string StringValue => PackedValue.AsString();
|
||||
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that powers the entire analysis system (as it currently is).<br/>
|
||||
/// Takes in the result value from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <br/>
|
||||
/// OR a signal to reset the value that the caller is using the <see cref="PlayReportAnalyzer"/> for.
|
||||
/// </summary>
|
||||
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
|
||||
}
|
||||
Reference in New Issue
Block a user