0
0
Fork 0
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2024-12-22 19:25:44 +00:00

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-24 00:35:51 +10:00
parent 57d8f5b4b5
commit ce24341d1d
8 changed files with 117 additions and 53 deletions

View file

@ -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 // 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 +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 // 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 +1014,24 @@ 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);
} }
} }
} }
} }
} }
foreach (var titleId in titleIdsToSave)
SaveTitleUpdatesForGame(titleId);
foreach (var titleId in titleIdsToRefresh)
RefreshApplicationInfo(titleId);
} }
finally finally
{ {
@ -1011,6 +1042,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 +1444,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 +1454,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",
@ -725,8 +726,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

@ -102,6 +102,8 @@
"SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif", "SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif",
"SettingsTabGeneralHideCursorAlways": "Toujours", "SettingsTabGeneralHideCursorAlways": "Toujours",
"SettingsTabGeneralGameDirectories": "Dossiers des jeux", "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", "SettingsTabGeneralAdd": "Ajouter",
"SettingsTabGeneralRemove": "Retirer", "SettingsTabGeneralRemove": "Retirer",
"SettingsTabSystem": "Système", "SettingsTabSystem": "Système",
@ -708,6 +710,11 @@
"CheatWindowHeading": "Cheats disponibles pour {0} [{1}]", "CheatWindowHeading": "Cheats disponibles pour {0} [{1}]",
"BuildId": "BuildId:", "BuildId": "BuildId:",
"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)",
"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)", "ModWindowHeading": "{0} Mod(s)",
"UserProfilesEditProfile": "Éditer la sélection", "UserProfilesEditProfile": "Éditer la sélection",
"Cancel": "Annuler", "Cancel": "Annuler",

View file

@ -52,6 +52,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;
@ -1259,7 +1260,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
{ {
@ -1270,14 +1271,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);
}); });
} }
@ -1533,14 +1537,18 @@ namespace Ryujinx.Ava.UI.ViewModels
public async Task LoadDlcFromFolder() public async Task LoadDlcFromFolder()
{ {
await LoadContentFromFolder(LocaleKeys.AutoloadDlcAddedMessage, await LoadContentFromFolder(
dirs => ApplicationLibrary.AutoLoadDownloadableContents(dirs)); LocaleKeys.AutoloadDlcAddedMessage,
LocaleKeys.AutoloadDlcRemovedMessage,
ApplicationLibrary.AutoLoadDownloadableContents);
} }
public async Task LoadTitleUpdatesFromFolder() public async Task LoadTitleUpdatesFromFolder()
{ {
await LoadContentFromFolder(LocaleKeys.AutoloadUpdateAddedMessage, await LoadContentFromFolder(
dirs => ApplicationLibrary.AutoLoadTitleUpdates(dirs)); 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" />
<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 <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

@ -647,10 +647,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;
@ -662,28 +662,21 @@ namespace Ryujinx.Ava.UI.Windows
applicationLibraryThread.Start(); applicationLibraryThread.Start();
} }
private Task ShowNewContentAddedDialog(int numDlcAdded, int numUpdatesAdded) private void ShowNewContentAddedDialog(int numDlcAdded, int numDlcRemoved, int numUpdatesAdded, int numUpdatesRemoved)
{ {
var msg = ""; 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
};
if (numDlcAdded > 0 && numUpdatesAdded > 0) string msg = String.Join("\r\n", messages);
{
msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAndUpdateAddedMessage], numDlcAdded, numUpdatesAdded);
}
else if (numDlcAdded > 0)
{
msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadDlcAddedMessage], numDlcAdded);
}
else if (numUpdatesAdded > 0)
{
msg = string.Format(LocaleManager.Instance[LocaleKeys.AutoloadUpdateAddedMessage], numUpdatesAdded);
}
else
{
return Task.CompletedTask;
}
return Dispatcher.UIThread.InvokeAsync(async () => if (String.IsNullOrWhiteSpace(msg))
return;
Dispatcher.UIThread.InvokeAsync(async () =>
{ {
await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle],
msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);