diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 7339d725c..9d7e5ea8e 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -514,7 +514,7 @@ namespace Ryujinx.UI.App.Common if (nca.Header.ContentType == NcaContentType.PublicData) { - titleUpdates.Add(new DownloadableContentModel(nca.Header.TitleId, filePath, fileEntry.FullPath, false)); + titleUpdates.Add(new DownloadableContentModel(nca.Header.TitleId, filePath, fileEntry.FullPath)); } } @@ -1294,7 +1294,7 @@ namespace Ryujinx.UI.App.Common { return new Nca(_virtualFileSystem.KeySet, ncaStorage); } - catch (Exception ex) { } + catch (Exception) { } return null; } diff --git a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs index cea338592..d1b1734fb 100644 --- a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs +++ b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs @@ -1,24 +1,11 @@ namespace Ryujinx.UI.Common.Models { - public class DownloadableContentModel + public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath) { - public ulong TitleId { get; } - public string ContainerPath { get; } - public string FullPath { get; } - public bool Enabled { get; } - public bool IsBundled { get; } + public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci"; public string FileName => System.IO.Path.GetFileName(ContainerPath); public string TitleIdStr => TitleId.ToString("X16"); public ulong TitleIdBase => TitleId & ~0x1FFFUL; - - public DownloadableContentModel(ulong titleId, string containerPath, string fullPath, bool enabled) - { - TitleId = titleId; - ContainerPath = containerPath; - FullPath = fullPath; - Enabled = enabled; - IsBundled = System.IO.Path.GetExtension(containerPath)?.ToLower() == ".xci"; - } } } diff --git a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs index 2f89d16b3..2c11c90c2 100644 --- a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs @@ -1,23 +1,10 @@ namespace Ryujinx.UI.Common.Models { - public class TitleUpdateModel + public record TitleUpdateModel(ulong TitleId, ulong Version, string DisplayVersion, string Path) { - public ulong TitleId { get; } - public ulong Version { get; } - public string DisplayVersion { get; } - public string Path { get; } - public bool IsBundled { get; } - + public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci"; + public string TitleIdStr => TitleId.ToString("X16"); public ulong TitleIdBase => TitleId & ~0x1FFFUL; - - public TitleUpdateModel(ulong titleId, ulong version, string displayVersion, string path) - { - TitleId = titleId; - Version = version; - DisplayVersion = displayVersion; - Path = path; - IsBundled = System.IO.Path.GetExtension(path)?.ToLower() == ".xci"; - } } } diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index 5edd02308..ab833218b 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs @@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication); } } @@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication); } } diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index defe99dde..45496722b 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -35,6 +35,7 @@ namespace Ryujinx.Ava.UI.ViewModels private readonly string _downloadableContentJsonPath; private readonly VirtualFileSystem _virtualFileSystem; + private readonly ApplicationLibrary _applicationLibrary; private AvaloniaList _downloadableContents = new(); private AvaloniaList _views = new(); private AvaloniaList _selectedDownloadableContents = new(); @@ -93,9 +94,10 @@ namespace Ryujinx.Ava.UI.ViewModels get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); } - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { _virtualFileSystem = virtualFileSystem; + _applicationLibrary = applicationLibrary; _applicationData = applicationData; @@ -145,12 +147,11 @@ namespace Ryujinx.Ava.UI.ViewModels { var content = new DownloadableContentModel(nca.Header.TitleId, downloadableContentContainer.ContainerPath, - downloadableContentNca.FullPath, - downloadableContentNca.Enabled); + downloadableContentNca.FullPath); DownloadableContents.Add(content); - if (content.Enabled) + if (downloadableContentNca.Enabled) { SelectedDownloadableContents.Add(content); } @@ -240,34 +241,23 @@ namespace Ryujinx.Ava.UI.ViewModels return true; } - using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem); + if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs)) + { + return false; + } bool success = false; - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + foreach (var dlc in dlcs) { - using var ncaFile = new UniqueRef(); - - partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path); - if (nca == null) + if (dlc.TitleIdBase != _applicationData.IdBase) { continue; } - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if (nca.GetProgramIdBase() != _applicationData.IdBase) - { - continue; - } + DownloadableContents.Add(dlc); + Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(dlc)); - var content = new DownloadableContentModel(nca.Header.TitleId, path, fileEntry.FullPath, true); - DownloadableContents.Add(content); - Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(content)); - - success = true; - } + success = true; } if (success) @@ -282,6 +272,7 @@ namespace Ryujinx.Ava.UI.ViewModels public void Remove(DownloadableContentModel model) { DownloadableContents.Remove(model); + SelectedDownloadableContents.Remove(model); OnPropertyChanged(nameof(UpdateCount)); Sort(); } @@ -289,13 +280,15 @@ namespace Ryujinx.Ava.UI.ViewModels public void RemoveAll() { DownloadableContents.Clear(); + SelectedDownloadableContents.Clear(); OnPropertyChanged(nameof(UpdateCount)); Sort(); } public void EnableAll() { - SelectedDownloadableContents = new(DownloadableContents); + SelectedDownloadableContents.Clear(); + SelectedDownloadableContents.AddRange(DownloadableContents); } public void DisableAll() @@ -303,6 +296,16 @@ namespace Ryujinx.Ava.UI.ViewModels SelectedDownloadableContents.Clear(); } + public void Enable(DownloadableContentModel model) + { + SelectedDownloadableContents.ReplaceOrAdd(model, model); + } + + public void Disable(DownloadableContentModel model) + { + SelectedDownloadableContents.Remove(model); + } + public void Save() { _downloadableContentContainerList.Clear(); @@ -327,7 +330,7 @@ namespace Ryujinx.Ava.UI.ViewModels container.DownloadableContentNcaList.Add(new DownloadableContentNca { - Enabled = downloadableContent.Enabled, + Enabled = SelectedDownloadableContents.Contains(downloadableContent), TitleId = downloadableContent.TitleId, FullPath = downloadableContent.FullPath, }); diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index 3f6cc46b7..8d651d6cf 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -38,6 +38,7 @@ namespace Ryujinx.Ava.UI.ViewModels public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } + private ApplicationLibrary ApplicationLibrary { get; } private ApplicationData ApplicationData { get; } private AvaloniaList _titleUpdates = new(); @@ -78,9 +79,10 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { VirtualFileSystem = virtualFileSystem; + ApplicationLibrary = applicationLibrary; ApplicationData = applicationData; @@ -161,54 +163,36 @@ namespace Ryujinx.Ava.UI.ViewModels { return; } - - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - + try { - using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, VirtualFileSystem); - - Dictionary updates = pfs.GetContentData(ContentMetaType.Patch, VirtualFileSystem, checkLevel); - - Nca patchNca = null; - Nca controlNca = null; - - if (updates.TryGetValue(ApplicationData.Id, out ContentMetaData content)) - { - patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program); - controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control); - } - - if (controlNca != null && patchNca != null) - { - ApplicationControlProperty controlData = new(); - - using UniqueRef nacpFile = new(); - - controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - - var displayVersion = controlData.DisplayVersionString.ToString(); - var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version, displayVersion, path); - - TitleUpdates.Add(update); - - if (selected) - { - Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = update); - } - } - else + if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var titleUpdates)) { if (!ignoreNotFound) { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + Dispatcher.UIThread.InvokeAsync(() => + ContentDialogHelper.CreateErrorDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + } + + return; + } + + foreach (var titleUpdate in titleUpdates) + { + if (titleUpdate.TitleIdBase != ApplicationData.Id) + { + continue; + } + + TitleUpdates.Add(titleUpdate); + + if (selected) + { + Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate); } } - } - catch (Exception ex) + } catch (Exception ex) { Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path))); } @@ -254,7 +238,7 @@ namespace Ryujinx.Ava.UI.ViewModels { TitleUpdateWindowData.Paths.Add(update.Path); - if (update == SelectedUpdate) + if (update == SelectedUpdate as TitleUpdateModel) { TitleUpdateWindowData.Selected = update.Path; } diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index a94e78630..9ba655e0e 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -77,8 +77,9 @@ SelectionMode="Multiple, Toggle" Background="Transparent" SelectionChanged="OnSelectionChanged" - SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}" + SelectedItems="{Binding SelectedDownloadableContents}" ItemsSource="{Binding Views}"> + diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs index 5185ed5f7..ab4898e80 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs @@ -24,21 +24,21 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { - DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData); + DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationLibrary, applicationData); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData), + Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationLibrary, applicationData), Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdBaseString), }; @@ -89,12 +89,7 @@ namespace Ryujinx.Ava.UI.Windows { if (content is DownloadableContentModel model) { - var index = ViewModel.DownloadableContents.IndexOf(model); - - if (index != -1) - { - // ViewModel.DownloadableContents[index].Enabled = true; - } + ViewModel.Enable(model); } } @@ -102,12 +97,7 @@ namespace Ryujinx.Ava.UI.Windows { if (content is DownloadableContentModel model) { - var index = ViewModel.DownloadableContents.IndexOf(model); - - if (index != -1) - { - // ViewModel.DownloadableContents[index].Enabled = false; - } + ViewModel.Disable(model); } } } diff --git a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs index 259ac023c..9e26c134d 100644 --- a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml.cs @@ -26,21 +26,21 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { - DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationLibrary, applicationData); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new TitleUpdateWindow(virtualFileSystem, applicationData), + Content = new TitleUpdateWindow(virtualFileSystem, applicationLibrary, applicationData), Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdBaseString), };