From df9e6e481211d8e064bb8c7d86cef40e6fb77114 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 15:51:27 -0600 Subject: [PATCH 1/3] UI: Added the ability to view Compat information on right click, and on clicking the status itself like the title ID button. --- src/Ryujinx/Assets/locales.json | 50 +++++++++++++++++++ .../UI/Controls/ApplicationContextMenu.axaml | 7 ++- .../Controls/ApplicationContextMenu.axaml.cs | 7 +++ .../UI/Controls/ApplicationListView.axaml | 28 ++++++++--- .../UI/Controls/ApplicationListView.axaml.cs | 19 +++++++ .../UI/ViewModels/MainWindowViewModel.cs | 11 ++++ .../UI/Views/Main/MainMenuBarView.axaml.cs | 2 +- .../AppLibrary/ApplicationLibrary.cs | 8 +++ .../Utilities/Compat/CompatibilityCsv.cs | 25 ++++------ .../Utilities/Compat/CompatibilityList.axaml | 2 +- .../Compat/CompatibilityList.axaml.cs | 7 ++- 11 files changed, 141 insertions(+), 25 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 18842ce72..3da0b1728 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -2522,6 +2522,56 @@ "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": "GameListContextMenuOpenModsDirectory", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 475b26787..797bc27e0 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -19,6 +19,12 @@ Header="{ext:Locale GameListContextMenuCreateShortcut}" Icon="{ext:Icon fa-solid fa-bookmark}" ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" /> + - - + Background="{DynamicResource AppListBackgroundColor}" + Margin="-1, 0, 0, 0" + Padding="0" > + + + + + appData = + ApplicationLibrary.Applications.Lookup(SelectedApplication.Id); + + return appData.HasValue && appData.Value.HasPlayabilityInfo; + } + } + public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index f6c43aade..a0bcd1aa2 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -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 () => { diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index dec265623..ee86a4a33 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -135,6 +135,14 @@ namespace Ryujinx.Ava.Utilities.AppLibrary return id.ToString("X16"); } + public bool FindApplication(ulong id, out ApplicationData foundData) + { + DynamicData.Kernel.Optional appData = Applications.Lookup(id); + foundData = appData.HasValue ? appData.Value : null; + + return appData.HasValue; + } + /// The configured key set is missing a key. /// The NCA header could not be decrypted. /// The NCA version is not supported. diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs index d0e251fe0..c3fcf99ca 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs @@ -113,20 +113,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 { diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml index 73ec84c53..132b10e26 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml @@ -34,7 +34,7 @@ Text="{ext:Locale CompatibilityListWarning}" /> - + diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs index e0d3b0c56..30d2649bc 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs @@ -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 ?? "" + } } }; From fafb99c702a83a294838359535100f5883b86822 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 15:57:32 -0600 Subject: [PATCH 2/3] misc: chore: [ci skip] don't even bother looking up the application; the tag present on the control *is* a valid title ID and can't reasonably change in between the tag being set and playability information being requested. Even if it does, worst case scenario the compat list that pops up has no results. --- src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs index 95fc911d0..7c6b0cf15 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -39,13 +39,7 @@ namespace Ryujinx.Ava.UI.Controls if (sender is not Button { Content: TextBlock playabilityLabel }) return; - if (!ulong.TryParse((string)playabilityLabel.Tag, NumberStyles.HexNumber, null, out ulong titleId)) - return; - - if (!mwvm.ApplicationLibrary.FindApplication(titleId, out ApplicationData appData)) - return; - - await CompatibilityList.Show(appData.IdString); + await CompatibilityList.Show((string)playabilityLabel.Tag); } private async void IdString_OnClick(object sender, RoutedEventArgs e) From e8a7d5b0b74d1d5ad2ba09cc7c07f6ba333972d3 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 4 Feb 2025 17:21:54 -0600 Subject: [PATCH 3/3] UI: Only show DLC RomFS button under Extract Data when DLCs are available. Also convert the constructor of DlcSelectViewModel to expect a normal title id and not one already converted to the base ID. --- .../UI/Controls/ApplicationContextMenu.axaml | 1 + .../Controls/ApplicationContextMenu.axaml.cs | 2 +- .../UI/ViewModels/DlcSelectViewModel.cs | 4 +--- .../UI/ViewModels/MainWindowViewModel.cs | 2 ++ .../Utilities/AppLibrary/ApplicationLibrary.cs | 18 ++++++++++++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 797bc27e0..2804485fe 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -117,6 +117,7 @@ Header="{ext:Locale GameListContextMenuExtractDataRomFS}" ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" /> diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index f29f70432..0d81484ba 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -334,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) { diff --git a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs index d50d8249a..b486aa766 100644 --- a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs @@ -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(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f0e05d517..632e3b4f0 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -360,6 +360,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } + 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; diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs index ee86a4a33..75737c3e5 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationLibrary.cs @@ -142,6 +142,24 @@ namespace Ryujinx.Ava.Utilities.AppLibrary return appData.HasValue; } + + public bool FindUpdate(ulong id, out TitleUpdateModel foundData) + { + Gommon.Optional 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 DownloadableContentModel[] FindDlcsFor(ulong id) + => DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray(); + + public bool HasDlcs(ulong id) + => DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL)); /// The configured key set is missing a key. /// The NCA header could not be decrypted.