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