diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 5d407390a..044eccbea 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1,5 +1,6 @@ using DynamicData; using DynamicData.Kernel; +using Gommon; using LibHac; using LibHac.Common; using LibHac.Fs; @@ -801,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; @@ -900,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; @@ -979,27 +1014,21 @@ 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); } } } } } + + titleIdsToSave.ForEach(titleId => SaveTitleUpdatesForGame(titleId)); + titleIdsToRefresh.ForEach(titleId => RefreshApplicationInfo(titleId)); } finally { @@ -1010,6 +1039,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); @@ -1394,8 +1441,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)) @@ -1404,17 +1451,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 6dfd18773..26342ec4b 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", @@ -733,8 +734,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 564d033d4..e90aa6cec 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -106,6 +106,7 @@ "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", @@ -733,8 +734,9 @@ "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)", - "AutoloadDlcAndUpdateAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) et {1} nouvelle(s) mise(s) à jour ajouté(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 fad303f63..05104cf54 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -53,6 +53,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; @@ -1280,7 +1281,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 { @@ -1291,14 +1292,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); }); } @@ -1554,12 +1558,18 @@ namespace Ryujinx.Ava.UI.ViewModels public async Task LoadDlcFromFolder() { - await LoadContentFromFolder(LocaleKeys.AutoloadDlcAddedMessage, ApplicationLibrary.AutoLoadDownloadableContents); + await LoadContentFromFolder( + LocaleKeys.AutoloadDlcAddedMessage, + LocaleKeys.AutoloadDlcRemovedMessage, + ApplicationLibrary.AutoLoadDownloadableContents); } public async Task LoadTitleUpdatesFromFolder() { - await LoadContentFromFolder(LocaleKeys.AutoloadUpdateAddedMessage, ApplicationLibrary.AutoLoadTitleUpdates); + 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 0fc9ea1bb..5d22b891c 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 96dd9fe39..92485a569 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 399165b16..58fe0548c 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -129,7 +129,10 @@ - + + + +