From 5e0b1ccd6e39f1c0943f26e584745ec45d3f09d7 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Wed, 14 Aug 2024 21:54:32 -0400 Subject: [PATCH 01/31] Add hooks to ApplicationLibrary for loading DLC/updates --- .../App/ApplicationLibrary.cs | 359 +++++++++++++++++- .../App/DownloadableContentAddedEventArgs.cs | 11 + .../App/TitleUpdateAddedEventArgs.cs | 10 + 3 files changed, 373 insertions(+), 7 deletions(-) create mode 100644 src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs create mode 100644 src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 2defc1f6c..f26014821 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -16,6 +16,7 @@ using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.HLE.Utilities; using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration.System; using System; @@ -37,6 +38,8 @@ namespace Ryujinx.UI.App.Common public Language DesiredLanguage { get; set; } public event EventHandler ApplicationAdded; public event EventHandler ApplicationCountUpdated; + public event EventHandler TitleUpdateAdded; + public event EventHandler DownloadableContentAdded; private readonly byte[] _nspIcon; private readonly byte[] _xciIcon; @@ -275,7 +278,7 @@ namespace Ryujinx.UI.App.Common catch (FileNotFoundException) { Logger.Warning?.Print(LogClass.Application, $"The file was not found: '{applicationPath}'"); - + return false; } @@ -473,6 +476,125 @@ namespace Ryujinx.UI.App.Common return true; } + + public bool TryGetDownloadableContentFromFile(string filePath, out List<(ulong TitleId, string ContainerPath, string FullPath)> titleUpdates) + { + titleUpdates = []; + + try + { + string extension = Path.GetExtension(filePath).ToLower(); + + using FileStream file = new(filePath, FileMode.Open, FileAccess.Read); + + switch (extension) + { + case ".xci": + case ".nsp": + { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); + // Dictionary updates = pfs.GetContentData(ContentMetaType.AddOnContent, _virtualFileSystem, checkLevel); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage()); + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + titleUpdates.Add((nca.Header.TitleId, filePath, fileEntry.FullPath)); + } + } + + if (titleUpdates.Count == 0) + { + return false; + } + return true; + } + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); + } + return false; + } + + public bool TryGetTitleUpdatesFromFile(string filePath, out List<(ulong, string)> titleUpdates) + { + titleUpdates = []; + + try + { + string extension = Path.GetExtension(filePath).ToLower(); + + using FileStream file = new(filePath, FileMode.Open, FileAccess.Read); + + switch (extension) + { + case ".xci": + case ".nsp": + { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); + + Dictionary updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel); + if (updates.Count == 0) + { + return false; + } + + titleUpdates.AddRange(updates.Select(it => (it.Key, filePath))); + + return true; + } + } + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); + } + return false; + } public void CancelLoading() { @@ -524,12 +646,12 @@ namespace Ryujinx.UI.App.Common IEnumerable files = Directory.EnumerateFiles(appDir, "*", options).Where(file => { return - (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || - (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) || - (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) || - (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) || - (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) || - (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value); + (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || + (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) || + (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) || + (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) || + (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) || + (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value); }); foreach (string app in files) @@ -610,6 +732,208 @@ namespace Ryujinx.UI.App.Common } } + public void LoadDownloadableContents(List appDirs) + { + // Logger.Warning?.Print(LogClass.Application, "JIMMY load DLC"); + _cancellationToken = new CancellationTokenSource(); + + // Builds the applications list with paths to found applications + List applicationPaths = new(); + + try + { + foreach (string appDir in appDirs) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, + $"The specified game directory \"{appDir}\" does not exist."); + + continue; + } + + try + { + EnumerationOptions options = new() + { + RecurseSubdirectories = true, IgnoreInaccessible = false, + }; + + IEnumerable files = Directory.EnumerateFiles(appDir, "*", options).Where( + file => + { + return + (Path.GetExtension(file).ToLower() is ".nsp" && + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || + (Path.GetExtension(file).ToLower() is ".xci" && + ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); + }); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + var fileInfo = new FileInfo(app); + + try + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; + + applicationPaths.Add(fullPath); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to get access to directory: \"{appDir}\""); + } + } + + // Loops through applications list, creating a struct and then firing an event containing the struct for each application + foreach (string applicationPath in applicationPaths) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + if (TryGetDownloadableContentFromFile(applicationPath, out List<(ulong, string, string)> applications)) + { + foreach (var application in applications) + { + OnDownloadableContentAdded(new DownloadableContentAddedEventArgs + { + TitleId = application.Item1, + ContainerFilePath = application.Item2, + NcaPath = application.Item3 + }); + } + } + } + } + finally + { + _cancellationToken.Dispose(); + _cancellationToken = null; + } + + } + + public void LoadTitleUpdates(List appDirs) + { + // Logger.Warning?.Print(LogClass.Application, "JIMMY title updates"); + _cancellationToken = new CancellationTokenSource(); + + // Builds the applications list with paths to found applications + List applicationPaths = new(); + + try + { + foreach (string appDir in appDirs) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, + $"The specified game directory \"{appDir}\" does not exist."); + + continue; + } + + try + { + EnumerationOptions options = new() + { + RecurseSubdirectories = true, IgnoreInaccessible = false, + }; + + IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) + .Where(file => + { + return + (Path.GetExtension(file).ToLower() is ".nsp" && + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || + (Path.GetExtension(file).ToLower() is ".xci" && + ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); + }); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + var fileInfo = new FileInfo(app); + + try + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? + fileInfo.FullName; + + applicationPaths.Add(fullPath); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to get access to directory: \"{appDir}\""); + } + } + + // Loops through applications list, creating a struct and then firing an event containing the struct for each application + foreach (string applicationPath in applicationPaths) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + if (TryGetTitleUpdatesFromFile(applicationPath, + out List<(ulong, string)> titleUpdates)) + { + foreach (var application in titleUpdates) + { + OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() + { + TitleId = application.Item1, + FilePath = application.Item2, + }); + } + } + } + } + finally + { + _cancellationToken.Dispose(); + _cancellationToken = null; + } + } + protected void OnApplicationAdded(ApplicationAddedEventArgs e) { ApplicationAdded?.Invoke(null, e); @@ -619,6 +943,16 @@ namespace Ryujinx.UI.App.Common { ApplicationCountUpdated?.Invoke(null, e); } + + protected void OnTitleUpdateAdded(TitleUpdateAddedEventArgs e) + { + TitleUpdateAdded?.Invoke(null, e); + } + + protected void OnDownloadableContentAdded(DownloadableContentAddedEventArgs e) + { + DownloadableContentAdded?.Invoke(null, e); + } public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { @@ -936,5 +1270,16 @@ namespace Ryujinx.UI.App.Common return false; } + + private Nca TryOpenNca(IStorage ncaStorage) + { + try + { + return new Nca(_virtualFileSystem.KeySet, ncaStorage); + } + catch (Exception ex) { } + + return null; + } } } diff --git a/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs new file mode 100644 index 000000000..ebf70b8a3 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Ryujinx.UI.App.Common +{ + public class DownloadableContentAddedEventArgs : EventArgs + { + public ulong TitleId { get; set; } + public string ContainerFilePath { get; set; } + public string NcaPath { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs new file mode 100644 index 000000000..e28e3cc3c --- /dev/null +++ b/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.UI.App.Common +{ + public class TitleUpdateAddedEventArgs : EventArgs + { + public ulong TitleId { get; set; } + public string FilePath { get; set; } + } +} From 2e93c96c866d07ad12bdd695e4bfbef3bd6c1216 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Wed, 14 Aug 2024 21:54:58 -0400 Subject: [PATCH 02/31] Trigger DLC/update load on games refresh --- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 348412e78..884b717c7 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -144,6 +144,16 @@ namespace Ryujinx.Ava.UI.Windows }); } + private void ApplicationLibrary_DownloadableContentAdded(object sender, DownloadableContentAddedEventArgs e) + { + // Console.WriteLine("[{0}]: {1} ({2})", e.TitleId, e.ContainerFilePath, e.NcaPath); + } + + private void ApplicationLibrary_TitleUpdateAdded(object sender, TitleUpdateAddedEventArgs e) + { + // Console.WriteLine("[{0}]: {1}", e.TitleId, e.FilePath); + } + private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound); @@ -473,6 +483,8 @@ namespace Ryujinx.Ava.UI.Windows ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + ApplicationLibrary.DownloadableContentAdded += ApplicationLibrary_DownloadableContentAdded; + ApplicationLibrary.TitleUpdateAdded += ApplicationLibrary_TitleUpdateAdded; ViewModel.RefreshFirmwareStatus(); @@ -638,7 +650,9 @@ namespace Ryujinx.Ava.UI.Windows Thread applicationLibraryThread = new(() => { ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; - ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs); + TimeIt("games", () => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs)); + TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); + TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents(ConfigurationState.Instance.UI.GameDirs)); _isLoading = false; }) @@ -648,5 +662,14 @@ namespace Ryujinx.Ava.UI.Windows }; applicationLibraryThread.Start(); } + + private static void TimeIt(string tag, Action act) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + act(); + watch.Stop(); + var elapsedMs = watch.ElapsedMilliseconds; + Console.WriteLine("[{0}] {1} ms", tag, elapsedMs); + } } } From e1171086f4006593e137351974fbd270e026d803 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Thu, 15 Aug 2024 21:47:04 -0400 Subject: [PATCH 03/31] Initial moving of DLC/updates to UI.Common --- .../App/ApplicationLibrary.cs | 7 +--- .../Models/DownloadableContentModel.cs | 23 ++++++++++ .../Models/TitleUpdateModel.cs | 22 ++++++++++ .../DownloadableContentLabelConverter.cs | 42 +++++++++++++++++++ .../UI/Helpers/TitleUpdateLabelConverter.cs | 42 +++++++++++++++++++ .../UI/Models/DownloadableContentModel.cs | 39 ----------------- src/Ryujinx/UI/Models/TitleUpdateModel.cs | 21 ---------- .../DownloadableContentManagerViewModel.cs | 9 ++-- .../UI/ViewModels/TitleUpdateViewModel.cs | 3 +- .../DownloadableContentManagerWindow.axaml | 16 +++++-- .../DownloadableContentManagerWindow.axaml.cs | 5 ++- .../UI/Windows/TitleUpdateWindow.axaml | 16 +++++-- .../UI/Windows/TitleUpdateWindow.axaml.cs | 1 + 13 files changed, 167 insertions(+), 79 deletions(-) create mode 100644 src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs create mode 100644 src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs create mode 100644 src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs create mode 100644 src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs delete mode 100644 src/Ryujinx/UI/Models/DownloadableContentModel.cs delete mode 100644 src/Ryujinx/UI/Models/TitleUpdateModel.cs diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index f26014821..99e4dc078 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -497,7 +497,6 @@ namespace Ryujinx.UI.App.Common : IntegrityCheckLevel.None; using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); - // Dictionary updates = pfs.GetContentData(ContentMetaType.AddOnContent, _virtualFileSystem, checkLevel); foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { @@ -517,11 +516,7 @@ namespace Ryujinx.UI.App.Common } } - if (titleUpdates.Count == 0) - { - return false; - } - return true; + return titleUpdates.Count != 0; } } } diff --git a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs new file mode 100644 index 000000000..6668fb212 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs @@ -0,0 +1,23 @@ +namespace Ryujinx.UI.Common.Models +{ + public class DownloadableContentModel + { + public ulong TitleId { get; } + public string ContainerPath { get; } + public string FullPath { get; } + public bool Enabled { get; } + public bool IsBundled { get; } + + public string FileName => System.IO.Path.GetFileName(ContainerPath); + public string TitleIdStr => TitleId.ToString("X16"); + + 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 new file mode 100644 index 000000000..259a623f6 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs @@ -0,0 +1,22 @@ +namespace Ryujinx.UI.Common.Models +{ + public class TitleUpdateModel + { + public ulong TitleId { get; } + public ulong Version { get; } + public string DisplayVersion { get; } + public string Path { get; } + public bool IsBundled { get; } + + public string TitleIdStr => TitleId.ToString("X16"); + + 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/Helpers/DownloadableContentLabelConverter.cs b/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs new file mode 100644 index 000000000..0ef63e490 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class DownloadableContentLabelConverter : IMultiValueConverter + { + public static DownloadableContentLabelConverter Instance = new(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Any(it => it is UnsetValueType)) + { + return BindingOperations.DoNothing; + } + + if (values.Count != 2 || !targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (values is not [string label, bool isBundled]) + { + return null; + } + + return isBundled ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {label}" : label; + } + + public object[] ConvertBack(object[] values, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + } +} diff --git a/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs b/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs new file mode 100644 index 000000000..f454d7620 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class TitleUpdateLabelConverter : IMultiValueConverter + { + public static TitleUpdateLabelConverter Instance = new(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Any(it => it is UnsetValueType)) + { + return BindingOperations.DoNothing; + } + + if (values.Count != 2 || !targetType.IsAssignableFrom(typeof(string))) + { + return null; + } + + if (values is not [string label, bool isBundled]) + { + return null; + } + + var key = isBundled ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel; + return LocaleManager.Instance.UpdateAndGetDynamicValue(key, label); + } + + public object[] ConvertBack(object[] values, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Ryujinx/UI/Models/DownloadableContentModel.cs b/src/Ryujinx/UI/Models/DownloadableContentModel.cs deleted file mode 100644 index 1409d9713..000000000 --- a/src/Ryujinx/UI/Models/DownloadableContentModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.ViewModels; -using System.IO; - -namespace Ryujinx.Ava.UI.Models -{ - public class DownloadableContentModel : BaseModel - { - private bool _enabled; - - public bool Enabled - { - get => _enabled; - set - { - _enabled = value; - - OnPropertyChanged(); - } - } - - public string TitleId { get; } - public string ContainerPath { get; } - public string FullPath { get; } - - public string FileName => Path.GetFileName(ContainerPath); - - public string Label => - Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName; - - public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) - { - TitleId = titleId; - ContainerPath = containerPath; - FullPath = fullPath; - Enabled = enabled; - } - } -} diff --git a/src/Ryujinx/UI/Models/TitleUpdateModel.cs b/src/Ryujinx/UI/Models/TitleUpdateModel.cs deleted file mode 100644 index 46f6f46d8..000000000 --- a/src/Ryujinx/UI/Models/TitleUpdateModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Ryujinx.Ava.Common.Locale; - -namespace Ryujinx.Ava.UI.Models -{ - public class TitleUpdateModel - { - public uint Version { get; } - public string Path { get; } - public string Label { get; } - - public TitleUpdateModel(uint version, string displayVersion, string path) - { - Version = version; - Label = LocaleManager.Instance.UpdateAndGetDynamicValue( - System.IO.Path.GetExtension(path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel, - displayVersion - ); - Path = path; - } - } -} diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index c919a7ad1..defe99dde 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -19,6 +19,7 @@ using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Utilities; using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Models; using System; using System.Collections.Generic; using System.IO; @@ -142,7 +143,7 @@ namespace Ryujinx.Ava.UI.ViewModels Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath); if (nca != null) { - var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), + var content = new DownloadableContentModel(nca.Header.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath, downloadableContentNca.Enabled); @@ -183,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (arg is DownloadableContentModel content) { - return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleId.ToLower().Contains(_search.ToLower()); + return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleIdStr.ToLower().Contains(_search.ToLower()); } return false; @@ -261,7 +262,7 @@ namespace Ryujinx.Ava.UI.ViewModels continue; } - var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true); + var content = new DownloadableContentModel(nca.Header.TitleId, path, fileEntry.FullPath, true); DownloadableContents.Add(content); Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(content)); @@ -327,7 +328,7 @@ namespace Ryujinx.Ava.UI.ViewModels container.DownloadableContentNcaList.Add(new DownloadableContentNca { Enabled = downloadableContent.Enabled, - TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16), + TitleId = downloadableContent.TitleId, FullPath = downloadableContent.FullPath, }); } diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index e9b39dfe1..3f6cc46b7 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -20,6 +20,7 @@ using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Utilities; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Models; using System; using System.Collections.Generic; using System.IO; @@ -190,7 +191,7 @@ namespace Ryujinx.Ava.UI.ViewModels nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); var displayVersion = controlData.DisplayVersionString.ToString(); - var update = new TitleUpdateModel(content.Version.Version, displayVersion, path); + var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version, displayVersion, path); TitleUpdates.Add(update); diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index 98aac09ce..a94e78630 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -6,13 +6,17 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" + xmlns:models="clr-namespace:Ryujinx.UI.Common.Models;assembly=Ryujinx.UI.Common" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" Width="500" Height="380" mc:Ignorable="d" x:DataType="viewModels:DownloadableContentManagerViewModel" Focusable="True"> + + + @@ -96,8 +100,14 @@ VerticalAlignment="Center" MaxLines="2" TextWrapping="Wrap" - TextTrimming="CharacterEllipsis" - Text="{Binding Label}" /> + TextTrimming="CharacterEllipsis"> + + + + + + + + + + @@ -38,8 +42,14 @@ + TextWrapping="Wrap"> + + + + + + + Date: Thu, 15 Aug 2024 22:17:37 -0400 Subject: [PATCH 04/31] Use new models in ApplicationLibrary --- .../App/ApplicationLibrary.cs | 60 +++++++++++++------ .../App/DownloadableContentAddedEventArgs.cs | 5 +- .../App/TitleUpdateAddedEventArgs.cs | 4 +- .../Models/DownloadableContentModel.cs | 1 + .../Models/TitleUpdateModel.cs | 1 + src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 6 +- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 99e4dc078..7339d725c 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -19,6 +19,7 @@ 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.Models; using System; using System.Collections.Generic; using System.IO; @@ -29,6 +30,7 @@ using System.Text.Json; using System.Threading; using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; +using SpanHelpers = LibHac.Common.SpanHelpers; using TimeSpan = System.TimeSpan; namespace Ryujinx.UI.App.Common @@ -477,7 +479,7 @@ namespace Ryujinx.UI.App.Common return true; } - public bool TryGetDownloadableContentFromFile(string filePath, out List<(ulong TitleId, string ContainerPath, string FullPath)> titleUpdates) + public bool TryGetDownloadableContentFromFile(string filePath, out List titleUpdates) { titleUpdates = []; @@ -512,7 +514,7 @@ namespace Ryujinx.UI.App.Common if (nca.Header.ContentType == NcaContentType.PublicData) { - titleUpdates.Add((nca.Header.TitleId, filePath, fileEntry.FullPath)); + titleUpdates.Add(new DownloadableContentModel(nca.Header.TitleId, filePath, fileEntry.FullPath, false)); } } @@ -539,7 +541,7 @@ namespace Ryujinx.UI.App.Common return false; } - public bool TryGetTitleUpdatesFromFile(string filePath, out List<(ulong, string)> titleUpdates) + public bool TryGetTitleUpdatesFromFile(string filePath, out List titleUpdates) { titleUpdates = []; @@ -557,17 +559,43 @@ namespace Ryujinx.UI.App.Common IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; - - using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); - Dictionary updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel); + using IFileSystem pfs = + PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); + + Dictionary updates = + pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel); + if (updates.Count == 0) { return false; } - titleUpdates.AddRange(updates.Select(it => (it.Key, filePath))); + foreach ((_, ContentMetaData content) in updates) + { + Nca patchNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); + Nca 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, filePath); + + titleUpdates.Add(update); + } + } + return true; } } @@ -729,7 +757,6 @@ namespace Ryujinx.UI.App.Common public void LoadDownloadableContents(List appDirs) { - // Logger.Warning?.Print(LogClass.Application, "JIMMY load DLC"); _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications @@ -806,15 +833,13 @@ namespace Ryujinx.UI.App.Common return; } - if (TryGetDownloadableContentFromFile(applicationPath, out List<(ulong, string, string)> applications)) + if (TryGetDownloadableContentFromFile(applicationPath, out List downloadableContents)) { - foreach (var application in applications) + foreach (var downloadableContent in downloadableContents) { OnDownloadableContentAdded(new DownloadableContentAddedEventArgs { - TitleId = application.Item1, - ContainerFilePath = application.Item2, - NcaPath = application.Item3 + DownloadableContent = downloadableContent, }); } } @@ -830,7 +855,6 @@ namespace Ryujinx.UI.App.Common public void LoadTitleUpdates(List appDirs) { - // Logger.Warning?.Print(LogClass.Application, "JIMMY title updates"); _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications @@ -908,15 +932,13 @@ namespace Ryujinx.UI.App.Common return; } - if (TryGetTitleUpdatesFromFile(applicationPath, - out List<(ulong, string)> titleUpdates)) + if (TryGetTitleUpdatesFromFile(applicationPath, out List titleUpdates)) { - foreach (var application in titleUpdates) + foreach (var titleUpdate in titleUpdates) { OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() { - TitleId = application.Item1, - FilePath = application.Item2, + TitleUpdate = titleUpdate, }); } } diff --git a/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs index ebf70b8a3..f81caa62b 100644 --- a/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs +++ b/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs @@ -1,11 +1,10 @@ +using Ryujinx.UI.Common.Models; using System; namespace Ryujinx.UI.App.Common { public class DownloadableContentAddedEventArgs : EventArgs { - public ulong TitleId { get; set; } - public string ContainerFilePath { get; set; } - public string NcaPath { get; set; } + public DownloadableContentModel DownloadableContent { get; set; } } } diff --git a/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs index e28e3cc3c..bb266d639 100644 --- a/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs +++ b/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs @@ -1,10 +1,10 @@ +using Ryujinx.UI.Common.Models; using System; namespace Ryujinx.UI.App.Common { public class TitleUpdateAddedEventArgs : EventArgs { - public ulong TitleId { get; set; } - public string FilePath { get; set; } + public TitleUpdateModel TitleUpdate { get; set; } } } diff --git a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs index 6668fb212..cea338592 100644 --- a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs +++ b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs @@ -10,6 +10,7 @@ namespace Ryujinx.UI.Common.Models 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) { diff --git a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs index 259a623f6..2f89d16b3 100644 --- a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs @@ -9,6 +9,7 @@ namespace Ryujinx.UI.Common.Models public bool IsBundled { get; } public string TitleIdStr => TitleId.ToString("X16"); + public ulong TitleIdBase => TitleId & ~0x1FFFUL; public TitleUpdateModel(ulong titleId, ulong version, string displayVersion, string path) { diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 884b717c7..b59bf5009 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -146,12 +146,14 @@ namespace Ryujinx.Ava.UI.Windows private void ApplicationLibrary_DownloadableContentAdded(object sender, DownloadableContentAddedEventArgs e) { - // Console.WriteLine("[{0}]: {1} ({2})", e.TitleId, e.ContainerFilePath, e.NcaPath); + var it = e.DownloadableContent; + Console.WriteLine("[{0}]: {1} ({2})", it.TitleIdBase, it.ContainerPath, it.FullPath); } private void ApplicationLibrary_TitleUpdateAdded(object sender, TitleUpdateAddedEventArgs e) { - // Console.WriteLine("[{0}]: {1}", e.TitleId, e.FilePath); + var it = e.TitleUpdate; + Console.WriteLine("[{0}]: {1}", it.TitleIdBase, it.Path); } private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) From 48b7517284daeac3fdc982da8192ce170630b061 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Thu, 15 Aug 2024 23:41:12 -0400 Subject: [PATCH 05/31] Make dlc/updates records; use ApplicationLibrary for loading logic --- .../App/ApplicationLibrary.cs | 4 +- .../Models/DownloadableContentModel.cs | 17 +---- .../Models/TitleUpdateModel.cs | 19 +---- .../Controls/ApplicationContextMenu.axaml.cs | 4 +- .../DownloadableContentManagerViewModel.cs | 55 ++++++++------- .../UI/ViewModels/TitleUpdateViewModel.cs | 70 +++++++------------ .../DownloadableContentManagerWindow.axaml | 3 +- .../DownloadableContentManagerWindow.axaml.cs | 22 ++---- .../UI/Windows/TitleUpdateWindow.axaml.cs | 8 +-- 9 files changed, 77 insertions(+), 125 deletions(-) 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), }; From 472feb968076e465e5e46d0f78c387060c3fdcbf Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Fri, 16 Aug 2024 20:17:21 -0400 Subject: [PATCH 06/31] Fix a bug with DLC window; rework some logic --- .../DownloadableContentManagerViewModel.cs | 12 +++++- .../UI/ViewModels/TitleUpdateViewModel.cs | 39 ++++++------------- .../DownloadableContentManagerWindow.axaml | 3 +- .../UI/Windows/TitleUpdateWindow.axaml | 2 +- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 45496722b..9f96ed4a5 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -176,12 +176,22 @@ namespace Ryujinx.Ava.UI.ViewModels .Filter(Filter) .Bind(out var view).AsObservableList(); + // NOTE(jpr): this works around a bug where calling _views.Clear also clears SelectedDownloadableContents for + // some reason. so we save the items here and add them back after + var items = SelectedDownloadableContents.ToArray(); + _views.Clear(); _views.AddRange(view); + + foreach (DownloadableContentModel item in items) + { + SelectedDownloadableContents.ReplaceOrAdd(item, item); + } + OnPropertyChanged(nameof(Views)); } - private bool Filter(object arg) + private bool Filter(T arg) { if (arg is DownloadableContentModel content) { diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index 8d651d6cf..605bf6aee 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -2,24 +2,14 @@ using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; using Avalonia.Threading; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Ncm; -using LibHac.Ns; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; +using DynamicData.Kernel; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.HLE.Utilities; using Ryujinx.UI.App.Common; -using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Models; using System; using System.Collections.Generic; @@ -27,14 +17,15 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Application = Avalonia.Application; -using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; -using SpanHelpers = LibHac.Common.SpanHelpers; namespace Ryujinx.Ava.UI.ViewModels { + public record TitleUpdateViewNoUpdateSentinal(); + public class TitleUpdateViewModel : BaseModel { + public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } @@ -43,7 +34,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList _titleUpdates = new(); private AvaloniaList _views = new(); - private object _selectedUpdate; + private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal(); private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -123,9 +114,8 @@ namespace Ryujinx.Ava.UI.ViewModels AddUpdate(path); } - TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == TitleUpdateWindowData.Selected, null); - - SelectedUpdate = selected; + var selected = TitleUpdates.FirstOrOptional(x => x.Path == TitleUpdateWindowData.Selected); + SelectedUpdate = selected.HasValue ? selected.Value : new TitleUpdateViewNoUpdateSentinal(); // NOTE: Save the list again to remove leftovers. Save(); @@ -137,23 +127,16 @@ namespace Ryujinx.Ava.UI.ViewModels var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version); Views.Clear(); - Views.Add(new BaseModel()); + Views.Add(new TitleUpdateViewNoUpdateSentinal()); Views.AddRange(sortedUpdates); - if (SelectedUpdate == null) + if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal) { SelectedUpdate = Views[0]; } - else if (!TitleUpdates.Contains(SelectedUpdate)) + else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate)) { - if (Views.Count > 1) - { - SelectedUpdate = Views[1]; - } - else - { - SelectedUpdate = Views[0]; - } + SelectedUpdate = Views.Count > 1 ? Views[1] : Views[0]; } } diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index 9ba655e0e..fa6116780 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -77,9 +77,8 @@ SelectionMode="Multiple, Toggle" Background="Transparent" SelectionChanged="OnSelectionChanged" - SelectedItems="{Binding SelectedDownloadableContents}" + SelectedItems="{Binding SelectedDownloadableContents, Mode=OneWay}" ItemsSource="{Binding Views}"> - diff --git a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml index c0e84279a..28fcd3343 100644 --- a/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml +++ b/src/Ryujinx/UI/Windows/TitleUpdateWindow.axaml @@ -82,7 +82,7 @@ + DataType="viewModels:TitleUpdateViewNoUpdateSentinal"> From a90a6b2786aae0815916a8624353b7bfeae4beb8 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Fri, 16 Aug 2024 22:14:33 -0400 Subject: [PATCH 07/31] Extract DLC json load logic --- .../Helper/DownloadableContentsHelper.cs | 107 ++++++++++++++++++ .../DownloadableContentManagerViewModel.cs | 36 ++---- 2 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs new file mode 100644 index 000000000..5e8ae911d --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -0,0 +1,107 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Utilities; +using Ryujinx.UI.Common.Models; +using System; +using System.Collections.Generic; +using System.IO; +using Path = System.IO.Path; + +namespace Ryujinx.UI.Common.Helper +{ + public static class DownloadableContentsHelper + { + private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public static List<(DownloadableContentModel, bool IsEnabled)> LoadSavedDownloadableContents(VirtualFileSystem vfs, ulong applicationIdBase) + { + var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("X16"), "dlc.json"); + + if (!File.Exists(downloadableContentJsonPath)) + { + return []; + } + + try + { + var downloadableContentContainerList = JsonHelper.DeserializeFromFile(downloadableContentJsonPath, + _serializerContext.ListDownloadableContentContainer); + return LoadDownloadableContents(vfs, downloadableContentContainerList); + } + catch + { + Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize."); + return []; + } + } + + private static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContents(VirtualFileSystem vfs, List downloadableContentContainers) + { + var result = new List<(DownloadableContentModel, bool IsEnabled)>(); + + foreach (DownloadableContentContainer downloadableContentContainer in downloadableContentContainers) + { + if (!File.Exists(downloadableContentContainer.ContainerPath)) + { + continue; + } + + using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, vfs); + + foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList) + { + using UniqueRef ncaFile = new(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + // Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath); + Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage()); + if (nca == null) + { + // result.Add((content, downloadableContentNca.Enabled)); + continue; + } + + var content = new DownloadableContentModel(nca.Header.TitleId, + downloadableContentContainer.ContainerPath, + downloadableContentNca.FullPath); + + result.Add((content, downloadableContentNca.Enabled)); + + // if (downloadableContentNca.Enabled) + // { + // SelectedDownloadableContents.Add(content); + // } + + // OnPropertyChanged(nameof(UpdateCount)); + } + } + + return result; + } + + private static Nca TryOpenNca(VirtualFileSystem vfs, IStorage ncaStorage) + { + try + { + return new Nca(vfs.KeySet, ncaStorage); + } + catch (Exception ex) + { + // Dispatcher.UIThread.InvokeAsync(async () => + // { + // await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath)); + // }); + } + + return null; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 9f96ed4a5..b25d88a24 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -19,6 +19,7 @@ using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Utilities; using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Models; using System; using System.Collections.Generic; @@ -130,36 +131,17 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { - foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) + var savedDlc = DownloadableContentsHelper.LoadSavedDownloadableContents(_virtualFileSystem, _applicationData.IdBase); + foreach ((DownloadableContentModel dlc, bool isEnabled) in savedDlc) { - if (File.Exists(downloadableContentContainer.ContainerPath)) + DownloadableContents.Add(dlc); + + if (isEnabled) { - using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, _virtualFileSystem); - - foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList) - { - using UniqueRef ncaFile = new(); - - partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath); - if (nca != null) - { - var content = new DownloadableContentModel(nca.Header.TitleId, - downloadableContentContainer.ContainerPath, - downloadableContentNca.FullPath); - - DownloadableContents.Add(content); - - if (downloadableContentNca.Enabled) - { - SelectedDownloadableContents.Add(content); - } - - OnPropertyChanged(nameof(UpdateCount)); - } - } + SelectedDownloadableContents.Add(dlc); } + + OnPropertyChanged(nameof(UpdateCount)); } // NOTE: Try to load downloadable contents from PFS last to preserve enabled state. From 7850a2b2aa47d2044ddf91c95cde4c86439c0459 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Fri, 16 Aug 2024 23:26:25 -0400 Subject: [PATCH 08/31] 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; }) From 47e2cc6f02e54de5a58765fa3ba45150c131cb84 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Fri, 16 Aug 2024 23:28:30 -0400 Subject: [PATCH 09/31] Run formatter --- .../App/ApplicationLibrary.cs | 44 ++++++++++--------- .../Helper/DownloadableContentsHelper.cs | 6 +-- .../DownloadableContentLabelConverter.cs | 4 +- .../UI/Helpers/TitleUpdateLabelConverter.cs | 6 +-- .../DownloadableContentManagerViewModel.cs | 10 ++--- .../UI/ViewModels/TitleUpdateViewModel.cs | 9 ++-- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 4 +- 7 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 4bcbb993e..987052fad 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -294,7 +294,7 @@ namespace Ryujinx.UI.App.Common catch (FileNotFoundException) { Logger.Warning?.Print(LogClass.Application, $"The file was not found: '{applicationPath}'"); - + return false; } @@ -492,7 +492,7 @@ namespace Ryujinx.UI.App.Common return true; } - + public bool TryGetDownloadableContentFromFile(string filePath, out List titleUpdates) { titleUpdates = []; @@ -511,9 +511,9 @@ namespace Ryujinx.UI.App.Common IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; - + using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); - + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { using var ncaFile = new UniqueRef(); @@ -554,7 +554,7 @@ namespace Ryujinx.UI.App.Common } return false; } - + public bool TryGetTitleUpdatesFromFile(string filePath, out List titleUpdates) { titleUpdates = []; @@ -579,7 +579,7 @@ namespace Ryujinx.UI.App.Common Dictionary updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel); - + if (updates.Count == 0) { return false; @@ -609,7 +609,7 @@ namespace Ryujinx.UI.App.Common titleUpdates.Add(update); } } - + return true; } } @@ -737,7 +737,7 @@ namespace Ryujinx.UI.App.Common AppData = application, }); } - + _applications.Edit(it => { foreach (var application in applications) @@ -783,7 +783,7 @@ namespace Ryujinx.UI.App.Common _downloadableContents.Edit(it => { it.Clear(); - + foreach (ApplicationData application in Applications.Items) { var res = DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); @@ -791,18 +791,18 @@ namespace Ryujinx.UI.App.Common } }); } - + 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() // { // @@ -812,7 +812,7 @@ namespace Ryujinx.UI.App.Common { _cancellationToken = new CancellationTokenSource(); _downloadableContents.Clear(); - + // Builds the applications list with paths to found applications List applicationPaths = new(); @@ -837,7 +837,8 @@ namespace Ryujinx.UI.App.Common { EnumerationOptions options = new() { - RecurseSubdirectories = true, IgnoreInaccessible = false, + RecurseSubdirectories = true, + IgnoreInaccessible = false, }; IEnumerable files = Directory.EnumerateFiles(appDir, "*", options).Where( @@ -896,7 +897,7 @@ namespace Ryujinx.UI.App.Common DownloadableContent = downloadableContent, }); } - + _downloadableContents.Edit(it => { foreach (var downloadableContent in downloadableContents) @@ -918,7 +919,7 @@ namespace Ryujinx.UI.App.Common { _cancellationToken = new CancellationTokenSource(); _titleUpdates.Clear(); - + // Builds the applications list with paths to found applications List applicationPaths = new(); @@ -943,7 +944,8 @@ namespace Ryujinx.UI.App.Common { EnumerationOptions options = new() { - RecurseSubdirectories = true, IgnoreInaccessible = false, + RecurseSubdirectories = true, + IgnoreInaccessible = false, }; IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) @@ -1003,7 +1005,7 @@ namespace Ryujinx.UI.App.Common TitleUpdate = titleUpdate, }); } - + _titleUpdates.Edit(it => { foreach (var titleUpdate in titleUpdates) @@ -1030,12 +1032,12 @@ namespace Ryujinx.UI.App.Common { ApplicationCountUpdated?.Invoke(null, e); } - + protected void OnTitleUpdateAdded(TitleUpdateAddedEventArgs e) { TitleUpdateAdded?.Invoke(null, e); } - + protected void OnDownloadableContentAdded(DownloadableContentAddedEventArgs e) { DownloadableContentAdded?.Invoke(null, e); @@ -1357,7 +1359,7 @@ namespace Ryujinx.UI.App.Common return false; } - + private Nca TryOpenNca(IStorage ncaStorage) { try diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs index 31cc757e5..940dd576f 100644 --- a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -19,7 +19,7 @@ namespace Ryujinx.UI.Common.Helper public static class DownloadableContentsHelper { private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - + public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase) { // _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); @@ -86,7 +86,7 @@ namespace Ryujinx.UI.Common.Helper private static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContents(VirtualFileSystem vfs, List downloadableContentContainers) { var result = new List<(DownloadableContentModel, bool IsEnabled)>(); - + foreach (DownloadableContentContainer downloadableContentContainer in downloadableContentContainers) { if (!File.Exists(downloadableContentContainer.ContainerPath)) @@ -127,7 +127,7 @@ namespace Ryujinx.UI.Common.Helper return result; } - + private static Nca TryOpenNca(VirtualFileSystem vfs, IStorage ncaStorage) { try diff --git a/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs b/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs index 0ef63e490..22193b97e 100644 --- a/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs +++ b/src/Ryujinx/UI/Helpers/DownloadableContentLabelConverter.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Ava.UI.Helpers { return BindingOperations.DoNothing; } - + if (values.Count != 2 || !targetType.IsAssignableFrom(typeof(string))) { return null; @@ -29,7 +29,7 @@ namespace Ryujinx.Ava.UI.Helpers { return null; } - + return isBundled ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {label}" : label; } diff --git a/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs b/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs index f454d7620..cbb6edff1 100644 --- a/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs +++ b/src/Ryujinx/UI/Helpers/TitleUpdateLabelConverter.cs @@ -12,14 +12,14 @@ namespace Ryujinx.Ava.UI.Helpers internal class TitleUpdateLabelConverter : IMultiValueConverter { public static TitleUpdateLabelConverter Instance = new(); - + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { if (values.Any(it => it is UnsetValueType)) { return BindingOperations.DoNothing; } - + if (values.Count != 2 || !targetType.IsAssignableFrom(typeof(string))) { return null; @@ -29,7 +29,7 @@ namespace Ryujinx.Ava.UI.Helpers { return null; } - + var key = isBundled ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel; return LocaleManager.Instance.UpdateAndGetDynamicValue(key, label); } diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index e0df5401e..dcfe57d4e 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -134,12 +134,12 @@ namespace Ryujinx.Ava.UI.ViewModels foreach ((DownloadableContentModel dlc, bool isEnabled) in _applicationLibrary.DownloadableContents.Items.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase)) { DownloadableContents.Add(dlc); - + if (isEnabled) { SelectedDownloadableContents.Add(dlc); } - + OnPropertyChanged(nameof(UpdateCount)); } @@ -163,12 +163,12 @@ namespace Ryujinx.Ava.UI.ViewModels _views.Clear(); _views.AddRange(view); - + foreach (DownloadableContentModel item in items) { SelectedDownloadableContents.ReplaceOrAdd(item, item); } - + OnPropertyChanged(nameof(Views)); } @@ -291,7 +291,7 @@ namespace Ryujinx.Ava.UI.ViewModels { SelectedDownloadableContents.ReplaceOrAdd(model, model); } - + public void Disable(DownloadableContentModel model) { SelectedDownloadableContents.Remove(model); diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index 605bf6aee..1af455201 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -22,10 +22,10 @@ using Path = System.IO.Path; namespace Ryujinx.Ava.UI.ViewModels { public record TitleUpdateViewNoUpdateSentinal(); - + public class TitleUpdateViewModel : BaseModel { - + public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } @@ -146,7 +146,7 @@ namespace Ryujinx.Ava.UI.ViewModels { return; } - + try { if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var titleUpdates)) @@ -175,7 +175,8 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate); } } - } catch (Exception ex) + } + catch (Exception ex) { Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path))); } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index dd2b6a14a..41e007c16 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -149,7 +149,7 @@ namespace Ryujinx.Ava.UI.Windows var it = e.DownloadableContent; Console.WriteLine("[{0}]: {1} ({2})", it.TitleIdBase, it.ContainerPath, it.FullPath); } - + private void ApplicationLibrary_TitleUpdateAdded(object sender, TitleUpdateAddedEventArgs e) { var it = e.TitleUpdate; @@ -664,7 +664,7 @@ namespace Ryujinx.Ava.UI.Windows }; applicationLibraryThread.Start(); } - + private static void TimeIt(string tag, Action act) { var watch = System.Diagnostics.Stopwatch.StartNew(); From 1eb7146b90495397e54f28c10fba47410c7a1b10 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sat, 17 Aug 2024 10:52:11 -0400 Subject: [PATCH 10/31] Run formatter for real --- src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs index 940dd576f..d0d9b00e1 100644 --- a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -134,7 +134,7 @@ namespace Ryujinx.UI.Common.Helper { return new Nca(vfs.KeySet, ncaStorage); } - catch (Exception ex) + catch (Exception) { // Dispatcher.UIThread.InvokeAsync(async () => // { From 57de6a7dc518d74724be044535223cba4b8e66cf Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sat, 17 Aug 2024 14:17:21 -0400 Subject: [PATCH 11/31] Refactor more logic out of DLC manager VM --- .../Helper/DownloadableContentsHelper.cs | 22 +-- .../Models/DownloadableContentModel.cs | 1 + .../Models/TitleUpdateModel.cs | 1 + src/Ryujinx/Assets/Locales/en_US.json | 2 + src/Ryujinx/UI/Helpers/Glyph.cs | 1 + src/Ryujinx/UI/Helpers/GlyphValueConverter.cs | 1 + .../DownloadableContentManagerViewModel.cs | 166 ++++++------------ .../DownloadableContentManagerWindow.axaml | 23 ++- 8 files changed, 87 insertions(+), 130 deletions(-) diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs index d0d9b00e1..0c5c1bdd8 100644 --- a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -22,8 +22,7 @@ namespace Ryujinx.UI.Common.Helper public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase) { - // _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); - var downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json"); + var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase); if (!File.Exists(downloadableContentJsonPath)) { @@ -77,9 +76,7 @@ namespace Ryujinx.UI.Common.Helper 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"); + var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase); JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer); } @@ -102,11 +99,9 @@ namespace Ryujinx.UI.Common.Helper partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - // Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath); Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage()); if (nca == null) { - // result.Add((content, downloadableContentNca.Enabled)); continue; } @@ -115,13 +110,6 @@ namespace Ryujinx.UI.Common.Helper downloadableContentNca.FullPath); result.Add((content, downloadableContentNca.Enabled)); - - // if (downloadableContentNca.Enabled) - // { - // SelectedDownloadableContents.Add(content); - // } - - // OnPropertyChanged(nameof(UpdateCount)); } } @@ -136,6 +124,7 @@ namespace Ryujinx.UI.Common.Helper } catch (Exception) { + // TODO(jpr): emit failure // Dispatcher.UIThread.InvokeAsync(async () => // { // await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath)); @@ -144,5 +133,10 @@ namespace Ryujinx.UI.Common.Helper return null; } + + private static string PathToGameDLCJson(ulong applicationIdBase) + { + return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json"); + } } } diff --git a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs index 16b4c9f75..95c64f078 100644 --- a/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs +++ b/src/Ryujinx.UI.Common/Models/DownloadableContentModel.cs @@ -1,5 +1,6 @@ namespace Ryujinx.UI.Common.Models { + // NOTE: most consuming code relies on this model being value-comparable public record DownloadableContentModel(ulong TitleId, string ContainerPath, string FullPath) { public bool IsBundled { get; } = 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 045dfe845..5422e1303 100644 --- a/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.UI.Common/Models/TitleUpdateModel.cs @@ -1,5 +1,6 @@ namespace Ryujinx.UI.Common.Models { + // NOTE: most consuming code relies on this model being value-comparable public record TitleUpdateModel(ulong TitleId, ulong Version, string DisplayVersion, string Path) { public bool IsBundled { get; } = System.IO.Path.GetExtension(Path)?.ToLower() == ".xci"; diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 74e18056b..c62c64ffa 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -711,7 +711,9 @@ "UpdateWindowTitle": "Title Update Manager", "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "{0} Downloadable Content(s)", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", diff --git a/src/Ryujinx/UI/Helpers/Glyph.cs b/src/Ryujinx/UI/Helpers/Glyph.cs index f257dc02c..a6888a67b 100644 --- a/src/Ryujinx/UI/Helpers/Glyph.cs +++ b/src/Ryujinx/UI/Helpers/Glyph.cs @@ -5,5 +5,6 @@ namespace Ryujinx.Ava.UI.Helpers List, Grid, Chip, + Important, } } diff --git a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs index 7da23648e..1544d33ae 100644 --- a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs +++ b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs @@ -14,6 +14,7 @@ namespace Ryujinx.Ava.UI.Helpers { Glyph.List, char.ConvertFromUtf32((int)Symbol.List) }, { Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll) }, { Glyph.Chip, char.ConvertFromUtf32(59748) }, + { Glyph.Important, char.ConvertFromUtf32((int)Symbol.Important) }, }; public GlyphValueConverter(string key) diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index dcfe57d4e..7f1e83d38 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -3,39 +3,22 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; using Avalonia.Threading; using DynamicData; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Tools.Fs; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; +using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.HLE.Utilities; using Ryujinx.UI.App.Common; -using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Models; -using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Application = Avalonia.Application; -using Path = System.IO.Path; namespace Ryujinx.Ava.UI.ViewModels { public class DownloadableContentManagerViewModel : BaseModel { - private readonly List _downloadableContentContainerList; - private readonly string _downloadableContentJsonPath; - - private readonly VirtualFileSystem _virtualFileSystem; private readonly ApplicationLibrary _applicationLibrary; private AvaloniaList _downloadableContents = new(); private AvaloniaList _views = new(); @@ -45,8 +28,6 @@ namespace Ryujinx.Ava.UI.ViewModels private readonly ApplicationData _applicationData; private readonly IStorageProvider _storageProvider; - private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public AvaloniaList DownloadableContents { get => _downloadableContents; @@ -97,7 +78,6 @@ namespace Ryujinx.Ava.UI.ViewModels public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) { - _virtualFileSystem = virtualFileSystem; _applicationLibrary = applicationLibrary; _applicationData = applicationData; @@ -107,31 +87,14 @@ namespace Ryujinx.Ava.UI.ViewModels _storageProvider = desktop.MainWindow.StorageProvider; } - _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json"); - - if (!File.Exists(_downloadableContentJsonPath)) - { - _downloadableContentContainerList = new List(); - - Save(); - } - - try - { - _downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, _serializerContext.ListDownloadableContentContainer); - } - catch - { - Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize."); - _downloadableContentContainerList = new List(); - } - LoadDownloadableContents(); } private void LoadDownloadableContents() { - foreach ((DownloadableContentModel dlc, bool isEnabled) in _applicationLibrary.DownloadableContents.Items.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase)) + var dlcs = _applicationLibrary.DownloadableContents.Items + .Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase); + foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) { DownloadableContents.Add(dlc); @@ -144,7 +107,10 @@ namespace Ryujinx.Ava.UI.ViewModels } // NOTE: Try to load downloadable contents from PFS last to preserve enabled state. - AddDownloadableContent(_applicationData.Path); + if (AddDownloadableContent(_applicationData.Path, out var newDlc) && newDlc > 0) + { + ShowNewDlcAddedDialog(newDlc); + } // NOTE: Save the list again to remove leftovers. Save(); @@ -153,7 +119,11 @@ namespace Ryujinx.Ava.UI.ViewModels public void Sort() { - DownloadableContents.AsObservableChangeSet() + DownloadableContents + // Sort bundled last + .OrderBy(it => it.IsBundled ? 0 : 1) + .ThenBy(it => it.TitleId) + .AsObservableChangeSet() .Filter(Filter) .Bind(out var view).AsObservableList(); @@ -182,23 +152,6 @@ namespace Ryujinx.Ava.UI.ViewModels return false; } - private Nca TryOpenNca(IStorage ncaStorage, string containerPath) - { - try - { - return new Nca(_virtualFileSystem.KeySet, ncaStorage); - } - catch (Exception ex) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath)); - }); - } - - return null; - } - public async void Add() { var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions @@ -216,20 +169,30 @@ namespace Ryujinx.Ava.UI.ViewModels }, }); + var totalDlcAdded = 0; foreach (var file in result) { - if (!AddDownloadableContent(file.Path.LocalPath)) + if (!AddDownloadableContent(file.Path.LocalPath, out var newDlcAdded)) { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); } + + totalDlcAdded += newDlcAdded; + } + + if (totalDlcAdded > 0) + { + await ShowNewDlcAddedDialog(0); } } - private bool AddDownloadableContent(string path) + private bool AddDownloadableContent(string path, out int numDlcAdded) { - if (!File.Exists(path) || _downloadableContentContainerList.Any(x => x.ContainerPath == path)) + numDlcAdded = 0; + + if (!File.Exists(path)) { - return true; + return false; } if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs)) @@ -237,41 +200,43 @@ namespace Ryujinx.Ava.UI.ViewModels return false; } - bool success = false; - foreach (var dlc in dlcs) + foreach (var dlc in dlcs.Where(dlc => dlc.TitleIdBase == _applicationData.IdBase)) { - if (dlc.TitleIdBase != _applicationData.IdBase) + if (!DownloadableContents.Contains(dlc)) { - continue; + DownloadableContents.Add(dlc); + Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc)); + + numDlcAdded++; } - - DownloadableContents.Add(dlc); - Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(dlc)); - - success = true; } - if (success) + if (numDlcAdded > 0) { OnPropertyChanged(nameof(UpdateCount)); Sort(); } - return success; + return true; } public void Remove(DownloadableContentModel model) { - DownloadableContents.Remove(model); SelectedDownloadableContents.Remove(model); - OnPropertyChanged(nameof(UpdateCount)); - Sort(); + + if (!model.IsBundled) + { + DownloadableContents.Remove(model); + OnPropertyChanged(nameof(UpdateCount)); + Sort(); + } } public void RemoveAll() { - DownloadableContents.Clear(); SelectedDownloadableContents.Clear(); + DownloadableContents.RemoveMany(DownloadableContents.Where(it => !it.IsBundled)); + OnPropertyChanged(nameof(UpdateCount)); Sort(); } @@ -301,40 +266,15 @@ namespace Ryujinx.Ava.UI.ViewModels { 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); + private Task ShowNewDlcAddedDialog(int numAdded) + { + var msg = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowDlcAddedMessage], numAdded); + return Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); + }); } } diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index fa6116780..440d1bd6b 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -19,13 +19,30 @@ + + + + + + + Grid.Row="1"> @@ -64,7 +81,7 @@ Date: Sat, 17 Aug 2024 15:18:08 -0400 Subject: [PATCH 12/31] Auto-load bundled DLC on startup --- .../App/ApplicationLibrary.cs | 27 ++++++++++++++++--- .../DownloadableContentManagerViewModel.cs | 15 +++-------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 987052fad..a7b0a080e 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -786,12 +786,33 @@ namespace Ryujinx.UI.App.Common foreach (ApplicationData application in Applications.Items) { - var res = DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); - it.AddOrUpdate(res); + var savedDlc = DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedDlc); + + if(TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) + { + var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); + + bool addedNewDlc = false; + foreach (var dlc in bundledDlc) + { + if (!savedDlcLookup.Contains(dlc)) + { + addedNewDlc = true; + it.AddOrUpdate((dlc, true)); + } + } + + if (addedNewDlc) + { + var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, gameDlcs); + } + } } }); } - + public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { _downloadableContents.Edit(it => diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 7f1e83d38..18e17ff64 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -21,8 +21,8 @@ namespace Ryujinx.Ava.UI.ViewModels { private readonly ApplicationLibrary _applicationLibrary; private AvaloniaList _downloadableContents = new(); - private AvaloniaList _views = new(); private AvaloniaList _selectedDownloadableContents = new(); + private AvaloniaList _views = new(); private string _search; private readonly ApplicationData _applicationData; @@ -106,14 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); } - // NOTE: Try to load downloadable contents from PFS last to preserve enabled state. - if (AddDownloadableContent(_applicationData.Path, out var newDlc) && newDlc > 0) - { - ShowNewDlcAddedDialog(newDlc); - } - - // NOTE: Save the list again to remove leftovers. - Save(); Sort(); } @@ -182,7 +174,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (totalDlcAdded > 0) { - await ShowNewDlcAddedDialog(0); + await ShowNewDlcAddedDialog(totalDlcAdded); } } @@ -205,7 +197,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (!DownloadableContents.Contains(dlc)) { DownloadableContents.Add(dlc); - Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc)); + SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc); numDlcAdded++; } @@ -276,6 +268,5 @@ namespace Ryujinx.Ava.UI.ViewModels await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); }); } - } } From a381cea311a4e5f746ddb9d167b8b4a96a73e970 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sat, 17 Aug 2024 17:10:27 -0400 Subject: [PATCH 13/31] Autoload DLC --- .../App/ApplicationLibrary.cs | 267 +++++++++--------- src/Ryujinx/Assets/Locales/en_US.json | 4 + src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 32 ++- 3 files changed, 166 insertions(+), 137 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index a7b0a080e..fbafffb27 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1,4 +1,5 @@ using DynamicData; +using DynamicData.Kernel; using LibHac; using LibHac.Common; using LibHac.Fs; @@ -813,6 +814,12 @@ namespace Ryujinx.UI.App.Common }); } + private void SaveDownloadableContentsForGame(ulong titleIdBase) + { + var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs); + } + public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { _downloadableContents.Edit(it => @@ -824,18 +831,12 @@ namespace Ryujinx.UI.App.Common }); } - // public void LoadTitleUpdates() - // { - // - // } - - public void AutoLoadDownloadableContents(List appDirs) + public int AutoLoadDownloadableContents(List appDirs) { _cancellationToken = new CancellationTokenSource(); - _downloadableContents.Clear(); - // Builds the applications list with paths to found applications - List applicationPaths = new(); + List dlcPaths = new(); + int newDlcLoaded = 0; try { @@ -843,7 +844,7 @@ namespace Ryujinx.UI.App.Common { if (_cancellationToken.Token.IsCancellationRequested) { - return; + return newDlcLoaded; } if (!Directory.Exists(appDir)) @@ -867,16 +868,14 @@ namespace Ryujinx.UI.App.Common { return (Path.GetExtension(file).ToLower() is ".nsp" && - ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || - (Path.GetExtension(file).ToLower() is ".xci" && - ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value); }); foreach (string app in files) { if (_cancellationToken.Token.IsCancellationRequested) { - return; + return newDlcLoaded; } var fileInfo = new FileInfo(app); @@ -885,7 +884,7 @@ namespace Ryujinx.UI.App.Common { var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; - applicationPaths.Add(fullPath); + dlcPaths.Add(fullPath); } catch (IOException exception) { @@ -901,31 +900,26 @@ namespace Ryujinx.UI.App.Common } } - // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applicationPaths) + var appIdLookup = Applications.Items.Select(it => it.IdBase).ToHashSet(); + + foreach (string dlcPath in dlcPaths) { if (_cancellationToken.Token.IsCancellationRequested) { - return; + return newDlcLoaded; } - if (TryGetDownloadableContentFromFile(applicationPath, out List downloadableContents)) + if (TryGetDownloadableContentFromFile(dlcPath, out var foundDlcs)) { - foreach (var downloadableContent in downloadableContents) + foreach (var dlc in foundDlcs.Where(it => appIdLookup.Contains(it.TitleIdBase))) { - OnDownloadableContentAdded(new DownloadableContentAddedEventArgs + if (!DownloadableContents.Items.Any(it => it.Dlc == dlc)) { - DownloadableContent = downloadableContent, - }); - } - - _downloadableContents.Edit(it => - { - foreach (var downloadableContent in downloadableContents) - { - it.AddOrUpdate((downloadableContent, true)); + _downloadableContents.AddOrUpdate((dlc, true)); + SaveDownloadableContentsForGame(dlc.TitleIdBase); + newDlcLoaded++; } - }); + } } } } @@ -934,114 +928,117 @@ namespace Ryujinx.UI.App.Common _cancellationToken.Dispose(); _cancellationToken = null; } + + return newDlcLoaded; } public void AutoLoadTitleUpdates(List appDirs) { - _cancellationToken = new CancellationTokenSource(); - _titleUpdates.Clear(); - - // Builds the applications list with paths to found applications - List applicationPaths = new(); - - try - { - foreach (string appDir in appDirs) - { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } - - if (!Directory.Exists(appDir)) - { - Logger.Warning?.Print(LogClass.Application, - $"The specified game directory \"{appDir}\" does not exist."); - - continue; - } - - try - { - EnumerationOptions options = new() - { - RecurseSubdirectories = true, - IgnoreInaccessible = false, - }; - - IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) - .Where(file => - { - return - (Path.GetExtension(file).ToLower() is ".nsp" && - ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || - (Path.GetExtension(file).ToLower() is ".xci" && - ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); - }); - - foreach (string app in files) - { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } - - var fileInfo = new FileInfo(app); - - try - { - var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? - fileInfo.FullName; - - applicationPaths.Add(fullPath); - } - catch (IOException exception) - { - Logger.Warning?.Print(LogClass.Application, - $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); - } - } - } - catch (UnauthorizedAccessException) - { - Logger.Warning?.Print(LogClass.Application, - $"Failed to get access to directory: \"{appDir}\""); - } - } - - // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applicationPaths) - { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } - - if (TryGetTitleUpdatesFromFile(applicationPath, out List titleUpdates)) - { - foreach (var titleUpdate in titleUpdates) - { - OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() - { - TitleUpdate = titleUpdate, - }); - } - - _titleUpdates.Edit(it => - { - foreach (var titleUpdate in titleUpdates) - { - it.AddOrUpdate((titleUpdate, false)); - } - }); - } - } - } - finally - { - _cancellationToken.Dispose(); - _cancellationToken = null; - } + return; + // _cancellationToken = new CancellationTokenSource(); + // _titleUpdates.Clear(); + // + // // Builds the applications list with paths to found applications + // List applicationPaths = new(); + // + // try + // { + // foreach (string appDir in appDirs) + // { + // if (_cancellationToken.Token.IsCancellationRequested) + // { + // return; + // } + // + // if (!Directory.Exists(appDir)) + // { + // Logger.Warning?.Print(LogClass.Application, + // $"The specified game directory \"{appDir}\" does not exist."); + // + // continue; + // } + // + // try + // { + // EnumerationOptions options = new() + // { + // RecurseSubdirectories = true, + // IgnoreInaccessible = false, + // }; + // + // IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) + // .Where(file => + // { + // return + // (Path.GetExtension(file).ToLower() is ".nsp" && + // ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || + // (Path.GetExtension(file).ToLower() is ".xci" && + // ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); + // }); + // + // foreach (string app in files) + // { + // if (_cancellationToken.Token.IsCancellationRequested) + // { + // return; + // } + // + // var fileInfo = new FileInfo(app); + // + // try + // { + // var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? + // fileInfo.FullName; + // + // applicationPaths.Add(fullPath); + // } + // catch (IOException exception) + // { + // Logger.Warning?.Print(LogClass.Application, + // $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + // } + // } + // } + // catch (UnauthorizedAccessException) + // { + // Logger.Warning?.Print(LogClass.Application, + // $"Failed to get access to directory: \"{appDir}\""); + // } + // } + // + // // Loops through applications list, creating a struct and then firing an event containing the struct for each application + // foreach (string applicationPath in applicationPaths) + // { + // if (_cancellationToken.Token.IsCancellationRequested) + // { + // return; + // } + // + // if (TryGetTitleUpdatesFromFile(applicationPath, out List titleUpdates)) + // { + // foreach (var titleUpdate in titleUpdates) + // { + // OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() + // { + // TitleUpdate = titleUpdate, + // }); + // } + // + // _titleUpdates.Edit(it => + // { + // foreach (var titleUpdate in titleUpdates) + // { + // it.AddOrUpdate((titleUpdate, false)); + // } + // }); + // } + // } + // } + // finally + // { + // _cancellationToken.Dispose(); + // _cancellationToken = null; + // } } protected void OnApplicationAdded(ApplicationAddedEventArgs e) diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index c62c64ffa..269ece7f1 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -709,11 +709,15 @@ "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Title Update Manager", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "{0} Downloadable Content(s)", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadDlcAndUpdateAddedMessage": "{0} new downloadable content(s) and {0} new update(s) added", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 41e007c16..f9267df53 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -485,8 +485,6 @@ namespace Ryujinx.Ava.UI.Windows ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; - ApplicationLibrary.DownloadableContentAdded += ApplicationLibrary_DownloadableContentAdded; - ApplicationLibrary.TitleUpdateAdded += ApplicationLibrary_TitleUpdateAdded; ViewModel.RefreshFirmwareStatus(); @@ -655,6 +653,14 @@ namespace Ryujinx.Ava.UI.Windows TimeIt("games", () => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs)); // TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents()); + // TODO(jpr): conditional + var dlcLoaded = 0; + TimeIt("AUTO DLC", () => dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(ConfigurationState.Instance.UI.GameDirs)); + + if (dlcLoaded > 0) + { + ShowNewContentAddedDialog(dlcLoaded, 0); + } _isLoading = false; }) @@ -673,5 +679,27 @@ namespace Ryujinx.Ava.UI.Windows var elapsedMs = watch.ElapsedMilliseconds; Console.WriteLine("[{0}] {1} ms", tag, elapsedMs); } + + private Task ShowNewContentAddedDialog(int numDlcAdded, int numUpdatesAdded) + { + var msg = ""; + + if (numDlcAdded > 0 && numUpdatesAdded > 0) + { + msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAndUpdateAddedMessage], numDlcAdded, numUpdatesAdded); + } else if (numDlcAdded > 0) + { + msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded); + } else if (numUpdatesAdded > 0) + { + msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded); + } + + return msg == "" ? Task.CompletedTask : Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], + msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); + }); + } } } From bc60126a2482171d171a2e713d18f64ce47aa898 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sat, 17 Aug 2024 17:26:13 -0400 Subject: [PATCH 14/31] Add setting for autoloading dlc/updates --- .../App/ApplicationLibrary.cs | 225 +++++++++--------- .../Configuration/ConfigurationFileFormat.cs | 7 +- .../Configuration/ConfigurationState.cs | 18 ++ src/Ryujinx/Assets/Locales/en_US.json | 3 +- .../UI/ViewModels/SettingsViewModel.cs | 3 + .../UI/Views/Settings/SettingsUIView.axaml | 3 + src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 37 ++- 7 files changed, 172 insertions(+), 124 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index fbafffb27..c92ef3b6a 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -779,6 +779,11 @@ namespace Ryujinx.UI.App.Common } } + public void LoadTitleUpdates() + { + return; + } + public void LoadDownloadableContents() { _downloadableContents.Edit(it => @@ -789,8 +794,8 @@ namespace Ryujinx.UI.App.Common { var savedDlc = DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); it.AddOrUpdate(savedDlc); - - if(TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) + + if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) { var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); @@ -813,13 +818,13 @@ namespace Ryujinx.UI.App.Common } }); } - + private void SaveDownloadableContentsForGame(ulong titleIdBase) { var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList(); DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs); } - + public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { _downloadableContents.Edit(it => @@ -932,113 +937,113 @@ namespace Ryujinx.UI.App.Common return newDlcLoaded; } - public void AutoLoadTitleUpdates(List appDirs) + public int AutoLoadTitleUpdates(List appDirs) { - return; - // _cancellationToken = new CancellationTokenSource(); - // _titleUpdates.Clear(); - // - // // Builds the applications list with paths to found applications - // List applicationPaths = new(); - // - // try - // { - // foreach (string appDir in appDirs) - // { - // if (_cancellationToken.Token.IsCancellationRequested) - // { - // return; - // } - // - // if (!Directory.Exists(appDir)) - // { - // Logger.Warning?.Print(LogClass.Application, - // $"The specified game directory \"{appDir}\" does not exist."); - // - // continue; - // } - // - // try - // { - // EnumerationOptions options = new() - // { - // RecurseSubdirectories = true, - // IgnoreInaccessible = false, - // }; - // - // IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) - // .Where(file => - // { - // return - // (Path.GetExtension(file).ToLower() is ".nsp" && - // ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || - // (Path.GetExtension(file).ToLower() is ".xci" && - // ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); - // }); - // - // foreach (string app in files) - // { - // if (_cancellationToken.Token.IsCancellationRequested) - // { - // return; - // } - // - // var fileInfo = new FileInfo(app); - // - // try - // { - // var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? - // fileInfo.FullName; - // - // applicationPaths.Add(fullPath); - // } - // catch (IOException exception) - // { - // Logger.Warning?.Print(LogClass.Application, - // $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); - // } - // } - // } - // catch (UnauthorizedAccessException) - // { - // Logger.Warning?.Print(LogClass.Application, - // $"Failed to get access to directory: \"{appDir}\""); - // } - // } - // - // // Loops through applications list, creating a struct and then firing an event containing the struct for each application - // foreach (string applicationPath in applicationPaths) - // { - // if (_cancellationToken.Token.IsCancellationRequested) - // { - // return; - // } - // - // if (TryGetTitleUpdatesFromFile(applicationPath, out List titleUpdates)) - // { - // foreach (var titleUpdate in titleUpdates) - // { - // OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() - // { - // TitleUpdate = titleUpdate, - // }); - // } - // - // _titleUpdates.Edit(it => - // { - // foreach (var titleUpdate in titleUpdates) - // { - // it.AddOrUpdate((titleUpdate, false)); - // } - // }); - // } - // } - // } - // finally - // { - // _cancellationToken.Dispose(); - // _cancellationToken = null; - // } + return 0; + // _cancellationToken = new CancellationTokenSource(); + // _titleUpdates.Clear(); + // + // // Builds the applications list with paths to found applications + // List applicationPaths = new(); + // + // try + // { + // foreach (string appDir in appDirs) + // { + // if (_cancellationToken.Token.IsCancellationRequested) + // { + // return; + // } + // + // if (!Directory.Exists(appDir)) + // { + // Logger.Warning?.Print(LogClass.Application, + // $"The specified game directory \"{appDir}\" does not exist."); + // + // continue; + // } + // + // try + // { + // EnumerationOptions options = new() + // { + // RecurseSubdirectories = true, + // IgnoreInaccessible = false, + // }; + // + // IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) + // .Where(file => + // { + // return + // (Path.GetExtension(file).ToLower() is ".nsp" && + // ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || + // (Path.GetExtension(file).ToLower() is ".xci" && + // ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); + // }); + // + // foreach (string app in files) + // { + // if (_cancellationToken.Token.IsCancellationRequested) + // { + // return; + // } + // + // var fileInfo = new FileInfo(app); + // + // try + // { + // var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? + // fileInfo.FullName; + // + // applicationPaths.Add(fullPath); + // } + // catch (IOException exception) + // { + // Logger.Warning?.Print(LogClass.Application, + // $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + // } + // } + // } + // catch (UnauthorizedAccessException) + // { + // Logger.Warning?.Print(LogClass.Application, + // $"Failed to get access to directory: \"{appDir}\""); + // } + // } + // + // // Loops through applications list, creating a struct and then firing an event containing the struct for each application + // foreach (string applicationPath in applicationPaths) + // { + // if (_cancellationToken.Token.IsCancellationRequested) + // { + // return; + // } + // + // if (TryGetTitleUpdatesFromFile(applicationPath, out List titleUpdates)) + // { + // foreach (var titleUpdate in titleUpdates) + // { + // OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() + // { + // TitleUpdate = titleUpdate, + // }); + // } + // + // _titleUpdates.Edit(it => + // { + // foreach (var titleUpdate in titleUpdates) + // { + // it.AddOrUpdate((titleUpdate, false)); + // } + // }); + // } + // } + // } + // finally + // { + // _cancellationToken.Dispose(); + // _cancellationToken = null; + // } } protected void OnApplicationAdded(ApplicationAddedEventArgs e) diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index af3ad0a1d..e60fac09b 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 51; + public const int CurrentVersion = 52; /// /// Version of the configuration file format @@ -167,6 +167,11 @@ namespace Ryujinx.UI.Common.Configuration /// public bool RememberWindowState { get; set; } + /// + /// Enables or disables automatically loading DLC/title updates on library refresh. + /// + public bool AutoloadContent { get; set; } + /// /// Enables hardware-accelerated rendering for Avalonia /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 8420dc5d9..4cd85aa01 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -632,6 +632,11 @@ namespace Ryujinx.UI.Common.Configuration /// public ReactiveObject RememberWindowState { get; private set; } + /// + /// Enables or disables automatically loading DLC/title updates on library refresh. + /// + public ReactiveObject AutoloadContent { get; private set; } + /// /// Enables hardware-accelerated rendering for Avalonia /// @@ -654,6 +659,7 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart = new ReactiveObject(); ShowConfirmExit = new ReactiveObject(); RememberWindowState = new ReactiveObject(); + AutoloadContent = new ReactiveObject(); EnableHardwareAcceleration = new ReactiveObject(); HideCursor = new ReactiveObject(); } @@ -692,6 +698,7 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart = CheckUpdatesOnStart, ShowConfirmExit = ShowConfirmExit, RememberWindowState = RememberWindowState, + AutoloadContent = AutoloadContent, EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, EnableVsync = Graphics.EnableVsync, @@ -801,6 +808,7 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart.Value = true; ShowConfirmExit.Value = true; RememberWindowState.Value = true; + AutoloadContent.Value = false; EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; @@ -1477,6 +1485,15 @@ namespace Ryujinx.UI.Common.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.AutoloadContent = false; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; @@ -1508,6 +1525,7 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; RememberWindowState.Value = configurationFileFormat.RememberWindowState; + AutoloadContent.Value = configurationFileFormat.AutoloadContent; EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; HideCursor.Value = configurationFileFormat.HideCursor; Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 269ece7f1..38e252076 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -98,6 +98,7 @@ "SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch", "SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog", "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", + "SettingsTabGeneralAutoloadContent": "Automatically load DLC/updates", "SettingsTabGeneralHideCursor": "Hide Cursor:", "SettingsTabGeneralHideCursorNever": "Never", "SettingsTabGeneralHideCursorOnIdle": "On Idle", @@ -709,7 +710,7 @@ "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Title Update Manager", - "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowDlcAddedMessage": "{0} new update(s) added", "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 70e5fa5c7..9223e578d 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -132,6 +132,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } public bool RememberWindowState { get; set; } + public bool AutoloadContent { get; set; } public int HideCursor { get; set; } public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } @@ -392,6 +393,7 @@ namespace Ryujinx.Ava.UI.ViewModels CheckUpdatesOnStart = config.CheckUpdatesOnStart; ShowConfirmExit = config.ShowConfirmExit; RememberWindowState = config.RememberWindowState; + AutoloadContent = config.AutoloadContent; HideCursor = (int)config.HideCursor.Value; GameDirectories.Clear(); @@ -484,6 +486,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; config.ShowConfirmExit.Value = ShowConfirmExit; config.RememberWindowState.Value = RememberWindowState; + config.AutoloadContent.Value = AutoloadContent; config.HideCursor.Value = (HideCursorMode)HideCursor; if (_directoryChanged) diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index f9b9be44b..9d26effcc 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -39,6 +39,9 @@ + + + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs)); - // TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); + TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates()); TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents()); - // TODO(jpr): conditional - var dlcLoaded = 0; - TimeIt("AUTO DLC", () => dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(ConfigurationState.Instance.UI.GameDirs)); - if (dlcLoaded > 0) + if (ConfigurationState.Instance.AutoloadContent) { - ShowNewContentAddedDialog(dlcLoaded, 0); + var updatesLoaded = 0; + TimeIt("auto updates", + () => updatesLoaded = + ApplicationLibrary.AutoLoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); + + var dlcLoaded = 0; + TimeIt("auto dlc", + () => dlcLoaded = + ApplicationLibrary.AutoLoadDownloadableContents(ConfigurationState.Instance.UI.GameDirs)); + + ShowNewContentAddedDialog(dlcLoaded, updatesLoaded); } _isLoading = false; @@ -679,23 +686,29 @@ namespace Ryujinx.Ava.UI.Windows var elapsedMs = watch.ElapsedMilliseconds; Console.WriteLine("[{0}] {1} ms", tag, elapsedMs); } - + private Task ShowNewContentAddedDialog(int numDlcAdded, int numUpdatesAdded) { var msg = ""; - + if (numDlcAdded > 0 && numUpdatesAdded > 0) { msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAndUpdateAddedMessage], numDlcAdded, numUpdatesAdded); - } else if (numDlcAdded > 0) + } + else if (numDlcAdded > 0) { msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded); - } else if (numUpdatesAdded > 0) + } + else if (numUpdatesAdded > 0) { msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded); } - - return msg == "" ? Task.CompletedTask : Dispatcher.UIThread.InvokeAsync(async () => + else + { + return Task.CompletedTask; + } + + return Dispatcher.UIThread.InvokeAsync(async () => { await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); From 20e0dbe97f1fdd946e86d380383c45c7c8964b9d Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sun, 18 Aug 2024 16:37:18 -0400 Subject: [PATCH 15/31] Remove dead code; bind to AppLibrary apps directly in mainwindow --- .../App/ApplicationLibrary.cs | 15 +-------- .../App/DownloadableContentAddedEventArgs.cs | 10 ------ .../App/TitleUpdateAddedEventArgs.cs | 10 ------ .../UI/ViewModels/MainWindowViewModel.cs | 9 +++--- .../UI/Views/Main/MainStatusBarView.axaml.cs | 2 +- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 32 ++++++------------- 6 files changed, 17 insertions(+), 61 deletions(-) delete mode 100644 src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs delete mode 100644 src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index c92ef3b6a..35b9ca185 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1,5 +1,4 @@ using DynamicData; -using DynamicData.Kernel; using LibHac; using LibHac.Common; using LibHac.Fs; @@ -24,7 +23,6 @@ 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; @@ -44,8 +42,6 @@ namespace Ryujinx.UI.App.Common public Language DesiredLanguage { get; set; } public event EventHandler ApplicationAdded; public event EventHandler ApplicationCountUpdated; - public event EventHandler TitleUpdateAdded; - public event EventHandler DownloadableContentAdded; public IObservableCache Applications; public IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; @@ -553,6 +549,7 @@ namespace Ryujinx.UI.App.Common { Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); } + return false; } @@ -1056,16 +1053,6 @@ namespace Ryujinx.UI.App.Common ApplicationCountUpdated?.Invoke(null, e); } - protected void OnTitleUpdateAdded(TitleUpdateAddedEventArgs e) - { - TitleUpdateAdded?.Invoke(null, e); - } - - protected void OnDownloadableContentAdded(DownloadableContentAddedEventArgs e) - { - DownloadableContentAdded?.Invoke(null, e); - } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); diff --git a/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs deleted file mode 100644 index f81caa62b..000000000 --- a/src/Ryujinx.UI.Common/App/DownloadableContentAddedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ryujinx.UI.Common.Models; -using System; - -namespace Ryujinx.UI.App.Common -{ - public class DownloadableContentAddedEventArgs : EventArgs - { - public DownloadableContentModel DownloadableContent { get; set; } - } -} diff --git a/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs deleted file mode 100644 index bb266d639..000000000 --- a/src/Ryujinx.UI.Common/App/TitleUpdateAddedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ryujinx.UI.Common.Models; -using System; - -namespace Ryujinx.UI.App.Common -{ - public class TitleUpdateAddedEventArgs : EventArgs - { - public TitleUpdateModel TitleUpdate { get; set; } - } -} diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index bd9f165b9..a61978155 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; using DynamicData; +using DynamicData.Alias; using DynamicData.Binding; using LibHac.Common; using Ryujinx.Ava.Common; @@ -50,7 +51,7 @@ namespace Ryujinx.Ava.UI.ViewModels { private const int HotKeyPressDelayMs = 500; - private ObservableCollection _applications; + private ObservableCollectionExtended _applications; private string _aspectStatusText; private string _loadHeading; @@ -112,8 +113,8 @@ namespace Ryujinx.Ava.UI.ViewModels public MainWindowViewModel() { - Applications = new ObservableCollection(); - + Applications = new ObservableCollectionExtended(); + Applications.ToObservableChangeSet() .Filter(Filter) .Sort(GetComparer()) @@ -741,7 +742,7 @@ namespace Ryujinx.Ava.UI.ViewModels get => FileAssociationHelper.IsTypeAssociationSupported; } - public ObservableCollection Applications + public ObservableCollectionExtended Applications { get => _applications; set diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs index 528495713..18d0f1a48 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs @@ -53,7 +53,7 @@ namespace Ryujinx.Ava.UI.Views.Main { Window.LoadApplications(); } - + private void VolumeStatus_OnPointerWheelChanged(object sender, PointerWheelEventArgs e) { // Change the volume by 5% at a time diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 8afa757b8..7947b4896 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.Threading; +using DynamicData; using FluentAvalonia.UI.Controls; using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; @@ -26,6 +27,7 @@ using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; +using System.Reactive.Linq; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -45,6 +47,7 @@ namespace Ryujinx.Ava.UI.Windows private static string _launchApplicationId; private static bool _startFullscreen; internal readonly AvaHostUIHandler UiHandler; + private IDisposable _appLibraryAppsSubscription; public VirtualFileSystem VirtualFileSystem { get; private set; } public ContentManager ContentManager { get; private set; } @@ -136,26 +139,6 @@ namespace Ryujinx.Ava.UI.Windows Program.DesktopScaleFactor = this.RenderScaling; } - private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) - { - Dispatcher.UIThread.Post(() => - { - ViewModel.Applications.Add(e.AppData); - }); - } - - private void ApplicationLibrary_DownloadableContentAdded(object sender, DownloadableContentAddedEventArgs e) - { - var it = e.DownloadableContent; - Console.WriteLine("[{0}]: {1} ({2})", it.TitleIdBase, it.ContainerPath, it.FullPath); - } - - private void ApplicationLibrary_TitleUpdateAdded(object sender, TitleUpdateAddedEventArgs e) - { - var it = e.TitleUpdate; - Console.WriteLine("[{0}]: {1}", it.TitleIdBase, it.Path); - } - private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound); @@ -484,7 +467,12 @@ namespace Ryujinx.Ava.UI.Windows this); ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; - ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + _appLibraryAppsSubscription?.Dispose(); + _appLibraryAppsSubscription = ApplicationLibrary.Applications + .Connect() + .ObserveOn(SynchronizationContext.Current) + .Bind(ViewModel.Applications) + .Subscribe(); ViewModel.RefreshFirmwareStatus(); @@ -587,6 +575,7 @@ namespace Ryujinx.Ava.UI.Windows ApplicationLibrary.CancelLoading(); InputManager.Dispose(); + _appLibraryAppsSubscription?.Dispose(); Program.Exit(); base.OnClosing(e); @@ -608,7 +597,6 @@ namespace Ryujinx.Ava.UI.Windows public void LoadApplications() { _applicationsLoadedOnce = true; - ViewModel.Applications.Clear(); StatusBarView.LoadProgressBar.IsVisible = true; ViewModel.StatusBarProgressMaximum = 0; From 50cd3add3f512693fd6d574929664d3a56032295 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sun, 18 Aug 2024 16:55:33 -0400 Subject: [PATCH 16/31] Stub out bulk dlc menu item --- .../UI/ViewModels/MainWindowViewModel.cs | 28 +++++++++++++++++++ .../UI/Views/Main/MainMenuBarView.axaml | 5 ++++ 2 files changed, 33 insertions(+) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index a61978155..45a8383bc 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -8,6 +8,7 @@ using Avalonia.Threading; using DynamicData; using DynamicData.Alias; using DynamicData.Binding; +using FluentAvalonia.UI.Controls; using LibHac.Common; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; @@ -39,6 +40,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Key = Ryujinx.Input.Key; @@ -1505,6 +1507,32 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public async Task BulkLoadDlc() + { + var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], AllowMultiple = true, + }); + + if (result.Count > 0) + { + var dirs = result.Select(it => it.Path.LocalPath).ToList(); + var numDlcAdded = ApplicationLibrary.AutoLoadDownloadableContents(dirs); + + if (numDlcAdded > 0) + { + var msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded); + + await Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], + msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); + }); + } + } + } + public async Task OpenFolder() { var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index ac3736110..ac0662982 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -34,6 +34,11 @@ Header="{locale:Locale MenuBarFileOpenUnpacked}" IsEnabled="{Binding EnableNonGameRunningControls}" ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" /> + Date: Sun, 18 Aug 2024 20:08:33 -0400 Subject: [PATCH 17/31] Add localization; stub out bulk load updates --- src/Ryujinx/Assets/Locales/en_US.json | 4 ++ .../UI/ViewModels/MainWindowViewModel.cs | 53 +++++++++++-------- .../UI/Views/Main/MainMenuBarView.axaml | 11 ++-- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 38e252076..6f050ec1b 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -12,6 +12,8 @@ "MenuBarFileOpenFromFile": "_Load Application From File", "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "Load _Unpacked Game", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Open Ryujinx Folder", "MenuBarFileOpenLogsFolder": "Open Logs Folder", "MenuBarFileExit": "_Exit", @@ -600,6 +602,8 @@ "DebugLogTooltip": "Prints debug log messages in the console.\n\nOnly use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance.", "LoadApplicationFileTooltip": "Open a file explorer to choose a Switch compatible file to load", "LoadApplicationFolderTooltip": "Open a file explorer to choose a Switch compatible, unpacked application to load", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Open Ryujinx filesystem folder", "OpenRyujinxLogsTooltip": "Opens the folder where logs are written to", "ExitTooltip": "Exit Ryujinx", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 45a8383bc..51cd8592b 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1259,6 +1259,29 @@ namespace Ryujinx.Ava.UI.ViewModels _rendererWaitEvent.Set(); } + private async Task LoadContentFromFolder(LocaleKeys localeMessageKey, Func, int> onDirsSelected) + { + var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], AllowMultiple = true, + }); + + if (result.Count > 0) + { + var dirs = result.Select(it => it.Path.LocalPath).ToList(); + var numAdded = onDirsSelected(dirs); + + var msg = string.Format(LocaleManager.Instance[localeMessageKey], numAdded); + + await Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.ShowTextDialog( + LocaleManager.Instance[numAdded > 0 ? LocaleKeys.RyujinxConfirm : LocaleKeys.RyujinxInfo], + msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); + }); + } + } + #endregion #region PublicMethods @@ -1507,30 +1530,16 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async Task BulkLoadDlc() + public async Task LoadDlcFromFolder() { - var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], AllowMultiple = true, - }); + await LoadContentFromFolder(LocaleKeys.AutoloadDlcAddedMessage, + dirs => ApplicationLibrary.AutoLoadDownloadableContents(dirs)); + } - if (result.Count > 0) - { - var dirs = result.Select(it => it.Path.LocalPath).ToList(); - var numDlcAdded = ApplicationLibrary.AutoLoadDownloadableContents(dirs); - - if (numDlcAdded > 0) - { - var msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded); - - await Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.ShowTextDialog( - LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], - msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); - }); - } - } + public async Task LoadTitleUpdatesFromFolder() + { + await LoadContentFromFolder(LocaleKeys.AutoloadUpdateAddedMessage, + dirs => ApplicationLibrary.AutoLoadTitleUpdates(dirs)); } public async Task OpenFolder() diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index ac0662982..e7815bba8 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -35,10 +35,15 @@ IsEnabled="{Binding EnableNonGameRunningControls}" ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" /> + ToolTip.Tip="{locale:Locale LoadDlcFromFolderTooltip}" /> + Date: Sun, 18 Aug 2024 20:45:48 -0400 Subject: [PATCH 18/31] Set autoload dirs explicitly --- .../Configuration/ConfigurationFileFormat.cs | 7 ++- .../Configuration/ConfigurationState.cs | 18 ++++++ src/Ryujinx/Assets/Locales/en_US.json | 4 ++ .../UI/ViewModels/SettingsViewModel.cs | 36 +++++++++-- .../UI/Views/Settings/SettingsUIView.axaml | 63 +++++++++++++++++-- .../UI/Views/Settings/SettingsUIView.axaml.cs | 63 ++++++++++++++++--- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 7 ++- .../UI/Windows/SettingsWindow.axaml.cs | 2 +- 8 files changed, 173 insertions(+), 27 deletions(-) diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index e60fac09b..2ccb67c70 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 52; + public const int CurrentVersion = 53; /// /// Version of the configuration file format @@ -266,6 +266,11 @@ namespace Ryujinx.UI.Common.Configuration /// A list of directories containing games to be used to load games into the games list /// public List GameDirs { get; set; } + + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public List AutoloadDirs { get; set; } /// /// A list of file types to be hidden in the games List diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 4cd85aa01..f9179d0c0 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -121,6 +121,11 @@ namespace Ryujinx.UI.Common.Configuration /// A list of directories containing games to be used to load games into the games list /// public ReactiveObject> GameDirs { get; private set; } + + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public ReactiveObject> AutoloadDirs { get; private set; } /// /// A list of file types to be hidden in the games List @@ -192,6 +197,7 @@ namespace Ryujinx.UI.Common.Configuration GuiColumns = new Columns(); ColumnSort = new ColumnSortSettings(); GameDirs = new ReactiveObject>(); + AutoloadDirs = new ReactiveObject>(); ShownFileTypes = new ShownFileTypeSettings(); WindowStartup = new WindowStartupSettings(); EnableCustomTheme = new ReactiveObject(); @@ -735,6 +741,7 @@ namespace Ryujinx.UI.Common.Configuration SortAscending = UI.ColumnSort.SortAscending, }, GameDirs = UI.GameDirs, + AutoloadDirs = UI.AutoloadDirs, ShownFileTypes = new ShownFileTypes { NSP = UI.ShownFileTypes.NSP, @@ -844,6 +851,7 @@ namespace Ryujinx.UI.Common.Configuration UI.ColumnSort.SortColumnId.Value = 0; UI.ColumnSort.SortAscending.Value = false; UI.GameDirs.Value = new List(); + UI.AutoloadDirs.Value = new List(); UI.ShownFileTypes.NSP.Value = true; UI.ShownFileTypes.PFS0.Value = true; UI.ShownFileTypes.XCI.Value = true; @@ -1493,6 +1501,15 @@ namespace Ryujinx.UI.Common.Configuration configurationFileUpdated = true; } + + if (configurationFileFormat.Version < 53) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); + + configurationFileFormat.AutoloadDirs = new(); + + configurationFileUpdated = true; + } Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; @@ -1556,6 +1573,7 @@ namespace Ryujinx.UI.Common.Configuration UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; UI.GameDirs.Value = configurationFileFormat.GameDirs; + UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs; UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 6f050ec1b..339842616 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -106,6 +106,7 @@ "SettingsTabGeneralHideCursorOnIdle": "On Idle", "SettingsTabGeneralHideCursorAlways": "Always", "SettingsTabGeneralGameDirectories": "Game Directories", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", "SettingsTabGeneralAdd": "Add", "SettingsTabGeneralRemove": "Remove", "SettingsTabSystem": "System", @@ -559,6 +560,9 @@ "AddGameDirBoxTooltip": "Enter a game directory to add to the list", "AddGameDirTooltip": "Add a game directory to the list", "RemoveGameDirTooltip": "Remove selected game directory", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Use a custom Avalonia theme for the GUI to change the appearance of the emulator menus", "CustomThemePathTooltip": "Path to custom GUI theme", "CustomThemeBrowseTooltip": "Browse for a custom GUI theme", diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 9223e578d..57c576e1f 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -44,7 +44,8 @@ namespace Ryujinx.Ava.UI.ViewModels private int _graphicsBackendMultithreadingIndex; private float _volume; private bool _isVulkanAvailable = true; - private bool _directoryChanged; + private bool _gameDirectoryChanged; + private bool _autoloadDirectoryChanged; private readonly List _gpuIds = new(); private int _graphicsBackendIndex; private int _scalingFilter; @@ -115,12 +116,23 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; - public bool DirectoryChanged + public bool GameDirectoryChanged { - get => _directoryChanged; + get => _gameDirectoryChanged; set { - _directoryChanged = value; + _gameDirectoryChanged = value; + + OnPropertyChanged(); + } + } + + public bool AutoloadDirectoryChanged + { + get => _autoloadDirectoryChanged; + set + { + _autoloadDirectoryChanged = value; OnPropertyChanged(); } @@ -231,6 +243,7 @@ namespace Ryujinx.Ava.UI.ViewModels internal AvaloniaList TimeZones { get; set; } public AvaloniaList GameDirectories { get; set; } + public AvaloniaList AutoloadDirectories { get; set; } public ObservableCollection AvailableGpus { get; set; } public AvaloniaList NetworkInterfaceList @@ -273,6 +286,7 @@ namespace Ryujinx.Ava.UI.ViewModels public SettingsViewModel() { GameDirectories = new AvaloniaList(); + AutoloadDirectories = new AvaloniaList(); TimeZones = new AvaloniaList(); AvailableGpus = new ObservableCollection(); _validTzRegions = new List(); @@ -398,6 +412,9 @@ namespace Ryujinx.Ava.UI.ViewModels GameDirectories.Clear(); GameDirectories.AddRange(config.UI.GameDirs.Value); + + AutoloadDirectories.Clear(); + AutoloadDirectories.AddRange(config.UI.AutoloadDirs.Value); BaseStyleIndex = config.UI.BaseStyle.Value switch { @@ -489,11 +506,17 @@ namespace Ryujinx.Ava.UI.ViewModels config.AutoloadContent.Value = AutoloadContent; config.HideCursor.Value = (HideCursorMode)HideCursor; - if (_directoryChanged) + if (_gameDirectoryChanged) { List gameDirs = new(GameDirectories); config.UI.GameDirs.Value = gameDirs; } + + if (_autoloadDirectoryChanged) + { + List autoloadDirs = new(AutoloadDirectories); + config.UI.AutoloadDirs.Value = autoloadDirs; + } config.UI.BaseStyle.Value = BaseStyleIndex switch { @@ -590,7 +613,8 @@ namespace Ryujinx.Ava.UI.ViewModels SaveSettingsEvent?.Invoke(); - _directoryChanged = false; + _gameDirectoryChanged = false; + _autoloadDirectoryChanged = false; } private static void RevertIfNotSaved() diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index 9d26effcc..4f25cbebf 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -88,7 +88,7 @@ Orientation="Vertical" Spacing="10"> @@ -105,27 +105,78 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs index 996d15cdb..70ca32306 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs @@ -19,14 +19,14 @@ namespace Ryujinx.Ava.UI.Views.Settings InitializeComponent(); } - private async void AddButton_OnClick(object sender, RoutedEventArgs e) + private async void AddGameDirButton_OnClick(object sender, RoutedEventArgs e) { - string path = PathBox.Text; + string path = GameDirPathBox.Text; if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path)) { ViewModel.GameDirectories.Add(path); - ViewModel.DirectoryChanged = true; + ViewModel.GameDirectoryChanged = true; } else { @@ -40,25 +40,68 @@ namespace Ryujinx.Ava.UI.Views.Settings if (result.Count > 0) { ViewModel.GameDirectories.Add(result[0].Path.LocalPath); - ViewModel.DirectoryChanged = true; + ViewModel.GameDirectoryChanged = true; } } } } - private void RemoveButton_OnClick(object sender, RoutedEventArgs e) + private void RemoveGameDirButton_OnClick(object sender, RoutedEventArgs e) { - int oldIndex = GameList.SelectedIndex; + int oldIndex = GameDirsList.SelectedIndex; - foreach (string path in new List(GameList.SelectedItems.Cast())) + foreach (string path in new List(GameDirsList.SelectedItems.Cast())) { ViewModel.GameDirectories.Remove(path); - ViewModel.DirectoryChanged = true; + ViewModel.GameDirectoryChanged = true; } - if (GameList.ItemCount > 0) + if (GameDirsList.ItemCount > 0) { - GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0; + GameDirsList.SelectedIndex = oldIndex < GameDirsList.ItemCount ? oldIndex : 0; + } + } + + private async void AddAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) + { + string path = AutoloadDirPathBox.Text; + + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.AutoloadDirectories.Contains(path)) + { + ViewModel.AutoloadDirectories.Add(path); + ViewModel.AutoloadDirectoryChanged = true; + } + else + { + if (this.GetVisualRoot() is Window window) + { + var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + AllowMultiple = false, + }); + + if (result.Count > 0) + { + ViewModel.AutoloadDirectories.Add(result[0].Path.LocalPath); + ViewModel.AutoloadDirectoryChanged = true; + } + } + } + } + + private void RemoveAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) + { + int oldIndex = AutoloadDirsList.SelectedIndex; + + foreach (string path in new List(AutoloadDirsList.SelectedItems.Cast())) + { + ViewModel.AutoloadDirectories.Remove(path); + ViewModel.AutoloadDirectoryChanged = true; + } + + if (AutoloadDirsList.ItemCount > 0) + { + AutoloadDirsList.SelectedIndex = oldIndex < AutoloadDirsList.ItemCount ? oldIndex : 0; } } } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 7947b4896..3b471fee8 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -642,17 +642,18 @@ namespace Ryujinx.Ava.UI.Windows TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates()); TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents()); - if (ConfigurationState.Instance.AutoloadContent) + var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value; + if (autoloadDirs.Count > 0) { var updatesLoaded = 0; TimeIt("auto updates", () => updatesLoaded = - ApplicationLibrary.AutoLoadTitleUpdates(ConfigurationState.Instance.UI.GameDirs)); + ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs)); var dlcLoaded = 0; TimeIt("auto dlc", () => dlcLoaded = - ApplicationLibrary.AutoLoadDownloadableContents(ConfigurationState.Instance.UI.GameDirs)); + ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs)); ShowNewContentAddedDialog(dlcLoaded, updatesLoaded); } diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index 314501c52..4d7871886 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -39,7 +39,7 @@ namespace Ryujinx.Ava.UI.Windows { InputPage.InputView?.SaveCurrentProfile(); - if (Owner is MainWindow window && ViewModel.DirectoryChanged) + if (Owner is MainWindow window && (ViewModel.GameDirectoryChanged || ViewModel.AutoloadDirectoryChanged)) { window.LoadApplications(); } From 665d1d4f6f0e5d2e66fcd686873d3e3fe319a9e5 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sun, 18 Aug 2024 20:49:32 -0400 Subject: [PATCH 19/31] Remove autoload content checkbox --- .../Configuration/ConfigurationFileFormat.cs | 7 +------ .../Configuration/ConfigurationState.cs | 18 ------------------ src/Ryujinx/Assets/Locales/en_US.json | 1 - src/Ryujinx/UI/ViewModels/SettingsViewModel.cs | 3 --- .../UI/Views/Settings/SettingsUIView.axaml | 3 --- 5 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 2ccb67c70..c0c451134 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 53; + public const int CurrentVersion = 52; /// /// Version of the configuration file format @@ -167,11 +167,6 @@ namespace Ryujinx.UI.Common.Configuration /// public bool RememberWindowState { get; set; } - /// - /// Enables or disables automatically loading DLC/title updates on library refresh. - /// - public bool AutoloadContent { get; set; } - /// /// Enables hardware-accelerated rendering for Avalonia /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index f9179d0c0..7a05498bd 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -638,11 +638,6 @@ namespace Ryujinx.UI.Common.Configuration /// public ReactiveObject RememberWindowState { get; private set; } - /// - /// Enables or disables automatically loading DLC/title updates on library refresh. - /// - public ReactiveObject AutoloadContent { get; private set; } - /// /// Enables hardware-accelerated rendering for Avalonia /// @@ -665,7 +660,6 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart = new ReactiveObject(); ShowConfirmExit = new ReactiveObject(); RememberWindowState = new ReactiveObject(); - AutoloadContent = new ReactiveObject(); EnableHardwareAcceleration = new ReactiveObject(); HideCursor = new ReactiveObject(); } @@ -704,7 +698,6 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart = CheckUpdatesOnStart, ShowConfirmExit = ShowConfirmExit, RememberWindowState = RememberWindowState, - AutoloadContent = AutoloadContent, EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, EnableVsync = Graphics.EnableVsync, @@ -815,7 +808,6 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart.Value = true; ShowConfirmExit.Value = true; RememberWindowState.Value = true; - AutoloadContent.Value = false; EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; @@ -1497,15 +1489,6 @@ namespace Ryujinx.UI.Common.Configuration { Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); - configurationFileFormat.AutoloadContent = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 53) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); - configurationFileFormat.AutoloadDirs = new(); configurationFileUpdated = true; @@ -1542,7 +1525,6 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; RememberWindowState.Value = configurationFileFormat.RememberWindowState; - AutoloadContent.Value = configurationFileFormat.AutoloadContent; EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; HideCursor.Value = configurationFileFormat.HideCursor; Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 339842616..b9ba51d5d 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -100,7 +100,6 @@ "SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch", "SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog", "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", - "SettingsTabGeneralAutoloadContent": "Automatically load DLC/updates", "SettingsTabGeneralHideCursor": "Hide Cursor:", "SettingsTabGeneralHideCursorNever": "Never", "SettingsTabGeneralHideCursorOnIdle": "On Idle", diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 57c576e1f..bfcf5bdfc 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -144,7 +144,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } public bool RememberWindowState { get; set; } - public bool AutoloadContent { get; set; } public int HideCursor { get; set; } public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } @@ -407,7 +406,6 @@ namespace Ryujinx.Ava.UI.ViewModels CheckUpdatesOnStart = config.CheckUpdatesOnStart; ShowConfirmExit = config.ShowConfirmExit; RememberWindowState = config.RememberWindowState; - AutoloadContent = config.AutoloadContent; HideCursor = (int)config.HideCursor.Value; GameDirectories.Clear(); @@ -503,7 +501,6 @@ namespace Ryujinx.Ava.UI.ViewModels config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; config.ShowConfirmExit.Value = ShowConfirmExit; config.RememberWindowState.Value = RememberWindowState; - config.AutoloadContent.Value = AutoloadContent; config.HideCursor.Value = (HideCursorMode)HideCursor; if (_gameDirectoryChanged) diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index 4f25cbebf..86b77e566 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -39,9 +39,6 @@ - - - Date: Sun, 18 Aug 2024 20:53:46 -0400 Subject: [PATCH 20/31] Formatter --- src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 2 +- .../Configuration/ConfigurationFileFormat.cs | 2 +- src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs | 2 +- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 5 +++-- src/Ryujinx/UI/ViewModels/SettingsViewModel.cs | 6 +++--- src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs | 2 +- src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 35b9ca185..80436a892 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -549,7 +549,7 @@ namespace Ryujinx.UI.App.Common { Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); } - + return false; } diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index c0c451134..36811a548 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -261,7 +261,7 @@ namespace Ryujinx.UI.Common.Configuration /// A list of directories containing games to be used to load games into the games list /// public List GameDirs { get; set; } - + /// /// A list of directories containing DLC/updates the user wants to autoload during library refreshes /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 7a05498bd..2cb814afa 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -121,7 +121,7 @@ namespace Ryujinx.UI.Common.Configuration /// A list of directories containing games to be used to load games into the games list /// public ReactiveObject> GameDirs { get; private set; } - + /// /// A list of directories containing DLC/updates the user wants to autoload during library refreshes /// diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 51cd8592b..c9b645a5c 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -116,7 +116,7 @@ namespace Ryujinx.Ava.UI.ViewModels public MainWindowViewModel() { Applications = new ObservableCollectionExtended(); - + Applications.ToObservableChangeSet() .Filter(Filter) .Sort(GetComparer()) @@ -1263,7 +1263,8 @@ namespace Ryujinx.Ava.UI.ViewModels { var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { - Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], AllowMultiple = true, + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle], + AllowMultiple = true, }); if (result.Count > 0) diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index bfcf5bdfc..717d3b0ac 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -126,7 +126,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); } } - + public bool AutoloadDirectoryChanged { get => _autoloadDirectoryChanged; @@ -410,7 +410,7 @@ namespace Ryujinx.Ava.UI.ViewModels GameDirectories.Clear(); GameDirectories.AddRange(config.UI.GameDirs.Value); - + AutoloadDirectories.Clear(); AutoloadDirectories.AddRange(config.UI.AutoloadDirs.Value); @@ -508,7 +508,7 @@ namespace Ryujinx.Ava.UI.ViewModels List gameDirs = new(GameDirectories); config.UI.GameDirs.Value = gameDirs; } - + if (_autoloadDirectoryChanged) { List autoloadDirs = new(AutoloadDirectories); diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs index 18d0f1a48..528495713 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs @@ -53,7 +53,7 @@ namespace Ryujinx.Ava.UI.Views.Main { Window.LoadApplications(); } - + private void VolumeStatus_OnPointerWheelChanged(object sender, PointerWheelEventArgs e) { // Change the volume by 5% at a time diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs index 70ca32306..34ddce071 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs @@ -61,7 +61,7 @@ namespace Ryujinx.Ava.UI.Views.Settings GameDirsList.SelectedIndex = oldIndex < GameDirsList.ItemCount ? oldIndex : 0; } } - + private async void AddAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) { string path = AutoloadDirPathBox.Text; From 3d7ede533f379640180307b5d8edf983d03755b9 Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Sun, 18 Aug 2024 23:29:34 -0400 Subject: [PATCH 21/31] Begin extracting updates to match DLC refactors --- .../App/ApplicationLibrary.cs | 85 +++++++++ .../Helper/TitleUpdatesHelper.cs | 162 +++++++++++++++++ src/Ryujinx/Assets/Locales/en_US.json | 3 +- .../Controls/ApplicationContextMenu.axaml.cs | 4 +- .../DownloadableContentManagerViewModel.cs | 28 ++- .../UI/ViewModels/TitleUpdateViewModel.cs | 172 +++++++++--------- .../DownloadableContentManagerWindow.axaml | 3 +- .../DownloadableContentManagerWindow.axaml.cs | 10 +- .../UI/Windows/TitleUpdateWindow.axaml | 22 ++- .../UI/Windows/TitleUpdateWindow.axaml.cs | 10 +- 10 files changed, 393 insertions(+), 106 deletions(-) create mode 100644 src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 80436a892..81cd8d28a 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1,4 +1,5 @@ using DynamicData; +using DynamicData.Kernel; using LibHac; using LibHac.Common; using LibHac.Fs; @@ -741,6 +742,8 @@ namespace Ryujinx.UI.App.Common foreach (var application in applications) { it.AddOrUpdate(application); + LoadTitleUpdatesForApplication(application); + LoadDlcForApplication(application); } }); @@ -776,6 +779,71 @@ namespace Ryujinx.UI.App.Common } } + private void LoadTitleUpdatesForApplication(ApplicationData application) + { + _titleUpdates.Edit(it => + { + var savedUpdates = + TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedUpdates); + + var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected); + + if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) + { + var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); + + bool addedNewUpdate = false; + foreach (var update in bundledUpdates) + { + if (!savedUpdateLookup.Contains(update)) + { + addedNewUpdate = true; + it.AddOrUpdate((update, false)); + } + } + + if (addedNewUpdate) + { + var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList(); + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); + } + } + }); + } + + private void LoadDlcForApplication(ApplicationData application) + { + _downloadableContents.Edit(it => + { + var savedDlc = + DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedDlc); + + if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) + { + var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); + + bool addedNewDlc = false; + foreach (var dlc in bundledDlc) + { + if (!savedDlcLookup.Contains(dlc)) + { + addedNewDlc = true; + it.AddOrUpdate((dlc, true)); + } + } + + if (addedNewDlc) + { + var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, + gameDlcs); + } + } + }); + } + public void LoadTitleUpdates() { return; @@ -833,6 +901,23 @@ namespace Ryujinx.UI.App.Common }); } + private void SaveTitleUpdatesForGame(ulong titleIdBase) + { + var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList(); + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates); + } + + public void SaveTitleUpdatesForGame(ApplicationData application, List<(TitleUpdateModel, bool IsSelected)> updates) + { + _titleUpdates.Edit(it => + { + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, updates); + + it.Remove(it.Items.Where(item => item.TitleUpdate.TitleIdBase == application.IdBase)); + it.AddOrUpdate(updates); + }); + } + public int AutoLoadDownloadableContents(List appDirs) { _cancellationToken = new CancellationTokenSource(); diff --git a/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs new file mode 100644 index 000000000..9dc3d4f73 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs @@ -0,0 +1,162 @@ +using LibHac.Common; +using LibHac.Common.Keys; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Ncm; +using LibHac.Ns; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.HLE.Utilities; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Models; +using System; +using System.Collections.Generic; +using System.IO; +using ContentType = LibHac.Ncm.ContentType; +using Path = System.IO.Path; +using SpanHelpers = LibHac.Common.SpanHelpers; +using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata; + +namespace Ryujinx.UI.Common.Helper +{ + public static class TitleUpdatesHelper + { + private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase) + { + var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase); + + if (!File.Exists(titleUpdatesJsonPath)) + { + return []; + } + + try + { + var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata); + return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}"); + return []; + } + } + + public static void SaveTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates) + { + var titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = [], + }; + + foreach ((TitleUpdateModel update, bool isSelected) in updates) + { + titleUpdateWindowData.Paths.Add(update.Path); + if (isSelected) + { + if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected)) + { + Logger.Error?.Print(LogClass.Application, + $"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}"); + return; + } + + titleUpdateWindowData.Selected = update.Path; + } + } + + var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase); + JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata); + } + + private static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase) + { + var result = new List<(TitleUpdateModel, bool IsSelected)>(); + + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + foreach (string path in titleUpdateMetadata.Paths) + { + if (!File.Exists(path)) + { + continue; + } + + try + { + using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs); + + Dictionary updates = + pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel); + + Nca patchNca = null; + Nca controlNca = null; + + if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content)) + { + continue; + } + + patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program); + controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control); + + if (controlNca == null || patchNca == null) + { + continue; + } + + 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); + + result.Add((update, path == titleUpdateMetadata.Selected)); + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, + $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {path}"); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, + $"The file encountered was not of a valid type. File: '{path}' Error: {exception}"); + } + } + + return result; + } + + private static string PathToGameUpdatesJson(ulong applicationIdBase) + { + return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json"); + } + } +} diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index b9ba51d5d..6018b4a3c 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -717,7 +717,8 @@ "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Title Update Manager", - "UpdateWindowDlcAddedMessage": "{0} new update(s) added", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs index ab833218b..068968650 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.ApplicationLibrary, viewModel.SelectedApplication); + await TitleUpdateWindow.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication); } } @@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.ApplicationLibrary, viewModel.SelectedApplication); + await DownloadableContentManagerWindow.Show(viewModel.ApplicationLibrary, viewModel.SelectedApplication); } } diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs index 18e17ff64..18424df51 100644 --- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -23,6 +23,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList _downloadableContents = new(); private AvaloniaList _selectedDownloadableContents = new(); private AvaloniaList _views = new(); + private bool _showBundledContentNotice = false; private string _search; private readonly ApplicationData _applicationData; @@ -76,7 +77,17 @@ namespace Ryujinx.Ava.UI.ViewModels get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); } - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) + public bool ShowBundledContentNotice + { + get => _showBundledContentNotice; + set + { + _showBundledContentNotice = value; + OnPropertyChanged(); + } + } + + public DownloadableContentManagerViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData) { _applicationLibrary = applicationLibrary; @@ -94,9 +105,12 @@ namespace Ryujinx.Ava.UI.ViewModels { var dlcs = _applicationLibrary.DownloadableContents.Items .Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase); + + bool hasBundledContent = false; foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) { DownloadableContents.Add(dlc); + hasBundledContent = hasBundledContent || dlc.IsBundled; if (isEnabled) { @@ -106,6 +120,8 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); } + ShowBundledContentNotice = hasBundledContent; + Sort(); } @@ -187,12 +203,18 @@ namespace Ryujinx.Ava.UI.ViewModels return false; } - if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs)) + if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out var dlcs) || dlcs.Count == 0) { return false; } - foreach (var dlc in dlcs.Where(dlc => dlc.TitleIdBase == _applicationData.IdBase)) + var dlcsForThisGame = dlcs.Where(it => it.TitleIdBase == _applicationData.IdBase); + if (!dlcsForThisGame.Any()) + { + return false; + } + + foreach (var dlc in dlcsForThisGame) { if (!DownloadableContents.Contains(dlc)) { diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs index 1af455201..516908a6e 100644 --- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs @@ -2,22 +2,17 @@ using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; using Avalonia.Threading; -using DynamicData.Kernel; +using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common.Models; -using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Application = Avalonia.Application; -using Path = System.IO.Path; namespace Ryujinx.Ava.UI.ViewModels { @@ -25,18 +20,13 @@ namespace Ryujinx.Ava.UI.ViewModels public class TitleUpdateViewModel : BaseModel { - - public TitleUpdateMetadata TitleUpdateWindowData; - public readonly string TitleUpdateJsonPath; - private VirtualFileSystem VirtualFileSystem { get; } private ApplicationLibrary ApplicationLibrary { get; } private ApplicationData ApplicationData { get; } private AvaloniaList _titleUpdates = new(); private AvaloniaList _views = new(); private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal(); - - private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private bool _showBundledContentNotice = false; public AvaloniaList TitleUpdates { @@ -68,11 +58,20 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool ShowBundledContentNotice + { + get => _showBundledContentNotice; + set + { + _showBundledContentNotice = value; + OnPropertyChanged(); + } + } + public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationLibrary applicationLibrary, ApplicationData applicationData) + public TitleUpdateViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData) { - VirtualFileSystem = virtualFileSystem; ApplicationLibrary = applicationLibrary; ApplicationData = applicationData; @@ -82,43 +81,29 @@ namespace Ryujinx.Ava.UI.ViewModels StorageProvider = desktop.MainWindow.StorageProvider; } - TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdBaseString, "updates.json"); - - try - { - TitleUpdateWindowData = JsonHelper.DeserializeFromFile(TitleUpdateJsonPath, _serializerContext.TitleUpdateMetadata); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdBaseString} at {TitleUpdateJsonPath}"); - - TitleUpdateWindowData = new TitleUpdateMetadata - { - Selected = "", - Paths = new List(), - }; - - Save(); - } - LoadUpdates(); } private void LoadUpdates() { - // Try to load updates from PFS first - AddUpdate(ApplicationData.Path, true); + var updates = ApplicationLibrary.TitleUpdates.Items + .Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase); - foreach (string path in TitleUpdateWindowData.Paths) + bool hasBundledContent = false; + SelectedUpdate = new TitleUpdateViewNoUpdateSentinal(); + foreach ((TitleUpdateModel update, bool isSelected) in updates) { - AddUpdate(path); + TitleUpdates.Add(update); + hasBundledContent = hasBundledContent || update.IsBundled; + + if (isSelected) + { + SelectedUpdate = update; + } } - var selected = TitleUpdates.FirstOrOptional(x => x.Path == TitleUpdateWindowData.Selected); - SelectedUpdate = selected.HasValue ? selected.Value : new TitleUpdateViewNoUpdateSentinal(); + ShowBundledContentNotice = hasBundledContent; - // NOTE: Save the list again to remove leftovers. - Save(); SortUpdates(); } @@ -126,65 +111,76 @@ namespace Ryujinx.Ava.UI.ViewModels { var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version); + // NOTE(jpr): this works around a bug where calling Views.Clear also clears SelectedUpdate for + // some reason. so we save the item here and restore it after + var selected = SelectedUpdate; + Views.Clear(); Views.Add(new TitleUpdateViewNoUpdateSentinal()); Views.AddRange(sortedUpdates); + SelectedUpdate = selected; + if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal) { SelectedUpdate = Views[0]; } + // this is mainly to handle a scenario where the user removes the selected update else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate)) { SelectedUpdate = Views.Count > 1 ? Views[1] : Views[0]; } } - private void AddUpdate(string path, bool ignoreNotFound = false, bool selected = false) + private bool AddUpdate(string path, out int numUpdatesAdded) { - if (!File.Exists(path) || TitleUpdates.Any(x => x.Path == path)) + numUpdatesAdded = 0; + + if (!File.Exists(path)) { - return; + return false; } - try + if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var updates)) { - if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var titleUpdates)) + return false; + } + + var updatesForThisGame = updates.Where(it => it.TitleIdBase == ApplicationData.Id); + if (!updatesForThisGame.Any()) + { + return false; + } + + foreach (var update in updatesForThisGame) + { + if (!TitleUpdates.Contains(update)) { - if (!ignoreNotFound) - { - Dispatcher.UIThread.InvokeAsync(() => - ContentDialogHelper.CreateErrorDialog( - LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); - } + TitleUpdates.Add(update); + SelectedUpdate = update; - return; - } - - foreach (var titleUpdate in titleUpdates) - { - if (titleUpdate.TitleIdBase != ApplicationData.Id) - { - continue; - } - - TitleUpdates.Add(titleUpdate); - - if (selected) - { - Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = titleUpdate); - } + numUpdatesAdded++; } } - catch (Exception ex) + + if (numUpdatesAdded > 0) { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path))); + SortUpdates(); } + + return true; } public void RemoveUpdate(TitleUpdateModel update) { - TitleUpdates.Remove(update); + if (!update.IsBundled) + { + TitleUpdates.Remove(update); + } + else if (update == SelectedUpdate as TitleUpdateModel) + { + SelectedUpdate = new TitleUpdateViewNoUpdateSentinal(); + } SortUpdates(); } @@ -205,30 +201,36 @@ namespace Ryujinx.Ava.UI.ViewModels }, }); + var totalUpdatesAdded = 0; foreach (var file in result) { - AddUpdate(file.Path.LocalPath, selected: true); + if (!AddUpdate(file.Path.LocalPath, out var newUpdatesAdded)) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]); + } + + totalUpdatesAdded += newUpdatesAdded; } - SortUpdates(); + if (totalUpdatesAdded > 0) + { + await ShowNewUpdatesAddedDialog(totalUpdatesAdded); + } } public void Save() { - TitleUpdateWindowData.Paths.Clear(); - TitleUpdateWindowData.Selected = ""; + var updates = TitleUpdates.Select(it => (it, it == SelectedUpdate as TitleUpdateModel)).ToList(); + ApplicationLibrary.SaveTitleUpdatesForGame(ApplicationData, updates); + } - foreach (TitleUpdateModel update in TitleUpdates) + private Task ShowNewUpdatesAddedDialog(int numAdded) + { + var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded); + return Dispatcher.UIThread.InvokeAsync(async () => { - TitleUpdateWindowData.Paths.Add(update.Path); - - if (update == SelectedUpdate as TitleUpdateModel) - { - TitleUpdateWindowData.Selected = update.Path; - } - } - - JsonHelper.SerializeToFile(TitleUpdateJsonPath, TitleUpdateWindowData, _serializerContext.TitleUpdateMetadata); + await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); + }); } } } diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index 440d1bd6b..d53074499 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -28,7 +28,8 @@ Grid.Row="0" Margin="0 0 0 10" Spacing="5" - Orientation="Horizontal"> + Orientation="Horizontal" + IsVisible="{Binding ShowBundledContentNotice}"> + - + + + + + Date: Mon, 19 Aug 2024 00:53:00 -0400 Subject: [PATCH 22/31] Add title update autoloading --- .../App/ApplicationLibrary.cs | 448 +++++++++--------- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 25 +- 2 files changed, 224 insertions(+), 249 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 81cd8d28a..66ca95d80 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -44,9 +44,9 @@ namespace Ryujinx.UI.App.Common public event EventHandler ApplicationAdded; public event EventHandler ApplicationCountUpdated; - public IObservableCache Applications; - public IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; - public IObservableCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> DownloadableContents; + public readonly IObservableCache Applications; + public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; + public readonly IObservableCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> DownloadableContents; private readonly byte[] _nspIcon; private readonly byte[] _xciIcon; @@ -57,7 +57,7 @@ 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 _applications = new(it => (it.Id, 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); @@ -742,8 +742,12 @@ namespace Ryujinx.UI.App.Common foreach (var application in applications) { it.AddOrUpdate(application); - LoadTitleUpdatesForApplication(application); LoadDlcForApplication(application); + if (LoadTitleUpdatesForApplication(application)) + { + // Trigger a reload of the version data + RefreshApplicationInfo(application.IdBase); + } } }); @@ -779,117 +783,6 @@ namespace Ryujinx.UI.App.Common } } - private void LoadTitleUpdatesForApplication(ApplicationData application) - { - _titleUpdates.Edit(it => - { - var savedUpdates = - TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase); - it.AddOrUpdate(savedUpdates); - - var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected); - - if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) - { - var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); - - bool addedNewUpdate = false; - foreach (var update in bundledUpdates) - { - if (!savedUpdateLookup.Contains(update)) - { - addedNewUpdate = true; - it.AddOrUpdate((update, false)); - } - } - - if (addedNewUpdate) - { - var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList(); - TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); - } - } - }); - } - - private void LoadDlcForApplication(ApplicationData application) - { - _downloadableContents.Edit(it => - { - var savedDlc = - DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); - it.AddOrUpdate(savedDlc); - - if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) - { - var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); - - bool addedNewDlc = false; - foreach (var dlc in bundledDlc) - { - if (!savedDlcLookup.Contains(dlc)) - { - addedNewDlc = true; - it.AddOrUpdate((dlc, true)); - } - } - - if (addedNewDlc) - { - var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); - DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, - gameDlcs); - } - } - }); - } - - public void LoadTitleUpdates() - { - return; - } - - public void LoadDownloadableContents() - { - _downloadableContents.Edit(it => - { - it.Clear(); - - foreach (ApplicationData application in Applications.Items) - { - var savedDlc = DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); - it.AddOrUpdate(savedDlc); - - if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) - { - var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); - - bool addedNewDlc = false; - foreach (var dlc in bundledDlc) - { - if (!savedDlcLookup.Contains(dlc)) - { - addedNewDlc = true; - it.AddOrUpdate((dlc, true)); - } - } - - if (addedNewDlc) - { - var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); - DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, gameDlcs); - } - } - } - }); - } - - private void SaveDownloadableContentsForGame(ulong titleIdBase) - { - var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList(); - DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs); - } - public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { _downloadableContents.Edit(it => @@ -901,12 +794,6 @@ namespace Ryujinx.UI.App.Common }); } - private void SaveTitleUpdatesForGame(ulong titleIdBase) - { - var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList(); - TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates); - } - public void SaveTitleUpdatesForGame(ApplicationData application, List<(TitleUpdateModel, bool IsSelected)> updates) { _titleUpdates.Edit(it => @@ -937,7 +824,7 @@ namespace Ryujinx.UI.App.Common if (!Directory.Exists(appDir)) { Logger.Warning?.Print(LogClass.Application, - $"The specified game directory \"{appDir}\" does not exist."); + $"The specified autoload directory \"{appDir}\" does not exist."); continue; } @@ -1021,111 +908,113 @@ namespace Ryujinx.UI.App.Common public int AutoLoadTitleUpdates(List appDirs) { - return 0; - // _cancellationToken = new CancellationTokenSource(); - // _titleUpdates.Clear(); - // - // // Builds the applications list with paths to found applications - // List applicationPaths = new(); - // - // try - // { - // foreach (string appDir in appDirs) - // { - // if (_cancellationToken.Token.IsCancellationRequested) - // { - // return; - // } - // - // if (!Directory.Exists(appDir)) - // { - // Logger.Warning?.Print(LogClass.Application, - // $"The specified game directory \"{appDir}\" does not exist."); - // - // continue; - // } - // - // try - // { - // EnumerationOptions options = new() - // { - // RecurseSubdirectories = true, - // IgnoreInaccessible = false, - // }; - // - // IEnumerable files = Directory.EnumerateFiles(appDir, "*", options) - // .Where(file => - // { - // return - // (Path.GetExtension(file).ToLower() is ".nsp" && - // ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || - // (Path.GetExtension(file).ToLower() is ".xci" && - // ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value); - // }); - // - // foreach (string app in files) - // { - // if (_cancellationToken.Token.IsCancellationRequested) - // { - // return; - // } - // - // var fileInfo = new FileInfo(app); - // - // try - // { - // var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? - // fileInfo.FullName; - // - // applicationPaths.Add(fullPath); - // } - // catch (IOException exception) - // { - // Logger.Warning?.Print(LogClass.Application, - // $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); - // } - // } - // } - // catch (UnauthorizedAccessException) - // { - // Logger.Warning?.Print(LogClass.Application, - // $"Failed to get access to directory: \"{appDir}\""); - // } - // } - // - // // Loops through applications list, creating a struct and then firing an event containing the struct for each application - // foreach (string applicationPath in applicationPaths) - // { - // if (_cancellationToken.Token.IsCancellationRequested) - // { - // return; - // } - // - // if (TryGetTitleUpdatesFromFile(applicationPath, out List titleUpdates)) - // { - // foreach (var titleUpdate in titleUpdates) - // { - // OnTitleUpdateAdded(new TitleUpdateAddedEventArgs() - // { - // TitleUpdate = titleUpdate, - // }); - // } - // - // _titleUpdates.Edit(it => - // { - // foreach (var titleUpdate in titleUpdates) - // { - // it.AddOrUpdate((titleUpdate, false)); - // } - // }); - // } - // } - // } - // finally - // { - // _cancellationToken.Dispose(); - // _cancellationToken = null; - // } + _cancellationToken = new CancellationTokenSource(); + + List updatePaths = new(); + int numUpdatesLoaded = 0; + + try + { + foreach (string appDir in appDirs) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return numUpdatesLoaded; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, + $"The specified autoload directory \"{appDir}\" does not exist."); + + continue; + } + + try + { + EnumerationOptions options = new() + { + RecurseSubdirectories = true, + IgnoreInaccessible = false, + }; + + IEnumerable files = Directory.EnumerateFiles(appDir, "*", options).Where( + file => + { + return + (Path.GetExtension(file).ToLower() is ".nsp" && + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value); + }); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return numUpdatesLoaded; + } + + var fileInfo = new FileInfo(app); + + try + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; + + updatePaths.Add(fullPath); + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, + $"Failed to get access to directory: \"{appDir}\""); + } + } + + var appIdLookup = Applications.Items.Select(it => it.IdBase).ToHashSet(); + + foreach (string updatePath in updatePaths) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return numUpdatesLoaded; + } + + if (TryGetTitleUpdatesFromFile(updatePath, out var foundUpdates)) + { + foreach (var update in foundUpdates.Where(it => appIdLookup.Contains(it.TitleIdBase))) + { + if (!_titleUpdates.Lookup(update).HasValue) + { + var currentlySelected = TitleUpdates.Items.FirstOrOptional(it => + it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected); + + var shouldSelect = !currentlySelected.HasValue || + currentlySelected.Value.TitleUpdate.Version < update.Version; + _titleUpdates.AddOrUpdate((update, shouldSelect)); + SaveTitleUpdatesForGame(update.TitleIdBase); + numUpdatesLoaded++; + + if (shouldSelect) + { + RefreshApplicationInfo(update.TitleIdBase); + } + } + } + } + } + } + finally + { + _cancellationToken.Dispose(); + _cancellationToken = null; + } + + return numUpdatesLoaded; } protected void OnApplicationAdded(ApplicationAddedEventArgs e) @@ -1465,5 +1354,108 @@ namespace Ryujinx.UI.App.Common return null; } + + private void LoadDlcForApplication(ApplicationData application) + { + _downloadableContents.Edit(it => + { + var savedDlc = + DownloadableContentsHelper.LoadDownloadableContentsJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedDlc); + + if (TryGetDownloadableContentFromFile(application.Path, out var bundledDlc)) + { + var savedDlcLookup = savedDlc.Select(dlc => dlc.Item1).ToHashSet(); + + bool addedNewDlc = false; + foreach (var dlc in bundledDlc) + { + if (!savedDlcLookup.Contains(dlc)) + { + addedNewDlc = true; + it.AddOrUpdate((dlc, true)); + } + } + + if (addedNewDlc) + { + var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, + gameDlcs); + } + } + }); + } + + private bool LoadTitleUpdatesForApplication(ApplicationData application) + { + var modifiedVersion = false; + + _titleUpdates.Edit(it => + { + var savedUpdates = + TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase); + it.AddOrUpdate(savedUpdates); + + var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected); + + if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) + { + var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); + + bool addedNewUpdate = false; + foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version)) + { + if (!savedUpdateLookup.Contains(update)) + { + bool shouldSelect = false; + if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version) + { + shouldSelect = true; + selectedUpdate = Optional<(TitleUpdateModel, bool IsSelected)>.Create((update, true)); + } + + modifiedVersion = modifiedVersion || shouldSelect; + it.AddOrUpdate((update, shouldSelect)); + + addedNewUpdate = true; + } + } + + if (addedNewUpdate) + { + var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList(); + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); + } + } + }); + + return modifiedVersion; + } + + private void SaveDownloadableContentsForGame(ulong titleIdBase) + { + var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList(); + DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs); + } + + private void SaveTitleUpdatesForGame(ulong titleIdBase) + { + var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList(); + TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates); + } + + private void RefreshApplicationInfo(ulong appIdBase) + { + var application = Applications.Items.First(it => it.IdBase == appIdBase); + + if (!TryGetApplicationsFromFile(application.Path, out List applications)) + { + return; + } + + var newApplication = applications.First(it => it.IdBase == appIdBase); + _applications.AddOrUpdate(newApplication); + } } } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 3b471fee8..1388c94f3 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -638,22 +638,14 @@ namespace Ryujinx.Ava.UI.Windows Thread applicationLibraryThread = new(() => { ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; - TimeIt("games", () => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs)); - TimeIt("updates", () => ApplicationLibrary.LoadTitleUpdates()); - TimeIt("DLC", () => ApplicationLibrary.LoadDownloadableContents()); + + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs); var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value; if (autoloadDirs.Count > 0) { - var updatesLoaded = 0; - TimeIt("auto updates", - () => updatesLoaded = - ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs)); - - var dlcLoaded = 0; - TimeIt("auto dlc", - () => dlcLoaded = - ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs)); + var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs); + var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs); ShowNewContentAddedDialog(dlcLoaded, updatesLoaded); } @@ -667,15 +659,6 @@ namespace Ryujinx.Ava.UI.Windows applicationLibraryThread.Start(); } - private static void TimeIt(string tag, Action act) - { - var watch = System.Diagnostics.Stopwatch.StartNew(); - act(); - watch.Stop(); - var elapsedMs = watch.ElapsedMilliseconds; - Console.WriteLine("[{0}] {1} ms", tag, elapsedMs); - } - private Task ShowNewContentAddedDialog(int numDlcAdded, int numUpdatesAdded) { var msg = ""; From 4bf437180f24f4f084b63f567cb2b8a6994a3c0e Mon Sep 17 00:00:00 2001 From: Jimmy Reichley Date: Mon, 19 Aug 2024 17:52:45 -0400 Subject: [PATCH 23/31] Reduce size of settings sections --- src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index 86b77e566..ac5e8371f 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -86,7 +86,7 @@ Spacing="10">