From 7850a2b2aa47d2044ddf91c95cde4c86439c0459 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Fri, 16 Aug 2024 23:26:25 -0400 Subject: [PATCH] Move more DLC logic out of view model --- .../App/ApplicationLibrary.cs | 82 +++++++++++++++++-- .../Helper/DownloadableContentsHelper.cs | 45 +++++++++- .../Models/DownloadableContentModel.cs | 2 +- .../Models/TitleUpdateModel.cs | 2 +- .../Ryujinx.UI.Common.csproj | 1 + .../DownloadableContentManagerViewModel.cs | 71 ++++++++-------- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 4 +- 7 files changed, 160 insertions(+), 47 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 9d7e5ea8e..4bcbb993e 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1,6 +1,6 @@ +using DynamicData; using LibHac; using LibHac.Common; -using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; @@ -19,9 +19,11 @@ using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Utilities; using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Models; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reflection; @@ -29,6 +31,7 @@ using System.Text; using System.Text.Json; using System.Threading; using ContentType = LibHac.Ncm.ContentType; +using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; using TimeSpan = System.TimeSpan; @@ -43,6 +46,10 @@ namespace Ryujinx.UI.App.Common public event EventHandler TitleUpdateAdded; public event EventHandler DownloadableContentAdded; + public IObservableCache Applications; + public IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; + public IObservableCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> DownloadableContents; + private readonly byte[] _nspIcon; private readonly byte[] _xciIcon; private readonly byte[] _ncaIcon; @@ -52,6 +59,9 @@ namespace Ryujinx.UI.App.Common private readonly VirtualFileSystem _virtualFileSystem; private readonly IntegrityCheckLevel _checkLevel; private CancellationTokenSource _cancellationToken; + private readonly SourceCache _applications = new(it => it.Path); + private readonly SourceCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> _titleUpdates = new(it => it.TitleUpdate); + private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc); private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -60,6 +70,10 @@ namespace Ryujinx.UI.App.Common _virtualFileSystem = virtualFileSystem; _checkLevel = checkLevel; + Applications = _applications.AsObservableCache(); + TitleUpdates = _titleUpdates.AsObservableCache(); + DownloadableContents = _downloadableContents.AsObservableCache(); + _nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png"); _xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png"); _ncaIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NCA.png"); @@ -105,7 +119,7 @@ namespace Ryujinx.UI.App.Common return data; } - /// The configured key set is missing a key. + /// The configured key set is missing a key. /// The NCA header could not be decrypted. /// The NCA version is not supported. /// An error occured while reading PFS data. @@ -181,7 +195,7 @@ namespace Ryujinx.UI.App.Common return null; } - /// The configured key set is missing a key. + /// The configured key set is missing a key. /// The NCA header could not be decrypted. /// The NCA version is not supported. /// An error occured while reading PFS data. @@ -638,6 +652,7 @@ namespace Ryujinx.UI.App.Common int numApplicationsLoaded = 0; _cancellationToken = new CancellationTokenSource(); + _applications.Clear(); // Builds the applications list with paths to found applications List applicationPaths = new(); @@ -722,6 +737,14 @@ namespace Ryujinx.UI.App.Common AppData = application, }); } + + _applications.Edit(it => + { + foreach (var application in applications) + { + it.AddOrUpdate(application); + } + }); if (applications.Count > 1) { @@ -755,9 +778,40 @@ namespace Ryujinx.UI.App.Common } } - public void LoadDownloadableContents(List appDirs) + public void LoadDownloadableContents() + { + _downloadableContents.Edit(it => + { + it.Clear(); + + foreach (ApplicationData application in Applications.Items) + { + var res = DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(res); + } + }); + } + + public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) + { + _downloadableContents.Edit(it => + { + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, dlcs); + + it.Remove(it.Items.Where(item => item.Dlc.TitleIdBase == application.IdBase)); + it.AddOrUpdate(dlcs); + }); + } + + // public void LoadTitleUpdates() + // { + // + // } + + public void AutoLoadDownloadableContents(List appDirs) { _cancellationToken = new CancellationTokenSource(); + _downloadableContents.Clear(); // Builds the applications list with paths to found applications List applicationPaths = new(); @@ -842,6 +896,14 @@ namespace Ryujinx.UI.App.Common DownloadableContent = downloadableContent, }); } + + _downloadableContents.Edit(it => + { + foreach (var downloadableContent in downloadableContents) + { + it.AddOrUpdate((downloadableContent, true)); + } + }); } } } @@ -850,12 +912,12 @@ namespace Ryujinx.UI.App.Common _cancellationToken.Dispose(); _cancellationToken = null; } - } - public void LoadTitleUpdates(List appDirs) + public void AutoLoadTitleUpdates(List appDirs) { _cancellationToken = new CancellationTokenSource(); + _titleUpdates.Clear(); // Builds the applications list with paths to found applications List applicationPaths = new(); @@ -941,6 +1003,14 @@ namespace Ryujinx.UI.App.Common TitleUpdate = titleUpdate, }); } + + _titleUpdates.Edit(it => + { + foreach (var titleUpdate in titleUpdates) + { + it.AddOrUpdate((titleUpdate, false)); + } + }); } } } diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs index 5e8ae911d..31cc757e5 100644 --- a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -20,9 +20,10 @@ namespace Ryujinx.UI.Common.Helper { private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static List<(DownloadableContentModel, bool IsEnabled)> LoadSavedDownloadableContents(VirtualFileSystem vfs, ulong applicationIdBase) + public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase) { - var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("X16"), "dlc.json"); + // _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); + var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json"); if (!File.Exists(downloadableContentJsonPath)) { @@ -42,6 +43,46 @@ namespace Ryujinx.UI.Common.Helper } } + public static void SaveDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs) + { + DownloadableContentContainer container = default; + List downloadableContentContainerList = new(); + + foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) + { + if (container.ContainerPath != dlc.ContainerPath) + { + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + downloadableContentContainerList.Add(container); + } + + container = new DownloadableContentContainer + { + ContainerPath = dlc.ContainerPath, + DownloadableContentNcaList = [], + }; + } + + container.DownloadableContentNcaList.Add(new DownloadableContentNca + { + Enabled = isEnabled, + TitleId = dlc.TitleId, + FullPath = dlc.FullPath, + }); + } + + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + downloadableContentContainerList.Add(container); + } + + // _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); + // var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json"); + var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json"); + JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer); + } + private static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContents(VirtualFileSystem vfs, List downloadableContentContainers) { var result = new List<(DownloadableContentModel, bool IsEnabled)>(); diff --git a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs index d1b1734fb..16b4c9f75 100644 --- a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs +++ b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs @@ -5,7 +5,7 @@ namespace Ryujinx.UI.Common.Models 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 string TitleIdStr => TitleId.ToString("x16"); public ulong TitleIdBase => TitleId & ~0x1FFFUL; } } diff --git a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs index 2c11c90c2..045dfe845 100644 --- a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs @@ -4,7 +4,7 @@ namespace Ryujinx.UI.Common.Models { public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci"; - public string TitleIdStr => TitleId.ToString("X16"); + public string TitleIdStr => TitleId.ToString("x16"); public ulong TitleIdBase => TitleId & ~0x1FFFUL; } } diff --git a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj index 387e998b0..fcbbaba30 100644 --- a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj +++ b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj @@ -56,6 +56,7 @@ + diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index b25d88a24..e0df5401e 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -131,8 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { - var savedDlc = DownloadableContentsHelper.LoadSavedDownloadableContents(_virtualFileSystem, _applicationData.IdBase); - foreach ((DownloadableContentModel dlc, bool isEnabled) in savedDlc) + foreach ((DownloadableContentModel dlc, bool isEnabled) in _applicationLibrary.DownloadableContents.Items.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase)) { DownloadableContents.Add(dlc); @@ -300,40 +299,42 @@ namespace Ryujinx.Ava.UI.ViewModels public void Save() { - _downloadableContentContainerList.Clear(); + var dlcs = DownloadableContents.Select(it => (it, SelectedDownloadableContents.Contains(it))).ToList(); + _applicationLibrary.SaveDownloadableContentsForGame(_applicationData, dlcs); + // _downloadableContentContainerList.Clear(); - DownloadableContentContainer container = default; - - foreach (DownloadableContentModel downloadableContent in DownloadableContents) - { - if (container.ContainerPath != downloadableContent.ContainerPath) - { - if (!string.IsNullOrWhiteSpace(container.ContainerPath)) - { - _downloadableContentContainerList.Add(container); - } - - container = new DownloadableContentContainer - { - ContainerPath = downloadableContent.ContainerPath, - DownloadableContentNcaList = new List(), - }; - } - - container.DownloadableContentNcaList.Add(new DownloadableContentNca - { - Enabled = SelectedDownloadableContents.Contains(downloadableContent), - TitleId = downloadableContent.TitleId, - FullPath = downloadableContent.FullPath, - }); - } - - if (!string.IsNullOrWhiteSpace(container.ContainerPath)) - { - _downloadableContentContainerList.Add(container); - } - - JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer); + // DownloadableContentContainer container = default; + // + // foreach (DownloadableContentModel downloadableContent in DownloadableContents) + // { + // if (container.ContainerPath != downloadableContent.ContainerPath) + // { + // if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + // { + // _downloadableContentContainerList.Add(container); + // } + // + // container = new DownloadableContentContainer + // { + // ContainerPath = downloadableContent.ContainerPath, + // DownloadableContentNcaList = new List(), + // }; + // } + // + // container.DownloadableContentNcaList.Add(new DownloadableContentNca + // { + // Enabled = SelectedDownloadableContents.Contains(downloadableContent), + // TitleId = downloadableContent.TitleId, + // FullPath = downloadableContent.FullPath, + // }); + // } + // + // if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + // { + // _downloadableContentContainerList.Add(container); + // } + // + // JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer); } } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index b59bf5009..dd2b6a14a 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -653,8 +653,8 @@ namespace Ryujinx.Ava.UI.Windows { ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; TimeIt("games", () => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs)); - TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); - TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents(ConfigurationState.Instance.UI.GameDirs)); + // TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); + TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents()); _isLoading = false; })