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
This commit is contained in:
Aaron Murgatroyd 2024-10-23 23:50:28 +10:00
parent 4d1ebaf9ba
commit 309c314bf0
9 changed files with 109 additions and 45 deletions

View File

@ -1,5 +1,6 @@
using DynamicData; using DynamicData;
using DynamicData.Kernel; using DynamicData.Kernel;
using Gommon;
using LibHac; using LibHac;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
@ -802,17 +803,31 @@ namespace Ryujinx.UI.App.Common
// Searches the provided directories for DLC NSP files that are _valid for the currently detected games in the // Searches the provided directories for DLC NSP files that are _valid for the currently detected games in the
// library_, and then enables those DLC. // library_, and then enables those DLC.
public int AutoLoadDownloadableContents(List<string> appDirs) public int AutoLoadDownloadableContents(List<string> appDirs, out int numDlcRemoved)
{ {
_cancellationToken = new CancellationTokenSource(); _cancellationToken = new CancellationTokenSource();
List<string> dlcPaths = new(); List<string> dlcPaths = new();
int newDlcLoaded = 0; int newDlcLoaded = 0;
numDlcRemoved = 0;
try 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) foreach (string appDir in appDirs)
{ {
Logger.Notice.Print(LogClass.Application, $"Auto loading DLC from: {appDir}");
if (_cancellationToken.Token.IsCancellationRequested) if (_cancellationToken.Token.IsCancellationRequested)
{ {
return newDlcLoaded; return newDlcLoaded;
@ -901,17 +916,37 @@ namespace Ryujinx.UI.App.Common
// Searches the provided directories for update NSP files that are _valid for the currently detected games in the // 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 // 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. // 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(); _cancellationToken = new CancellationTokenSource();
List<string> updatePaths = new(); List<string> updatePaths = new();
int numUpdatesLoaded = 0; int numUpdatesLoaded = 0;
numUpdatesRemoved = 0;
try 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) foreach (string appDir in appDirs)
{ {
Logger.Notice.Print(LogClass.Application, $"Auto loading updates from: {appDir}");
if (_cancellationToken.Token.IsCancellationRequested) if (_cancellationToken.Token.IsCancellationRequested)
{ {
return numUpdatesLoaded; return numUpdatesLoaded;
@ -980,27 +1015,21 @@ namespace Ryujinx.UI.App.Common
{ {
if (!_titleUpdates.Lookup(update).HasValue) if (!_titleUpdates.Lookup(update).HasValue)
{ {
var currentlySelected = TitleUpdates.Items.FirstOrOptional(it => bool shouldSelect = AddAndAutoSelectUpdate(update);
it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected); titleIdsToSave.Add(update.TitleIdBase);
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);
numUpdatesLoaded++; numUpdatesLoaded++;
if (shouldSelect) if (shouldSelect)
{ {
RefreshApplicationInfo(update.TitleIdBase); titleIdsToRefresh.Add(update.TitleIdBase);
} }
} }
} }
} }
} }
titleIdsToSave.ForEach(titleId => SaveTitleUpdatesForGame(titleId));
titleIdsToRefresh.ForEach(titleId => RefreshApplicationInfo(titleId));
} }
finally finally
{ {
@ -1011,6 +1040,24 @@ namespace Ryujinx.UI.App.Common
return numUpdatesLoaded; 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) protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e)
{ {
ApplicationCountUpdated?.Invoke(null, e); ApplicationCountUpdated?.Invoke(null, e);
@ -1395,8 +1442,8 @@ namespace Ryujinx.UI.App.Common
if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates))
{ {
var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet();
bool updatesChanged = false;
bool addedNewUpdate = false;
foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version)) foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version))
{ {
if (!savedUpdateLookup.Contains(update)) if (!savedUpdateLookup.Contains(update))
@ -1405,17 +1452,19 @@ namespace Ryujinx.UI.App.Common
if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version) if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version)
{ {
shouldSelect = true; 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; modifiedVersion = modifiedVersion || shouldSelect;
it.AddOrUpdate((update, 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(); var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList();
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates);

View File

@ -106,6 +106,7 @@
"SettingsTabGeneralHideCursorAlways": "Always", "SettingsTabGeneralHideCursorAlways": "Always",
"SettingsTabGeneralGameDirectories": "Game Directories", "SettingsTabGeneralGameDirectories": "Game Directories",
"SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories",
"SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically",
"SettingsTabGeneralAdd": "Add", "SettingsTabGeneralAdd": "Add",
"SettingsTabGeneralRemove": "Remove", "SettingsTabGeneralRemove": "Remove",
"SettingsTabSystem": "System", "SettingsTabSystem": "System",
@ -731,8 +732,9 @@
"DlcWindowHeading": "{0} Downloadable Content(s)", "DlcWindowHeading": "{0} Downloadable Content(s)",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcAddedMessage": "{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", "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)", "ModWindowHeading": "{0} Mod(s)",
"UserProfilesEditProfile": "Edit Selected", "UserProfilesEditProfile": "Edit Selected",
"Cancel": "Cancel", "Cancel": "Cancel",

View File

@ -106,6 +106,7 @@
"SettingsTabGeneralHideCursorAlways": "Toujours", "SettingsTabGeneralHideCursorAlways": "Toujours",
"SettingsTabGeneralGameDirectories": "Dossiers des jeux", "SettingsTabGeneralGameDirectories": "Dossiers des jeux",
"SettingsTabGeneralAutoloadDirectories": "Dossiers des mises à jour/DLC", "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", "SettingsTabGeneralAdd": "Ajouter",
"SettingsTabGeneralRemove": "Retirer", "SettingsTabGeneralRemove": "Retirer",
"SettingsTabSystem": "Système", "SettingsTabSystem": "Système",
@ -730,9 +731,9 @@
"DlcWindowBundledContentNotice": "Les DLC inclus avec le jeu ne peuvent pas être supprimés mais peuvent être désactivés.", "DlcWindowBundledContentNotice": "Les DLC inclus avec le jeu ne peuvent pas être supprimés mais peuvent être désactivés.",
"DlcWindowHeading": "{0} Contenu(s) téléchargeable(s)", "DlcWindowHeading": "{0} Contenu(s) téléchargeable(s)",
"DlcWindowDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(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)", "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)", "ModWindowHeading": "{0} Mod(s)",
"UserProfilesEditProfile": "Éditer la sélection", "UserProfilesEditProfile": "Éditer la sélection",
"Cancel": "Annuler", "Cancel": "Annuler",

View File

@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private PlayerIndex _playerId; private PlayerIndex _playerId;
private int _controller; private int _controller;
private readonly int _controllerNumber;
private string _controllerImage; private string _controllerImage;
private int _device; private int _device;
private object _configViewModel; private object _configViewModel;

View File

@ -51,6 +51,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public class MainWindowViewModel : BaseModel public class MainWindowViewModel : BaseModel
{ {
private const int HotKeyPressDelayMs = 500; private const int HotKeyPressDelayMs = 500;
private delegate int LoadContentFromFolderDelegate(List<string> dirs, out int numRemoved);
private ObservableCollectionExtended<ApplicationData> _applications; private ObservableCollectionExtended<ApplicationData> _applications;
private string _aspectStatusText; private string _aspectStatusText;
@ -1262,7 +1263,7 @@ namespace Ryujinx.Ava.UI.ViewModels
_rendererWaitEvent.Set(); _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 var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{ {
@ -1273,14 +1274,17 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0) if (result.Count > 0)
{ {
var dirs = result.Select(it => it.Path.LocalPath).ToList(); 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 Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.ShowTextDialog( 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); msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
}); });
} }
@ -1536,12 +1540,18 @@ namespace Ryujinx.Ava.UI.ViewModels
public async Task LoadDlcFromFolder() public async Task LoadDlcFromFolder()
{ {
await LoadContentFromFolder(LocaleKeys.AutoloadDlcAddedMessage, ApplicationLibrary.AutoLoadDownloadableContents); await LoadContentFromFolder(
LocaleKeys.AutoloadDlcAddedMessage,
LocaleKeys.AutoloadDlcRemovedMessage,
ApplicationLibrary.AutoLoadDownloadableContents);
} }
public async Task LoadTitleUpdatesFromFolder() public async Task LoadTitleUpdatesFromFolder()
{ {
await LoadContentFromFolder(LocaleKeys.AutoloadUpdateAddedMessage, ApplicationLibrary.AutoLoadTitleUpdates); await LoadContentFromFolder(
LocaleKeys.AutoloadUpdateAddedMessage,
LocaleKeys.AutoloadUpdateRemovedMessage,
ApplicationLibrary.AutoLoadTitleUpdates);
} }
public async Task OpenFolder() public async Task OpenFolder()

View File

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

View File

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

View File

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

View File

@ -640,10 +640,10 @@ namespace Ryujinx.Ava.UI.Windows
var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value; var autoloadDirs = ConfigurationState.Instance.UI.AutoloadDirs.Value;
if (autoloadDirs.Count > 0) if (autoloadDirs.Count > 0)
{ {
var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs); var updatesLoaded = ApplicationLibrary.AutoLoadTitleUpdates(autoloadDirs, out int updatesRemoved);
var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs); var dlcLoaded = ApplicationLibrary.AutoLoadDownloadableContents(autoloadDirs, out int dlcRemoved);
ShowNewContentAddedDialog(dlcLoaded, updatesLoaded); ShowNewContentAddedDialog(dlcLoaded, dlcRemoved, updatesLoaded, updatesRemoved);
} }
_isLoading = false; _isLoading = false;
@ -655,20 +655,20 @@ namespace Ryujinx.Ava.UI.Windows
applicationLibraryThread.Start(); 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[] messages = {
? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAndUpdateAddedMessage], numDlcAdded, numUpdatesAdded) numDlcRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcRemovedMessage], numDlcRemoved): null,
: numDlcAdded > 0 numDlcAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded): null,
? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded) numUpdatesRemoved > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateRemovedMessage], numUpdatesRemoved): null,
: numUpdatesAdded > 0 numUpdatesAdded > 0 ? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded) : null
? string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded) };
: null;
if (msg is null) string msg = String.Join("\r\n", messages);
if (String.IsNullOrWhiteSpace(msg))
return; return;
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle],