Compare commits

...

17 Commits

Author SHA1 Message Date
Frog Business
67bbe6b628 Merge f73ffd1fd0 into d1da937fce 2025-02-06 09:17:10 +01:00
Evan Husted
d1da937fce misc: chore: [ci skip] XMLdocs on new Play Report Analyzer members 2025-02-05 19:51:43 -06:00
Evan Husted
4a8f98126f [ci skip] remove test 2025-02-05 19:45:29 -06:00
Evan Husted
e55629a908 misc: chore: [ci skip] Play Report Analyzer: Added Multi Value formatters 2025-02-05 19:42:36 -06:00
Evan Husted
c638a7daf8 misc: chore: Move Play Report analyzer into a dedicated namespace and remove the PlayReport name prefix on types 2025-02-05 19:27:44 -06:00
Piplup
5e5e180fea PlayReportAnalyzer: Added Pokemon Scarlet and Violet (#630)
Every base game location excluding buildings are done, DLC locations
will be added at a later point
2025-02-05 18:32:27 -06:00
Hack茶ん
131fe71205 Update Korean translation (#624) 2025-02-05 02:40:37 -06:00
Evan Husted
6af388c623 misc: chore: [ci skip] oops forgot to localize the reset button & confirmation 2025-02-05 02:01:33 -06:00
Evan Husted
45cec4e7cf UI: In-app Configuration resetting 2025-02-05 01:42:27 -06:00
FluffyOMC
479b38f035 Add tooltips to game status (#625) 2025-02-05 00:42:20 -06:00
Evan Husted
3ecc7819cc UI: Fix the app list sort types using the newly changed localization keys 2025-02-04 23:47:24 -06:00
Evan Husted
4b1d94ccd8 misc: chore: [ci skip] use MultiplayerInfoConverter instance instead of constructing for every use 2025-02-04 23:36:36 -06:00
Evan Husted
4ae9f1c0d2 UI: Use Hosted Games & Player Count localization keys in list view too 2025-02-04 23:31:31 -06:00
Evan Husted
717851985e UI: Reorganize Game Info dialog popup + localization 2025-02-04 23:28:37 -06:00
Evan Husted
bd08a111a8 UI: Show what each value is in the Game Info dialog, add game icon 2025-02-04 22:47:12 -06:00
Evan Husted
1972a47f39 UI: Game stats button on right click for Grid view users 2025-02-04 19:32:17 -06:00
Barış Hamil
f73ffd1fd0 Ability to assign hotkeys to cycle controllers for players 2025-02-01 12:30:51 +03:00
28 changed files with 1371 additions and 315 deletions

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardHotkeys
@@ -13,5 +15,6 @@ namespace Ryujinx.Common.Configuration.Hid
public Key VolumeDown { get; set; }
public Key CustomVSyncIntervalIncrement { get; set; }
public Key CustomVSyncIntervalDecrement { get; set; }
public List<Key> CycleControllers { get; set; }
}
}

View File

@@ -40,6 +40,7 @@ using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
@@ -49,6 +50,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -1308,6 +1310,18 @@ namespace Ryujinx.Ava
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.CycleControllersPlayer1:
case KeyboardHotkeyState.CycleControllersPlayer2:
case KeyboardHotkeyState.CycleControllersPlayer3:
case KeyboardHotkeyState.CycleControllersPlayer4:
case KeyboardHotkeyState.CycleControllersPlayer5:
case KeyboardHotkeyState.CycleControllersPlayer6:
case KeyboardHotkeyState.CycleControllersPlayer7:
case KeyboardHotkeyState.CycleControllersPlayer8:
var player = currentHotkeyState - KeyboardHotkeyState.CycleControllersPlayer1;
var ivm = new UI.ViewModels.Input.InputViewModel();
Dispatcher.UIThread.Invoke(() => ivm.CyclePlayerDevice(player));
break;
case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear();
break;
@@ -1390,6 +1404,15 @@ namespace Ryujinx.Ava
state = KeyboardHotkeyState.CustomVSyncIntervalDecrement;
}
foreach (var cycle in ConfigurationState.Instance.Hid.Hotkeys.Value.CycleControllers?.Select((value, index) => (value, index)) ?? [])
{
if (_keyboardInterface.IsPressed((Key)cycle.value))
{
state = KeyboardHotkeyState.CycleControllersPlayer1 + cycle.index;
break;
}
}
return state;
}
}

View File

@@ -584,7 +584,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "UI를 숨긴 상태에서 게임 시작",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -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": {
@@ -2034,7 +2259,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "PPTC 캐시 제거",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2059,7 +2284,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "앱의 모든 PPTC 캐시 파일 삭제",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2384,7 +2609,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "선택한 DLC 파일에서 RomFS 추출",
"no_NO": "Pakk ut RomFS filene fra valgt DLC fil",
"pl_PL": "",
"pt_BR": "",
@@ -2534,7 +2759,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "호환성 항목 표시",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2559,7 +2784,57 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"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": "",
@@ -3334,7 +3609,7 @@
"he_IL": "",
"it_IT": "Aggiornamenti e DLC che fanno riferimento a file mancanti verranno disabilitati automaticamente",
"ja_JP": "",
"ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 언로드",
"ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 불러오기 취소",
"no_NO": "DLC og oppdateringer som henviser til manglende filer, vil bli lastet ned automatisk",
"pl_PL": "",
"pt_BR": "DLCs e Atualizações que se referem a arquivos ausentes serão descarregadas automaticamente",
@@ -4109,7 +4384,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "스웨덴어",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -4134,7 +4409,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "노르웨이어",
"no_NO": "Norsk",
"pl_PL": "",
"pt_BR": "",
@@ -4209,7 +4484,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "매치 시스템 시간",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -5397,6 +5672,31 @@
"zh_TW": "啟用警告日誌"
}
},
{
"ID": "SettingsTabHotkeysCycleControllers",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Cycle Controllers",
"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": "SettingsTabLoggingEnableErrorLogs",
"Translations": {
@@ -5847,6 +6147,56 @@
"zh_TW": "關閉"
}
},
{
"ID": "SettingsButtonReset",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Reset Settings",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsButtonResetConfirm",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "I want to reset my settings.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsButtonOk",
"Translations": {
@@ -7759,7 +8109,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "비활성화",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -7784,7 +8134,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "레인보우",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -7809,7 +8159,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "레인보우 속도",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -7834,7 +8184,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "색상",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -13084,7 +13434,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "다음에서 모든 PPTC 데이터를 제거하려고 합니다:\n\n{0}\n\n계속하시겠습니까?",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -19084,7 +19434,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "LED 설정",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -22009,7 +22359,7 @@
"he_IL": "ממשק רשת",
"it_IT": "Interfaccia di rete:",
"ja_JP": "ネットワークインタフェース:",
"ko_KR": "네트워크 인터페이스:",
"ko_KR": "네트워크 인터페이스 :",
"no_NO": "Nettverksgrensesnitt",
"pl_PL": "Interfejs sieci:",
"pt_BR": "Interface de rede:",
@@ -22984,7 +23334,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "최종 업데이트 : {0}",
"no_NO": "Sist oppdatert: {0}",
"pl_PL": "",
"pt_BR": "",
@@ -23222,6 +23572,131 @@
"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": ""
}
},
{
"ID": "ExtractAocListHeader",
"Translations": {
@@ -23234,7 +23709,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "추출할 DLC 선택",
"no_NO": "Velg en DLC og hente ut",
"pl_PL": "",
"pt_BR": "",

View File

@@ -14,5 +14,13 @@ namespace Ryujinx.Ava.Common
VolumeDown,
CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement,
CycleControllersPlayer1,
CycleControllersPlayer2,
CycleControllersPlayer3,
CycleControllersPlayer4,
CycleControllersPlayer5,
CycleControllersPlayer6,
CycleControllersPlayer7,
CycleControllersPlayer8
}
}

View File

@@ -4,16 +4,12 @@ using MsgPack;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.PlayReport;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace Ryujinx.Ava
@@ -130,8 +126,8 @@ namespace Ryujinx.Ava
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
PlayReportAnalyzer.FormattedValue formattedValue =
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
Analyzer.FormattedValue formattedValue =
PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return;

View File

@@ -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)
{

View File

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

View File

@@ -392,6 +392,12 @@ namespace Ryujinx.Ava.UI.Controls
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)
{
@@ -401,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);
}
}
}
}

View 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>

View 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})");
}
}
}

View File

@@ -93,7 +93,8 @@
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0"
Padding="0" >
Padding="0"
ToolTip.Tip="{Binding LocalizedStatusTooltip}">
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"
@@ -140,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>

View File

@@ -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;
}
}
}

View File

@@ -1,6 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Ryujinx.Ava.UI.Models.Input
{
@@ -28,8 +33,15 @@ namespace Ryujinx.Ava.UI.Models.Input
[ObservableProperty] private Key _customVSyncIntervalDecrement;
public ObservableCollection<CycleController> CycleControllers { get; set; } = new ObservableCollection<CycleController>();
public ICommand AddCycleController { get; set; }
public ICommand RemoveCycleController { get; set; }
public bool CanRemoveCycleController => CycleControllers.Count > 0 && CycleControllers.Count < 8;
public HotkeyConfig(KeyboardHotkeys config)
{
AddCycleController = MiniCommand.Create(() => CycleControllers.Add(new CycleController(CycleControllers.Count + 1, Key.Unbound)));
RemoveCycleController = MiniCommand.Create(() => CycleControllers.Remove(CycleControllers.Last()));
if (config == null)
return;
@@ -44,6 +56,7 @@ namespace Ryujinx.Ava.UI.Models.Input
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
CycleControllers.AddRange((config.CycleControllers ?? []).Select((x, i) => new CycleController(i + 1, x)));
}
public KeyboardHotkeys GetConfig() =>
@@ -60,6 +73,7 @@ namespace Ryujinx.Ava.UI.Models.Input
VolumeDown = VolumeDown,
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
CycleControllers = CycleControllers.Select(x => x.Hotkey).ToList()
};
}
}

View 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)}";
}
}

View File

@@ -0,0 +1,48 @@
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.ViewModels
{
public class CycleController : BaseModel
{
private string _player;
private Key _hotkey;
public string Player
{
get => _player;
set
{
_player = value;
OnPropertyChanged(nameof(Player));
}
}
public Key Hotkey
{
get => _hotkey;
set
{
_hotkey = value;
OnPropertyChanged(nameof(Hotkey));
}
}
public CycleController(int v, Key x)
{
Player = v switch
{
1 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer1],
2 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer2],
3 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer3],
4 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer4],
5 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer5],
6 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer6],
7 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer7],
8 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer8],
_ => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer] + " " + v
};
Hotkey = x;
}
}
}

View File

@@ -897,5 +897,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
AvaloniaKeyboardDriver.Dispose();
}
public void CyclePlayerDevice(int player)
{
LoadDevices();
PlayerId = (PlayerIndex)player;
Device = (Device + 1) % Devices.Count;
Save();
}
}
}

View File

@@ -633,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,
};
}

View File

@@ -2,7 +2,7 @@ using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using CommunityToolkit.Mvvm.Input;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
@@ -28,8 +28,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
@@ -722,6 +720,25 @@ namespace Ryujinx.Ava.UI.ViewModels
CloseWindow?.Invoke();
}
[ObservableProperty] private bool _wantsToReset;
public AsyncRelayCommand ResetButton => Commands.Create(async () =>
{
if (!WantsToReset) return;
CloseWindow?.Invoke();
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
RyujinxApp.MainWindow.LoadApplications();
await ContentDialogHelper.CreateInfoDialog(
$"Your {RyujinxApp.FullAppName} configuration has been reset.",
"",
string.Empty,
LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
"Configuration Reset");
});
public void CancelButton()
{
RevertIfNotSaved();

View File

@@ -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" />

View File

@@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -15,17 +15,18 @@
<viewModels:SettingsViewModel />
</Design.DataContext>
<UserControl.Styles>
<Style Selector="StackPanel > StackPanel">
<Style Selector="StackPanel StackPanel">
<Setter Property="Margin" Value="10, 0, 0, 0" />
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style Selector="StackPanel > StackPanel > TextBlock">
<Style Selector="StackPanel StackPanel > TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="230" />
</Style>
<Style Selector="ToggleButton">
<Style Selector="ToggleButton, Button">
<Setter Property="Width" Value="90" />
<Setter Property="Height" Value="27" />
<Setter Property="Padding" Value="0,5,0,5" /> <!-- Added vertical padding -->
</Style>
<Style Selector="ToggleButton > TextBlock">
<Setter Property="TextAlignment" Value="Center" />
@@ -39,79 +40,123 @@
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Name="SettingButtons"
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
Spacing="10"
Name="SettingButtons">
<TextBlock
Classes="h1"
Text="{ext:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" />
<ToggleButton Name="ToggleVSyncMode">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
<StackPanel
Margin="10,0,0,0"
Spacing="10"
Orientation="Vertical">
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" />
<ToggleButton Name="ToggleVSyncMode">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
<Separator Height="1" />
<StackPanel Margin="0">
<TextBlock
Classes="h1"
Text="{ext:Locale SettingsTabHotkeysCycleControllers}" />
<StackPanel Orientation="Horizontal" Spacing="10">
<Button
Content="{ext:Locale SettingsTabGeneralAdd}"
Margin="10,0,0,0"
Command="{Binding KeyboardHotkey.AddCycleController}" />
<Button
Content="{ext:Locale SettingsTabGeneralRemove}"
IsEnabled="{Binding KeyboardHotkey.CanRemoveCycleController}"
Command="{Binding KeyboardHotkey.RemoveCycleController}" />
</StackPanel>
</StackPanel>
<ItemsControl ItemsSource="{Binding KeyboardHotkey.CycleControllers}"
Name="CycleControllers">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Margin="10,0,0,0"
Orientation="Vertical"
Spacing="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Player}" />
<ToggleButton>
<TextBlock
Text="{Binding Hotkey, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</ScrollViewer>

View File

@@ -3,11 +3,14 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using DynamicData;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System.Linq;
using Button = Ryujinx.Input.Button;
using Key = Ryujinx.Common.Configuration.Hid.Key;
@@ -21,16 +24,21 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsHotkeysView()
{
InitializeComponent();
RegisterEvents();
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
CycleControllers.LayoutUpdated += (_, _1) => RegisterEvents();
}
private void RegisterEvents()
{
foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
{
if (visual is ToggleButton button and not CheckBox)
{
button.IsCheckedChanged -= Button_IsCheckedChanged;
button.IsCheckedChanged += Button_IsCheckedChanged;
}
}
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
@@ -116,6 +124,13 @@ namespace Ryujinx.Ava.UI.Views.Settings
case "CustomVSyncIntervalDecrement":
viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType<Key>();
break;
default:
var index = button.FindAncestorOfType<ItemsControl>().GetLogicalDescendants().OfType<ToggleButton>().IndexOf(button);
if (index >= 0 && viewModel.KeyboardHotkey.CycleControllers != null)
{
viewModel.KeyboardHotkey.CycleControllers[index].Hotkey = buttonValue.AsHidType<Key>();
}
break;
}
}
};

View File

@@ -108,24 +108,36 @@
</Style>
</ui:NavigationView.Styles>
</ui:NavigationView>
<ReversibleStackPanel
Grid.Row="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonOk}"
Command="{Binding OkButton}" />
<Button
HotKey="Escape"
Content="{ext:Locale SettingsButtonCancel}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
</ReversibleStackPanel>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button
IsEnabled="{Binding WantsToReset}"
Margin="10"
Content="{ext:Locale SettingsButtonReset}"
Command="{Binding ResetButton}" />
<CheckBox IsChecked="{Binding WantsToReset}"/>
<TextBlock Text="{ext:Locale SettingsButtonResetConfirm}"/>
</StackPanel>
<ReversibleStackPanel
Grid.Column="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonOk}"
Command="{Binding OkButton}" />
<Button
HotKey="Escape"
Content="{ext:Locale SettingsButtonCancel}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
</ReversibleStackPanel>
</Grid>
</Grid>
</window:StyleableAppWindow>

View File

@@ -46,9 +46,24 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
: 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; }

View File

@@ -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));

View File

@@ -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"

View File

@@ -1,85 +0,0 @@
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
namespace Ryujinx.Ava.Utilities
{
public static class PlayReport
{
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
.AddSpec(
"01007ef00011e000",
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
value.DoubleValue switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
=> value.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => PlayReportFormattedValue.ForceReset
};
}
}

View File

@@ -6,27 +6,27 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Ryujinx.Ava.Utilities
namespace Ryujinx.Ava.Utilities.PlayReport
{
/// <summary>
/// The entrypoint for the Play Report analysis system.
/// </summary>
public class PlayReportAnalyzer
public class Analyzer
{
private readonly List<PlayReportGameSpec> _specs = [];
private readonly List<GameSpec> _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)
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(string titleId, Func<GameSpec, GameSpec> transform)
{
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
_specs.Add(transform(new GameSpec { TitleIds = [titleId] }));
return this;
}
@@ -35,13 +35,13 @@ namespace Ryujinx.Ava.Utilities
/// </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)
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(string titleId, Action<GameSpec> transform)
{
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
_specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform));
return this;
}
@@ -50,15 +50,15 @@ namespace Ryujinx.Ava.Utilities
/// </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)
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(IEnumerable<string> titleIds,
Func<GameSpec, GameSpec> 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)}.");
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
_specs.Add(transform(new GameSpec { TitleIds = [..tids] }));
return this;
}
@@ -67,20 +67,20 @@ namespace Ryujinx.Ava.Utilities
/// </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)
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> 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)}.");
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
_specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform));
return this;
}
/// <summary>
/// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID.
/// Runs the configured <see cref="GameSpec.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>
@@ -95,25 +95,44 @@ namespace Ryujinx.Ava.Utilities
if (!playReport.IsDictionary)
return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
return FormattedValue.Unhandled;
foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
{
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue;
return formatSpec.ValueFormatter(new PlayReportValue
return formatSpec.ValueFormatter(new Value
{
Application = appMeta, PackedValue = valuePackObject
});
}
foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
{
List<MessagePackObject> packedObjects = [];
foreach (var reportKey in formatSpec.ReportKeys)
{
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(valuePackObject);
}
if (packedObjects.Count != formatSpec.ReportKeys.Length)
return FormattedValue.Unhandled;
return formatSpec.ValueFormatter(packedObjects
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
.ToArray());
}
return FormattedValue.Unhandled;
}
/// <summary>
/// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>.
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
/// </summary>
public readonly struct FormattedValue
{
@@ -123,7 +142,7 @@ namespace Ryujinx.Ava.Utilities
public bool Handled { get; private init; }
/// <summary>
/// Did the handler request the caller of the <see cref="PlayReportAnalyzer"/> to reset the existing value?
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
/// </summary>
public bool Reset { get; private init; }
@@ -151,42 +170,43 @@ namespace Ryujinx.Ava.Utilities
public static FormattedValue Unhandled => default;
/// <summary>
/// Return this to suggest the caller reset the value it's using the <see cref="PlayReportAnalyzer"/> for.
/// 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 };
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="PlayReportValueFormatter"/>.
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
/// </summary>
public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="PlayReportValueFormatter"/>.
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
public static ValueFormatter 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>
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
/// </summary>
public class PlayReportGameSpec
public class GameSpec
{
public required string[] TitleIds { get; init; }
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
/// <summary>
/// Add a value formatter to the current <see cref="PlayReportGameSpec"/>
/// Add a value formatter to the current <see cref="GameSpec"/>
/// 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)
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
{
@@ -196,15 +216,15 @@ namespace Ryujinx.Ava.Utilities
}
/// <summary>
/// Add a value formatter at a specific priority to the current <see cref="PlayReportGameSpec"/>
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
/// 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)
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(int priority, string reportKey,
ValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
{
@@ -212,6 +232,40 @@ namespace Ryujinx.Ava.Utilities
});
return this;
}
/// <summary>
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
/// </summary>
/// <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)
{
MultiValueFormatters.Add(new MultiFormatterSpec
{
Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
});
return this;
}
/// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
/// matching a specific set of keys 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="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)
{
MultiValueFormatters.Add(new MultiFormatterSpec
{
Priority = priority, ReportKeys = reportKeys, 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.
@@ -220,16 +274,26 @@ namespace Ryujinx.Ava.Utilities
{
public required int Priority { get; init; }
public required string ReportKey { get; init; }
public PlayReportValueFormatter ValueFormatter { get; init; }
public ValueFormatter ValueFormatter { get; init; }
}
/// <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 required int Priority { get; init; }
public required string[] ReportKeys { get; init; }
public MultiValueFormatter ValueFormatter { get; init; }
}
}
/// <summary>
/// The input data to a <see cref="PlayReportValueFormatter"/>,
/// The input data to a <see cref="ValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary>
public class PlayReportValue
public class Value
{
/// <summary>
/// The currently running application's <see cref="ApplicationMetadata"/>.
@@ -245,7 +309,7 @@ namespace Ryujinx.Ava.Utilities
/// 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.
/// so use XValue properties for that.
/// </summary>
public object BoxedValue => PackedValue.ToObject();
@@ -269,14 +333,26 @@ namespace Ryujinx.Ava.Utilities
}
/// <summary>
/// The delegate type that powers the entire analysis system (as it currently is).<br/>
/// The delegate type that powers single value formatters.<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.
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary>
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
public delegate Analyzer.FormattedValue ValueFormatter(Value value);
/// <summary>
/// The delegate type that powers multiple value formatters.<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="Analyzer"/> for.
/// </summary>
public delegate Analyzer.FormattedValue MultiValueFormatter(Value[] value);
}

View File

@@ -0,0 +1,129 @@
using static Ryujinx.Ava.Utilities.PlayReport.Analyzer;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public static class PlayReports
{
public static Analyzer Analyzer { get; } = new Analyzer()
.AddSpec(
"01007ef00011e000",
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", FormattedValue.AlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
)
.AddSpec(
["0100a3d008c5c000", "01008f6008c5e000"],
spec => spec
.AddValueFormatter("area_no", PokemonSVArea)
.AddValueFormatter("team_circle", PokemonSVUnionCircle)
);
private static FormattedValue BreathOfTheWild_MasterMode(Value value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
private static FormattedValue TearsOfTheKingdom_CurrentField(Value value) =>
value.DoubleValue switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static FormattedValue SuperMarioOdyssey_AssistMode(Value value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static FormattedValue SuperMarioOdysseyChina_AssistMode(Value value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static FormattedValue SuperMario3DWorldOrBowsersFury(Value value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static FormattedValue MarioKart8Deluxe_Mode(Value value)
=> value.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => FormattedValue.ForceReset
};
private static FormattedValue PokemonSVUnionCircle(Value value)
=> value.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
private static FormattedValue PokemonSVArea(Value value)
=> value.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
};
}
}