Forráskód Böngészése

Automatically remove invalid dlc and updates as part of auto load (#42)

* 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

* Fixed missing french message for AutoloadDlcAddedMessage
TheToid 1 éve
szülő
commit
7038a902c3

+ 67 - 18
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<string> appDirs)
+        public int AutoLoadDownloadableContents(List<string> appDirs, out int numDlcRemoved)
         {
             _cancellationToken = new CancellationTokenSource();
 
             List<string> 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<string> appDirs)
+        public int AutoLoadTitleUpdates(List<string> appDirs, out int numUpdatesRemoved)
         {
             _cancellationToken = new CancellationTokenSource();
 
             List<string> updatePaths = new();
             int numUpdatesLoaded = 0;
+            numUpdatesRemoved = 0;
 
             try
             {
+                var titleIdsToSave = new HashSet<ulong>();
+                var titleIdsToRefresh = new HashSet<ulong>();
+
+                // 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);

+ 3 - 1
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",

+ 3 - 1
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",

+ 16 - 6
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<string> dirs, out int numRemoved);
 
         private ObservableCollectionExtended<ApplicationData> _applications;
         private string _aspectStatusText;
@@ -1280,7 +1281,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             _rendererWaitEvent.Set();
         }
 
-        private async Task LoadContentFromFolder(LocaleKeys localeMessageKey, Func<List<string>, 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()

+ 1 - 1
src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml

@@ -52,7 +52,7 @@
                     </CheckBox>
                 </StackPanel>
                 <Separator Height="1" />
-                <StackPanel Orientation="Vertical" Spacing="2">
+                <StackPanel Orientation="Vertical" Spacing="5">
                     <TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
                     <TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{locale:Locale SettingsTabLoggingDeveloperOptionsNote}" />
                 </StackPanel>

+ 1 - 1
src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml

@@ -195,7 +195,7 @@
                 <Separator Height="1" />
                 <StackPanel
                     Orientation="Vertical"
-                    Spacing="2">
+                    Spacing="5">
                     <TextBlock
                         Classes="h1"
                         Text="{locale:Locale SettingsTabSystemHacks}" />

+ 5 - 2
src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml

@@ -129,7 +129,10 @@
                     </Grid>
                 </StackPanel>
                 <Separator Height="1" />
-                <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralAutoloadDirectories}" />
+               <StackPanel Orientation="Vertical" Spacing="5">
+                    <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralAutoloadDirectories}" />
+                    <TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{locale:Locale SettingsTabGeneralAutoloadNote}" />
+                </StackPanel>
                 <StackPanel
                     Margin="10,0,0,0"
                     HorizontalAlignment="Stretch"
@@ -137,7 +140,7 @@
                     Spacing="10">
                     <ListBox
                         Name="AutoloadDirsList"
-                        MinHeight="120"
+                        MinHeight="100"
                         ItemsSource="{Binding AutoloadDirectories}">
                         <ListBox.Styles>
                             <Style Selector="ListBoxItem">

+ 14 - 14
src/Ryujinx/UI/Windows/MainWindow.axaml.cs

@@ -633,10 +633,10 @@ namespace Ryujinx.Ava.UI.Windows
                 var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value;
                 if (autoloadDirs.Count > 0)
                 {
-                    var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs);
-                    var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs);
+                    var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs, out int updatesRemoved);
+                    var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs, out int dlcRemoved);
 
-                    ShowNewContentAddedDialog(dlcLoaded, updatesLoaded);
+                    ShowNewContentAddedDialog(dlcLoaded, dlcRemoved, updatesLoaded, updatesRemoved);
                 }
 
                 _isLoading = false;
@@ -648,19 +648,19 @@ namespace Ryujinx.Ava.UI.Windows
             applicationLibraryThread.Start();
         }
 
-        private void ShowNewContentAddedDialog(int numDlcAdded, int numUpdatesAdded)
+        private void ShowNewContentAddedDialog(int numDlcAdded, int numDlcRemoved, int numUpdatesAdded, int numUpdatesRemoved)
         {
-            string msg = numDlcAdded > 0 && numUpdatesAdded > 0
-                ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAndUpdateAddedMessage], numDlcAdded, numUpdatesAdded)
-                : numDlcAdded > 0
-                    ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded)
-                    : numUpdatesAdded > 0
-                        ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded)
-                        : null;
-
-            if (msg is null)
-                return;
+            string[] messages = {
+                numDlcRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcRemovedMessage], numDlcRemoved): null,
+                numDlcAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded): null,
+                numUpdatesRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateRemovedMessage], numUpdatesRemoved): null,
+                numUpdatesAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded) : null
+            };
 
+            string msg = String.Join("\r\n", messages);
+
+            if (String.IsNullOrWhiteSpace(msg))
+                return;
 
             Dispatcher.UIThread.InvokeAsync(async () =>
             {