From ce24341d1d0ebee030c8ee579793b40c6bb193fb Mon Sep 17 00:00:00 2001 From: Aaron Murgatroyd Date: Thu, 24 Oct 2024 00:35:51 +1000 Subject: [PATCH] Automatically remove invalid dlc and updates as part of auto load Fixed some minor label spacing issues in options dialog Removal of unused variable in input view model --- .../App/ApplicationLibrary.cs | 87 +++++++++++++++---- src/Ryujinx/Assets/Locales/en_US.json | 4 +- src/Ryujinx/Assets/Locales/fr_FR.json | 7 ++ .../UI/ViewModels/MainWindowViewModel.cs | 24 +++-- .../Views/Settings/SettingsLoggingView.axaml | 2 +- .../Views/Settings/SettingsSystemView.axaml | 2 +- .../UI/Views/Settings/SettingsUIView.axaml | 7 +- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 37 ++++---- 8 files changed, 117 insertions(+), 53 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index fbe05ed1..8a0bad18 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -802,17 +802,31 @@ namespace Ryujinx.UI.App.Common // Searches the provided directories for DLC NSP files that are _valid for the currently detected games in the // library_, and then enables those DLC. - public int AutoLoadDownloadableContents(List appDirs) + public int AutoLoadDownloadableContents(List appDirs, out int numDlcRemoved) { _cancellationToken = new CancellationTokenSource(); List dlcPaths = new(); int newDlcLoaded = 0; + numDlcRemoved = 0; try { + // Remove any downloadable content which can no longer be located on disk + Logger.Notice.Print(LogClass.Application, $"Removing non-existing Title DLCs"); + var dlcToRemove = _downloadableContents.Items + .Where(dlc => !File.Exists(dlc.Dlc.ContainerPath)) + .ToList(); + dlcToRemove.ForEach(dlc => + Logger.Warning?.Print(LogClass.Application, $"Title DLC removed: {dlc.Dlc.ContainerPath}") + ); + numDlcRemoved += dlcToRemove.Distinct().Count(); + _downloadableContents.RemoveKeys(dlcToRemove.Select(dlc => dlc.Dlc)); + foreach (string appDir in appDirs) { + Logger.Notice.Print(LogClass.Application, $"Auto loading DLC from: {appDir}"); + if (_cancellationToken.Token.IsCancellationRequested) { return newDlcLoaded; @@ -901,17 +915,37 @@ namespace Ryujinx.UI.App.Common // Searches the provided directories for update NSP files that are _valid for the currently detected games in the // library_, and then applies those updates. If a newly-detected update is a newer version than the currently // selected update (or if no update is currently selected), then that update will be selected. - public int AutoLoadTitleUpdates(List appDirs) + public int AutoLoadTitleUpdates(List appDirs, out int numUpdatesRemoved) { _cancellationToken = new CancellationTokenSource(); List updatePaths = new(); int numUpdatesLoaded = 0; + numUpdatesRemoved = 0; try { + var titleIdsToSave = new HashSet(); + var titleIdsToRefresh = new HashSet(); + + // Remove any updates which can no longer be located on disk + Logger.Notice.Print(LogClass.Application, $"Removing non-existing Title Updates"); + var updatesToRemove = _titleUpdates.Items + .Where(it => !File.Exists(it.TitleUpdate.Path)) + .ToList(); + + numUpdatesRemoved += updatesToRemove.Select(it => it.TitleUpdate).Distinct().Count(); + updatesToRemove.ForEach(ti => + Logger.Warning?.Print(LogClass.Application, $"Title update removed: {ti.TitleUpdate.Path}") + ); + _titleUpdates.RemoveKeys(updatesToRemove.Select(it => it.TitleUpdate)); + titleIdsToSave.UnionWith(updatesToRemove.Select(it => it.TitleUpdate.TitleIdBase)); + titleIdsToRefresh.UnionWith(updatesToRemove.Where(it => it.IsSelected).Select(update => update.TitleUpdate.TitleIdBase)); + foreach (string appDir in appDirs) { + Logger.Notice.Print(LogClass.Application, $"Auto loading updates from: {appDir}"); + if (_cancellationToken.Token.IsCancellationRequested) { return numUpdatesLoaded; @@ -980,27 +1014,24 @@ namespace Ryujinx.UI.App.Common { 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)); - - if (currentlySelected.HasValue && shouldSelect) - _titleUpdates.AddOrUpdate((currentlySelected.Value.TitleUpdate, false)); - - SaveTitleUpdatesForGame(update.TitleIdBase); + bool shouldSelect = AddAndAutoSelectUpdate(update); + titleIdsToSave.Add(update.TitleIdBase); numUpdatesLoaded++; if (shouldSelect) { - RefreshApplicationInfo(update.TitleIdBase); + titleIdsToRefresh.Add(update.TitleIdBase); } } } } } + + foreach (var titleId in titleIdsToSave) + SaveTitleUpdatesForGame(titleId); + + foreach (var titleId in titleIdsToRefresh) + RefreshApplicationInfo(titleId); } finally { @@ -1011,6 +1042,24 @@ namespace Ryujinx.UI.App.Common return numUpdatesLoaded; } + private bool AddAndAutoSelectUpdate(TitleUpdateModel update) + { + 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)); + + if (currentlySelected.HasValue && shouldSelect) + { + _titleUpdates.AddOrUpdate((currentlySelected.Value.TitleUpdate, false)); + } + + return shouldSelect; + } + protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) { ApplicationCountUpdated?.Invoke(null, e); @@ -1395,8 +1444,8 @@ namespace Ryujinx.UI.App.Common if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) { var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); + bool updatesChanged = false; - bool addedNewUpdate = false; foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version)) { if (!savedUpdateLookup.Contains(update)) @@ -1405,17 +1454,19 @@ namespace Ryujinx.UI.App.Common if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version) { shouldSelect = true; - selectedUpdate = Optional<(TitleUpdateModel, bool IsSelected)>.Create((update, true)); + if (selectedUpdate.HasValue) + _titleUpdates.AddOrUpdate((selectedUpdate.Value.Item1, false)); + selectedUpdate = DynamicData.Kernel.Optional<(TitleUpdateModel, bool IsSelected)>.Create((update, true)); } modifiedVersion = modifiedVersion || shouldSelect; it.AddOrUpdate((update, shouldSelect)); - addedNewUpdate = true; + updatesChanged = true; } } - if (addedNewUpdate) + if (updatesChanged) { var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList(); TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 45befacb..e275d08e 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -106,6 +106,7 @@ "SettingsTabGeneralHideCursorAlways": "Always", "SettingsTabGeneralGameDirectories": "Game Directories", "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Add", "SettingsTabGeneralRemove": "Remove", "SettingsTabSystem": "System", @@ -725,8 +726,9 @@ "DlcWindowHeading": "{0} Downloadable Content(s)", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", "AutoloadUpdateAddedMessage": "{0} new update(s) added", - "AutoloadDlcAndUpdateAddedMessage": "{0} new downloadable content(s) and {1} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index 99a06065..101c5b58 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -102,6 +102,8 @@ "SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif", "SettingsTabGeneralHideCursorAlways": "Toujours", "SettingsTabGeneralGameDirectories": "Dossiers des jeux", + "SettingsTabGeneralAutoloadDirectories": "Dossiers des mises à jour/DLC", + "SettingsTabGeneralAutoloadNote": "Les DLC et les mises à jour faisant référence aux fichiers manquants seront automatiquement déchargés.", "SettingsTabGeneralAdd": "Ajouter", "SettingsTabGeneralRemove": "Retirer", "SettingsTabSystem": "Système", @@ -708,6 +710,11 @@ "CheatWindowHeading": "Cheats disponibles pour {0} [{1}]", "BuildId": "BuildId:", "DlcWindowHeading": "{0} Contenu(s) téléchargeable(s)", + "DlcWindowDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(s)", + "AutoloadDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(s)", + "AutoloadDlcRemovedMessage": "{0} contenu(s) téléchargeable(s) manquant(s) supprimé(s)", + "AutoloadUpdateAddedMessage": "{0} nouvelle(s) mise(s) à jour ajoutée(s)", + "AutoloadUpdateRemovedMessage": "{0} mises à jour manquantes supprimées", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Éditer la sélection", "Cancel": "Annuler", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index c9b645a5..321f306c 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -52,6 +52,7 @@ namespace Ryujinx.Ava.UI.ViewModels public class MainWindowViewModel : BaseModel { private const int HotKeyPressDelayMs = 500; + private delegate int LoadContentFromFolderDelegate(List dirs, out int numRemoved); private ObservableCollectionExtended _applications; private string _aspectStatusText; @@ -1259,7 +1260,7 @@ namespace Ryujinx.Ava.UI.ViewModels _rendererWaitEvent.Set(); } - private async Task LoadContentFromFolder(LocaleKeys localeMessageKey, Func, int> onDirsSelected) + private async Task LoadContentFromFolder(LocaleKeys localeMessageAddedKey, LocaleKeys localeMessageRemovedKey, LoadContentFromFolderDelegate onDirsSelected) { var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { @@ -1270,14 +1271,17 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { var dirs = result.Select(it => it.Path.LocalPath).ToList(); - var numAdded = onDirsSelected(dirs); + var numAdded = onDirsSelected(dirs, out int numRemoved); - var msg = string.Format(LocaleManager.Instance[localeMessageKey], numAdded); + var msg = String.Join("\r\n", new string[] { + string.Format(LocaleManager.Instance[localeMessageRemovedKey], numRemoved), + string.Format(LocaleManager.Instance[localeMessageAddedKey], numAdded) + }); await Dispatcher.UIThread.InvokeAsync(async () => { await ContentDialogHelper.ShowTextDialog( - LocaleManager.Instance[numAdded > 0 ? LocaleKeys.RyujinxConfirm : LocaleKeys.RyujinxInfo], + LocaleManager.Instance[numAdded > 0 || numRemoved > 0 ? LocaleKeys.RyujinxConfirm : LocaleKeys.RyujinxInfo], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); }); } @@ -1533,14 +1537,18 @@ namespace Ryujinx.Ava.UI.ViewModels public async Task LoadDlcFromFolder() { - await LoadContentFromFolder(LocaleKeys.AutoloadDlcAddedMessage, - dirs => ApplicationLibrary.AutoLoadDownloadableContents(dirs)); + await LoadContentFromFolder( + LocaleKeys.AutoloadDlcAddedMessage, + LocaleKeys.AutoloadDlcRemovedMessage, + ApplicationLibrary.AutoLoadDownloadableContents); } public async Task LoadTitleUpdatesFromFolder() { - await LoadContentFromFolder(LocaleKeys.AutoloadUpdateAddedMessage, - dirs => ApplicationLibrary.AutoLoadTitleUpdates(dirs)); + await LoadContentFromFolder( + LocaleKeys.AutoloadUpdateAddedMessage, + LocaleKeys.AutoloadUpdateRemovedMessage, + ApplicationLibrary.AutoLoadTitleUpdates); } public async Task OpenFolder() diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml index 0fc9ea1b..5d22b891 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml @@ -52,7 +52,7 @@ - + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index e6f7c6e4..0db0eae6 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -195,7 +195,7 @@ + Spacing="5"> diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index ac5e8371..d91d68a3 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -129,7 +129,10 @@ - + + + +