diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa00a2aa..fbf9bfcde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## v1.35.1 +### Features + +- [Multiple] Improvements for missing file checks (#4213 by: HebaruSan) + ### Bugfixes - [Core] Skip corrupted .acf files in Steam library (#4200 by: HebaruSan) diff --git a/Core/Extensions/EnumerableExtensions.cs b/Core/Extensions/EnumerableExtensions.cs index 64a38025b..f5da93761 100644 --- a/Core/Extensions/EnumerableExtensions.cs +++ b/Core/Extensions/EnumerableExtensions.cs @@ -170,8 +170,9 @@ public static IEnumerable DistinctBy(this IEnumerable seq, FuncFunction to go from one node to the next /// All the nodes in the list as a sequence public static IEnumerable TraverseNodes(this T start, Func getNext) + where T : class { - for (T? t = start; t != null; t = getNext(t)) + for (T? t = start; t != null; t = Utilities.DefaultIfThrows(() => getNext(t))) { yield return t; } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index de8d2047f..d440a00e3 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -582,23 +582,62 @@ public static List FindInstallableFiles(CkanModule module, stri } } + /// + /// Returns contents of an installed module + /// + public static IEnumerable<(string path, bool dir, bool exists)> GetModuleContents( + GameInstance instance, + IReadOnlyCollection installed, + HashSet filters) + => GetModuleContents(instance, installed, + installed.SelectMany(f => f.TraverseNodes(Path.GetDirectoryName) + .Skip(1) + .Where(s => s.Length > 0) + .Select(CKANPathUtils.NormalizePath)) + .ToHashSet(), + filters); + + private static IEnumerable<(string path, bool dir, bool exists)> GetModuleContents( + GameInstance instance, + IReadOnlyCollection installed, + HashSet parents, + HashSet filters) + => installed.Where(f => !filters.Any(filt => f.Contains(filt))) + .GroupBy(parents.Contains) + .SelectMany(grp => + grp.Select(p => (path: p, + dir: grp.Key, + exists: grp.Key ? Directory.Exists(instance.ToAbsoluteGameDir(p)) + : File.Exists(instance.ToAbsoluteGameDir(p))))); + /// /// Returns the module contents if and only if we have it /// available in our cache, empty sequence otherwise. /// /// Intended for previews. /// - public static IEnumerable GetModuleContents(NetModuleCache Cache, - GameInstance instance, - CkanModule module, - HashSet filters) + public static IEnumerable<(string path, bool dir, bool exists)> GetModuleContents( + NetModuleCache Cache, + GameInstance instance, + CkanModule module, + HashSet filters) => (Cache.GetCachedFilename(module) is string filename - ? Utilities.DefaultIfThrows(() => FindInstallableFiles(module, filename, instance) - .Where(instF => !filters.Any(filt => - instF.destination != null - && instF.destination.Contains(filt)))) + ? GetModuleContents(instance, + Utilities.DefaultIfThrows( + () => FindInstallableFiles(module, filename, instance)), + filters) : null) - ?? Enumerable.Empty(); + ?? Enumerable.Empty<(string path, bool dir, bool exists)>(); + + private static IEnumerable<(string path, bool dir, bool exists)>? GetModuleContents( + GameInstance instance, + IEnumerable? installable, + HashSet filters) + => installable?.Where(instF => !filters.Any(filt => instF.destination != null + && instF.destination.Contains(filt))) + .Select(f => (path: instance.ToRelativeGameDir(f.destination), + dir: f.source.IsDirectory, + exists: true)); #endregion diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index c8e34ad6f..7e31111d6 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -1,11 +1,12 @@ using System; -using System.IO; using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; +using Autofac; using log4net; +using CKAN.Configuration; using CKAN.Versioning; using CKAN.Games; #if NETSTANDARD2_0 @@ -174,6 +175,8 @@ public static bool IsAutodetected(this IRegistryQuerier querier, string identifi public static bool HasUpdate(this IRegistryQuerier querier, string identifier, GameInstance? instance, + HashSet filters, + bool checkMissingFiles, out CkanModule? latestMod, ICollection? installed = null) { @@ -202,11 +205,10 @@ public static bool HasUpdate(this IRegistryQuerier querier, if (comp == -1 || (comp == 0 && !querier.MetadataChanged(identifier) // Check if any of the files or directories are missing - && (instance == null + && (!checkMissingFiles + || instance == null || (querier.InstalledModule(identifier) - ?.Files - .Select(instance.ToAbsoluteGameDir) - .All(p => Directory.Exists(p) || File.Exists(p)) + ?.AllFilesExist(instance, filters) // Manually installed, consider up to date ?? true)))) { @@ -222,14 +224,21 @@ public static bool HasUpdate(this IRegistryQuerier querier, public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, GameInstance? instance, - HashSet heldIdents) + HashSet heldIdents, + HashSet? ignoreMissingIdents = null) { + var filters = ServiceLocator.Container.Resolve() + .GlobalInstallFilters + .Concat(instance?.InstallFilters + ?? Enumerable.Empty()) + .ToHashSet(); // Get the absolute latest versions ignoring restrictions, // to break out of mutual version-depending deadlocks var unlimited = querier.Installed(false) .Keys .Select(ident => !heldIdents.Contains(ident) - && querier.HasUpdate(ident, instance, + && querier.HasUpdate(ident, instance, filters, + !ignoreMissingIdents?.Contains(ident) ?? true, out CkanModule? latest) && latest is not null && !latest.IsDLC @@ -237,21 +246,29 @@ public static Dictionary> CheckUpgradeable(this IRegistry : querier.GetInstalledVersion(ident)) .OfType() .ToList(); - return querier.CheckUpgradeable(instance, heldIdents, unlimited); + return querier.CheckUpgradeable(instance, heldIdents, unlimited, filters, ignoreMissingIdents); } public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, GameInstance? instance, HashSet heldIdents, - List initial) + List initial, + HashSet? filters = null, + HashSet? ignoreMissingIdents = null) { + filters ??= ServiceLocator.Container.Resolve() + .GlobalInstallFilters + .Concat(instance?.InstallFilters + ?? Enumerable.Empty()) + .ToHashSet(); // Use those as the installed modules var upgradeable = new List(); var notUpgradeable = new List(); foreach (var ident in initial.Select(module => module.identifier)) { if (!heldIdents.Contains(ident) - && querier.HasUpdate(ident, instance, + && querier.HasUpdate(ident, instance, filters, + !ignoreMissingIdents?.Contains(ident) ?? true, out CkanModule? latest, initial) && latest is not null && !latest.IsDLC) diff --git a/Core/Registry/InstalledModule.cs b/Core/Registry/InstalledModule.cs index 883375438..f6d23727b 100644 --- a/Core/Registry/InstalledModule.cs +++ b/Core/Registry/InstalledModule.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.ComponentModel; using System.Collections.Generic; using System.IO; @@ -45,10 +46,10 @@ public class InstalledModule [JsonProperty] private Dictionary installed_files; - public IEnumerable Files => installed_files.Keys; - public string identifier => source_module.identifier; - public CkanModule Module => source_module; - public DateTime InstallTime => install_time; + public IReadOnlyCollection Files => installed_files.Keys; + public string identifier => source_module.identifier; + public CkanModule Module => source_module; + public DateTime InstallTime => install_time; public bool AutoInstalled { @@ -132,6 +133,13 @@ public void Renormalise(GameInstance ksp) #endregion + public bool AllFilesExist(GameInstance instance, + HashSet filters) + // Don't make them reinstall files they've filtered out since installing + => Files.Where(f => !filters.Any(filt => f.Contains(filt))) + .Select(instance.ToAbsoluteGameDir) + .All(p => Directory.Exists(p) || File.Exists(p)); + public override string ToString() => string.Format(AutoInstalled ? Properties.Resources.InstalledModuleToStringAutoInstalled : Properties.Resources.InstalledModuleToString, diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 80cb6207c..d6fca7c0c 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -335,24 +335,27 @@ private void LabelsContextMenuStrip_Opening(object? sender, CancelEventArgs? e) private void labelMenuItem_Click(object? sender, EventArgs? e) { - if (user != null && manager != null && currentInstance != null && SelectedModule != null) + if (user != null + && manager != null + && currentInstance != null + && SelectedModule != null + && sender is ToolStripMenuItem item + && item.Tag is ModuleLabel mlbl) { - var item = sender as ToolStripMenuItem; - var mlbl = item?.Tag as ModuleLabel; - if (item?.Checked ?? false) + if (item.Checked) { - mlbl?.Add(currentInstance.game, SelectedModule.Identifier); + mlbl.Add(currentInstance.game, SelectedModule.Identifier); } else { - mlbl?.Remove(currentInstance.game, SelectedModule.Identifier); + mlbl.Remove(currentInstance.game, SelectedModule.Identifier); } var registry = RegistryManager.Instance(currentInstance, repoData).registry; mainModList.ReapplyLabels(SelectedModule, Conflicts?.ContainsKey(SelectedModule) ?? false, currentInstance.Name, currentInstance.game, registry); ModuleLabelList.ModuleLabels.Save(ModuleLabelList.DefaultPath); UpdateHiddenTagsAndLabels(); - if (mlbl?.HoldVersion ?? false) + if (mlbl.HoldVersion || mlbl.IgnoreMissingFiles) { UpdateCol.Visible = UpdateAllToolButton.Enabled = mainModList.ResetHasUpdate(currentInstance, registry, ChangeSet, ModGrid.Rows); @@ -538,19 +541,18 @@ public void MarkAllUpdates() { WithFrozenChangeset(() => { - foreach (var gmod in mainModList.full_list_of_mod_rows - .Values - .Select(row => row.Tag) - .OfType()) + var checkboxes = mainModList.full_list_of_mod_rows + .Values + .Where(row => row.Tag is GUIMod {Identifier: string ident} + && (!Main.Instance?.LabelsHeld(ident) ?? false)) + .SelectWithCatch(row => row.Cells[UpdateCol.Index], + (row, exc) => null) + .OfType(); + foreach (var checkbox in checkboxes) { - if (gmod?.HasUpdate ?? false) - { - if (!Main.Instance?.LabelsHeld(gmod.Identifier) ?? false) - { - gmod.SelectedMod = gmod.LatestCompatibleMod; - } - } + checkbox.Value = true; } + ModGrid.CommitEdit(DataGridViewDataErrorContexts.Commit); // only sort by Update column if checkbox in settings checked if (guiConfig?.AutoSortByUpdate ?? false) @@ -935,7 +937,7 @@ private void ModGrid_CellValueChanged(object? sender, DataGridViewCellEventArgs? : gmod.LatestCompatibleMod : gmod.InstalledMod?.Module; - if (nowChecked && gmod.SelectedMod == gmod.LatestCompatibleMod) + if (gmod.SelectedMod == gmod.LatestCompatibleMod) { // Reinstall, force update without change UpdateChangeSetAndConflicts(currentInstance, diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index fb5c77bc0..c1fe525e8 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -7,6 +7,7 @@ using Autofac; +using CKAN.Configuration; using CKAN.Versioning; using CKAN.GUI.Attributes; @@ -138,6 +139,19 @@ private void UpdateHeaderInfo(GUIMod gmod, GameVersionCriteria crit) var pageToAlert = module.IsCompatible(crit) ? RelationshipTabPage : VersionsTabPage; pageToAlert.ImageKey = "Stop"; } + if (manager?.CurrentInstance is GameInstance inst) + { + var filters = ServiceLocator.Container.Resolve() + .GlobalInstallFilters + .Concat(inst.InstallFilters) + .ToHashSet(); + ContentTabPage.ImageKey = ModuleLabels.IgnoreMissingIdentifiers(inst) + .Contains(gmod.Identifier) + || (gmod.InstalledMod?.AllFilesExist(inst, filters) + ?? true) + ? "" + : "Stop"; + } ModInfoTabControl.ResumeLayout(); }); diff --git a/GUI/Controls/ModInfoTabs/Contents.cs b/GUI/Controls/ModInfoTabs/Contents.cs index 898c87996..6b0a8df84 100644 --- a/GUI/Controls/ModInfoTabs/Contents.cs +++ b/GUI/Controls/ModInfoTabs/Contents.cs @@ -32,7 +32,8 @@ public GUIMod? SelectedModule if (value != selectedModule) { selectedModule = value; - Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(selectedModule?.ToModule())); + Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(selectedModule?.InstalledMod, + selectedModule?.ToModule())); } } get => selectedModule; @@ -41,9 +42,11 @@ public GUIMod? SelectedModule [ForbidGUICalls] public void RefreshModContentsTree() { - if (currentModContentsModule != null) + if (currentModContentsInstalledModule != null + || currentModContentsModule != null) { - Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(currentModContentsModule, true)); + Util.Invoke(ContentsPreviewTree, () => _UpdateModContentsTree(currentModContentsInstalledModule, + currentModContentsModule, true)); } } @@ -51,9 +54,10 @@ public void RefreshModContentsTree() private static GameInstanceManager? manager => Main.Instance?.Manager; - private GUIMod? selectedModule; - private CkanModule? currentModContentsModule; - private bool cancelExpandCollapse; + private GUIMod? selectedModule; + private InstalledModule? currentModContentsInstalledModule; + private CkanModule? currentModContentsModule; + private bool cancelExpandCollapse; private void ContentsPreviewTree_NodeMouseDoubleClick(object? sender, TreeNodeMouseClickEventArgs? e) { @@ -104,7 +108,8 @@ private void ContentsOpenButton_Click(object? sender, EventArgs? e) } } - private void _UpdateModContentsTree(CkanModule? module, bool force = false) + private void _UpdateModContentsTree(InstalledModule? instMod, CkanModule? module, + bool force = false) { if (module == null) { @@ -126,6 +131,7 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) else { currentModContentsModule = module; + currentModContentsInstalledModule = instMod; } if (module.IsMetapackage) { @@ -165,13 +171,12 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) var filters = ServiceLocator.Container.Resolve().GlobalInstallFilters .Concat(inst.InstallFilters) .ToHashSet(); - var tuples = ModuleInstaller.GetModuleContents(manager.Cache, inst, module, filters) - .Select(f => (path: inst.ToRelativeGameDir(f.destination), - dir: f.source.IsDirectory, - exists: !selectedModule.IsInstalled - || File.Exists(f.destination) - || Directory.Exists(f.destination))) - .ToArray(); + var tuples = (instMod != null + ? ModuleInstaller.GetModuleContents(inst, instMod.Files, filters) + : ModuleInstaller.GetModuleContents(manager.Cache, inst, + module, filters)) + // Load fully in bg + .ToArray(); // Stop if user switched to another mod if (rootNode.TreeView != null) { @@ -185,10 +190,11 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) dir, exists); } rootNode.ExpandAll(); - var initialFocus = FirstMatching(rootNode, - n => n.ForeColor == Color.Red) - ?? rootNode; - initialFocus.EnsureVisible(); + // First scroll to the top + rootNode.EnsureVisible(); + // Then scroll down to the first red node + FirstMatching(rootNode, n => n.ForeColor == Color.Red) + ?.EnsureVisible(); ContentsPreviewTree.EndUpdate(); UseWaitCursor = false; }); diff --git a/GUI/Dialogs/EditLabelsDialog.Designer.cs b/GUI/Dialogs/EditLabelsDialog.Designer.cs index f82ad13a9..415d3c4a5 100644 --- a/GUI/Dialogs/EditLabelsDialog.Designer.cs +++ b/GUI/Dialogs/EditLabelsDialog.Designer.cs @@ -46,6 +46,7 @@ private void InitializeComponent() this.AlertOnInstallCheckBox = new System.Windows.Forms.CheckBox(); this.RemoveOnInstallCheckBox = new System.Windows.Forms.CheckBox(); this.HoldVersionCheckBox = new System.Windows.Forms.CheckBox(); + this.IgnoreMissingFilesCheckBox = new System.Windows.Forms.CheckBox(); this.CreateButton = new System.Windows.Forms.Button(); this.CloseButton = new System.Windows.Forms.Button(); this.SaveButton = new System.Windows.Forms.Button(); @@ -67,6 +68,8 @@ private void InitializeComponent() // this.CreateButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)); + this.CreateButton.AutoSize = true; + this.CreateButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.CreateButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.CreateButton.Location = new System.Drawing.Point(10, 10); this.CreateButton.Name = "CreateButton"; @@ -121,6 +124,7 @@ private void InitializeComponent() this.EditDetailsPanel.Controls.Add(this.AlertOnInstallCheckBox); this.EditDetailsPanel.Controls.Add(this.RemoveOnInstallCheckBox); this.EditDetailsPanel.Controls.Add(this.HoldVersionCheckBox); + this.EditDetailsPanel.Controls.Add(this.IgnoreMissingFilesCheckBox); this.EditDetailsPanel.Controls.Add(this.SaveButton); this.EditDetailsPanel.Controls.Add(this.CancelEditButton); this.EditDetailsPanel.Controls.Add(this.DeleteButton); @@ -192,6 +196,8 @@ private void InitializeComponent() // this.ColorButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)); + this.ColorButton.AutoSize = true; + this.ColorButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.ColorButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.ColorButton.Location = new System.Drawing.Point(118, 40); this.ColorButton.Name = "ColorButton"; @@ -231,7 +237,7 @@ private void InitializeComponent() // this.NotifyOnChangesCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); this.NotifyOnChangesCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.NotifyOnChangesCheckBox.Location = new System.Drawing.Point(118, 130); + this.NotifyOnChangesCheckBox.Location = new System.Drawing.Point(118, 124); this.NotifyOnChangesCheckBox.Name = "NotifyOnChangesCheckBox"; this.NotifyOnChangesCheckBox.Size = new System.Drawing.Size(200, 23); resources.ApplyResources(this.NotifyOnChangesCheckBox, "NotifyOnChangesCheckBox"); @@ -240,7 +246,7 @@ private void InitializeComponent() // this.RemoveOnChangesCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); this.RemoveOnChangesCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.RemoveOnChangesCheckBox.Location = new System.Drawing.Point(118, 160); + this.RemoveOnChangesCheckBox.Location = new System.Drawing.Point(118, 148); this.RemoveOnChangesCheckBox.Name = "RemoveOnChangesCheckBox"; this.RemoveOnChangesCheckBox.Size = new System.Drawing.Size(200, 23); resources.ApplyResources(this.RemoveOnChangesCheckBox, "RemoveOnChangesCheckBox"); @@ -249,7 +255,7 @@ private void InitializeComponent() // this.AlertOnInstallCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); this.AlertOnInstallCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.AlertOnInstallCheckBox.Location = new System.Drawing.Point(118, 190); + this.AlertOnInstallCheckBox.Location = new System.Drawing.Point(118, 172); this.AlertOnInstallCheckBox.Name = "AlertOnInstallCheckBox"; this.AlertOnInstallCheckBox.Size = new System.Drawing.Size(200, 23); resources.ApplyResources(this.AlertOnInstallCheckBox, "AlertOnInstallCheckBox"); @@ -258,7 +264,7 @@ private void InitializeComponent() // this.RemoveOnInstallCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); this.RemoveOnInstallCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.RemoveOnInstallCheckBox.Location = new System.Drawing.Point(118, 220); + this.RemoveOnInstallCheckBox.Location = new System.Drawing.Point(118, 196); this.RemoveOnInstallCheckBox.Name = "RemoveOnInstallCheckBox"; this.RemoveOnInstallCheckBox.Size = new System.Drawing.Size(200, 23); resources.ApplyResources(this.RemoveOnInstallCheckBox, "RemoveOnInstallCheckBox"); @@ -267,15 +273,26 @@ private void InitializeComponent() // this.HoldVersionCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); this.HoldVersionCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.HoldVersionCheckBox.Location = new System.Drawing.Point(118, 250); + this.HoldVersionCheckBox.Location = new System.Drawing.Point(118, 220); this.HoldVersionCheckBox.Name = "HoldVersionCheckBox"; this.HoldVersionCheckBox.Size = new System.Drawing.Size(200, 23); resources.ApplyResources(this.HoldVersionCheckBox, "HoldVersionCheckBox"); + // + // IgnoreMissingFilesCheckBox + // + this.IgnoreMissingFilesCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.IgnoreMissingFilesCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.IgnoreMissingFilesCheckBox.Location = new System.Drawing.Point(118, 244); + this.IgnoreMissingFilesCheckBox.Name = "IgnoreMissingFilesCheckBox"; + this.IgnoreMissingFilesCheckBox.Size = new System.Drawing.Size(200, 23); + resources.ApplyResources(this.IgnoreMissingFilesCheckBox, "IgnoreMissingFilesCheckBox"); // // SaveButton // this.SaveButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)); + this.SaveButton.AutoSize = true; + this.SaveButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.SaveButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.SaveButton.Location = new System.Drawing.Point(38, 320); this.SaveButton.Name = "SaveButton"; @@ -289,6 +306,8 @@ private void InitializeComponent() // this.CancelEditButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)); + this.CancelEditButton.AutoSize = true; + this.CancelEditButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.CancelEditButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.CancelEditButton.Location = new System.Drawing.Point(118, 320); this.CancelEditButton.Name = "CancelEditButton"; @@ -302,6 +321,8 @@ private void InitializeComponent() // this.DeleteButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)); + this.DeleteButton.AutoSize = true; + this.DeleteButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.DeleteButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.DeleteButton.Location = new System.Drawing.Point(198, 320); this.DeleteButton.Name = "DeleteButton"; @@ -315,6 +336,8 @@ private void InitializeComponent() // this.CloseButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)); + this.CloseButton.AutoSize = true; + this.CloseButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.CloseButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.CloseButton.Location = new System.Drawing.Point(10, 397); this.CloseButton.Name = "CloseButton"; @@ -362,6 +385,7 @@ private void InitializeComponent() private System.Windows.Forms.CheckBox AlertOnInstallCheckBox; private System.Windows.Forms.CheckBox RemoveOnInstallCheckBox; private System.Windows.Forms.CheckBox HoldVersionCheckBox; + private System.Windows.Forms.CheckBox IgnoreMissingFilesCheckBox; private System.Windows.Forms.Label ColorLabel; private System.Windows.Forms.Button ColorButton; private System.Windows.Forms.Button CreateButton; diff --git a/GUI/Dialogs/EditLabelsDialog.cs b/GUI/Dialogs/EditLabelsDialog.cs index f791a148b..9867ddcb1 100644 --- a/GUI/Dialogs/EditLabelsDialog.cs +++ b/GUI/Dialogs/EditLabelsDialog.cs @@ -33,6 +33,7 @@ public EditLabelsDialog(IUser user, GameInstanceManager manager, ModuleLabelList ToolTip.SetToolTip(AlertOnInstallCheckBox, Properties.Resources.EditLabelsToolTipAlertOnInstall); ToolTip.SetToolTip(RemoveOnInstallCheckBox, Properties.Resources.EditLabelsToolTipRemoveOnInstall); ToolTip.SetToolTip(HoldVersionCheckBox, Properties.Resources.EditLabelsToolTipHoldVersion); + ToolTip.SetToolTip(IgnoreMissingFilesCheckBox, Properties.Resources.EditLabelsToolTipIgnoreMissingFiles); ToolTip.SetToolTip(MoveUpButton, Properties.Resources.EditLabelsToolTipMoveUp); ToolTip.SetToolTip(MoveDownButton, Properties.Resources.EditLabelsToolTipMoveDown); } @@ -204,6 +205,7 @@ private void StartEdit(ModuleLabel lbl) AlertOnInstallCheckBox.Checked = lbl.AlertOnInstall; RemoveOnInstallCheckBox.Checked = lbl.RemoveOnInstall; HoldVersionCheckBox.Checked = lbl.HoldVersion; + IgnoreMissingFilesCheckBox.Checked = lbl.IgnoreMissingFiles; DeleteButton.Enabled = labels.Labels.Contains(lbl); EnableDisableUpDownButtons(); @@ -300,12 +302,13 @@ private bool TrySave(out string errMsg) || string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem.ToString()) ? null : InstanceNameComboBox.SelectedItem.ToString(); - currentlyEditing.Hide = HideFromOtherFiltersCheckBox.Checked; - currentlyEditing.NotifyOnChange = NotifyOnChangesCheckBox.Checked; - currentlyEditing.RemoveOnChange = RemoveOnChangesCheckBox.Checked; - currentlyEditing.AlertOnInstall = AlertOnInstallCheckBox.Checked; - currentlyEditing.RemoveOnInstall = RemoveOnInstallCheckBox.Checked; - currentlyEditing.HoldVersion = HoldVersionCheckBox.Checked; + currentlyEditing.Hide = HideFromOtherFiltersCheckBox.Checked; + currentlyEditing.NotifyOnChange = NotifyOnChangesCheckBox.Checked; + currentlyEditing.RemoveOnChange = RemoveOnChangesCheckBox.Checked; + currentlyEditing.AlertOnInstall = AlertOnInstallCheckBox.Checked; + currentlyEditing.RemoveOnInstall = RemoveOnInstallCheckBox.Checked; + currentlyEditing.HoldVersion = HoldVersionCheckBox.Checked; + currentlyEditing.IgnoreMissingFiles = IgnoreMissingFilesCheckBox.Checked; if (!labels.Labels.Contains(currentlyEditing)) { labels.Labels = labels.Labels @@ -367,16 +370,16 @@ private bool HasChanges() ? null : InstanceNameComboBox.SelectedItem.ToString(); return EditDetailsPanel.Visible && currentlyEditing != null - && ( currentlyEditing.Name != NameTextBox.Text - || currentlyEditing.Color != ColorButton.BackColor - || currentlyEditing.InstanceName != newInst - || currentlyEditing.Hide != HideFromOtherFiltersCheckBox.Checked - || currentlyEditing.NotifyOnChange != NotifyOnChangesCheckBox.Checked - || currentlyEditing.RemoveOnChange != RemoveOnChangesCheckBox.Checked - || currentlyEditing.AlertOnInstall != AlertOnInstallCheckBox.Checked - || currentlyEditing.RemoveOnInstall != RemoveOnInstallCheckBox.Checked - || currentlyEditing.HoldVersion != HoldVersionCheckBox.Checked - ); + && ( currentlyEditing.Name != NameTextBox.Text + || currentlyEditing.Color != ColorButton.BackColor + || currentlyEditing.InstanceName != newInst + || currentlyEditing.Hide != HideFromOtherFiltersCheckBox.Checked + || currentlyEditing.NotifyOnChange != NotifyOnChangesCheckBox.Checked + || currentlyEditing.RemoveOnChange != RemoveOnChangesCheckBox.Checked + || currentlyEditing.AlertOnInstall != AlertOnInstallCheckBox.Checked + || currentlyEditing.RemoveOnInstall != RemoveOnInstallCheckBox.Checked + || currentlyEditing.HoldVersion != HoldVersionCheckBox.Checked + || currentlyEditing.IgnoreMissingFiles != IgnoreMissingFilesCheckBox.Checked); } private ModuleLabel? currentlyEditing; diff --git a/GUI/Dialogs/EditLabelsDialog.resx b/GUI/Dialogs/EditLabelsDialog.resx index 46082b6fb..d48c141b4 100644 --- a/GUI/Dialogs/EditLabelsDialog.resx +++ b/GUI/Dialogs/EditLabelsDialog.resx @@ -130,6 +130,7 @@ Alert on install Remove on install Don't upgrade + Ignore missing files Close Save Cancel diff --git a/GUI/Dialogs/InstallFiltersDialog.cs b/GUI/Dialogs/InstallFiltersDialog.cs index e9e1a10d6..6c312d163 100644 --- a/GUI/Dialogs/InstallFiltersDialog.cs +++ b/GUI/Dialogs/InstallFiltersDialog.cs @@ -22,6 +22,8 @@ public InstallFiltersDialog(IConfiguration globalConfig, GameInstance instance) this.instance = instance; } + public bool Changed { get; private set; } = false; + /// /// Open the user guide when the user presses F1 /// @@ -48,8 +50,12 @@ private void InstallFiltersDialog_Load(object? sender, EventArgs? e) private void InstallFiltersDialog_Closing(object? sender, CancelEventArgs? e) { - globalConfig.GlobalInstallFilters = GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); - instance.InstallFilters = InstanceFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); + var newGlobal = GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); + var newInstance = InstanceFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); + Changed = !globalConfig.GlobalInstallFilters.SequenceEqual(newGlobal) + || !instance.InstallFilters.SequenceEqual(newInstance); + globalConfig.GlobalInstallFilters = newGlobal; + instance.InstallFilters = newInstance; } private void AddMiniAVCButton_Click(object? sender, EventArgs? e) @@ -57,8 +63,7 @@ private void AddMiniAVCButton_Click(object? sender, EventArgs? e) GlobalFiltersTextBox.Text = string.Join(Environment.NewLine, GlobalFiltersTextBox.Text.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) .Concat(miniAVC) - .Distinct() - ); + .Distinct()); } private readonly IConfiguration globalConfig; diff --git a/GUI/Labels/ModuleLabel.cs b/GUI/Labels/ModuleLabel.cs index e8b1b154e..99e5ebfad 100644 --- a/GUI/Labels/ModuleLabel.cs +++ b/GUI/Labels/ModuleLabel.cs @@ -57,6 +57,10 @@ public ModuleLabel(string name) [DefaultValue(false)] public bool HoldVersion; + [JsonProperty("ignore_missing_files", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool IgnoreMissingFiles; + [JsonProperty("module_identifiers_by_game", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(JsonToGamesDictionaryConverter))] private readonly Dictionary> ModuleIdentifiers = diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs index 902d97c12..2bd084199 100644 --- a/GUI/Labels/ModuleLabelList.cs +++ b/GUI/Labels/ModuleLabelList.cs @@ -83,5 +83,10 @@ public IEnumerable HeldIdentifiers(GameInstance inst) => LabelsFor(inst.Name).Where(l => l.HoldVersion) .SelectMany(l => l.IdentifiersFor(inst.game)) .Distinct(); + + public IEnumerable IgnoreMissingIdentifiers(GameInstance inst) + => LabelsFor(inst.Name).Where(l => l.IgnoreMissingFiles) + .SelectMany(l => l.IdentifiersFor(inst.game)) + .Distinct(); } } diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 2211e12aa..ebb4f2384 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -724,6 +724,12 @@ private void installFiltersToolStripMenuItem_Click(object? sender, EventArgs? e) var dlg = new InstallFiltersDialog(ServiceLocator.Container.Resolve(), CurrentInstance); dlg.ShowDialog(this); Enabled = true; + if (dlg.Changed) + { + // The Update checkbox might appear or disappear if missing files were or are filtered out + RefreshModList(false); + ModInfo.RefreshModContentsTree(); + } } } diff --git a/GUI/Main/MainLabels.cs b/GUI/Main/MainLabels.cs index c08a2591e..f7089c261 100644 --- a/GUI/Main/MainLabels.cs +++ b/GUI/Main/MainLabels.cs @@ -68,6 +68,11 @@ public bool LabelsHeld(string identifier) && ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) .Any(l => l.HoldVersion && l.ContainsModule(CurrentInstance.game, identifier)); + public bool LabelsIgnoreMissing(string identifier) + => CurrentInstance != null + && ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Any(l => l.IgnoreMissingFiles && l.ContainsModule(CurrentInstance.game, identifier)); + #endregion } } diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index be46fbf74..2a12b8bc7 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -470,14 +470,17 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier registry, // Skip reinstalls .Where(upg => upg.Mod != upg.targetMod) .ToArray(); - if (upgrades.Length > 0) + if (upgrades.Length > 0 && instance != null) { var upgradeable = registry.CheckUpgradeable(instance, // Hold identifiers not chosen for upgrading registry.Installed(false) .Select(kvp => kvp.Key) .Except(upgrades.Select(ch => ch.Mod.identifier)) - .ToHashSet()) + .ToHashSet(), + ModuleLabelList.ModuleLabels + .IgnoreMissingIdentifiers(instance) + .ToHashSet()) [true] .ToDictionary(m => m.identifier, m => m); @@ -523,6 +526,8 @@ public bool ResetHasUpdate(GameInstance inst, { var upgGroups = registry.CheckUpgradeable(inst, ModuleLabelList.ModuleLabels.HeldIdentifiers(inst) + .ToHashSet(), + ModuleLabelList.ModuleLabels.IgnoreMissingIdentifiers(inst) .ToHashSet()); var dlls = registry.InstalledDlls.ToList(); foreach ((var upgradeable, var mods) in upgGroups) @@ -559,8 +564,13 @@ private void CheckRowUpgradeable(GameInstance inst, full_list_of_mod_rows[ident] = MakeRow(gmod, ChangeSet, inst.Name, inst.game); var rowIndex = row.Index; + var selected = row.Selected; rows.Remove(row); rows.Insert(rowIndex, newRow); + if (selected) + { + rows[rowIndex].Selected = true; + } } } } @@ -583,14 +593,16 @@ public IEnumerable GetGUIMods(IRegistryQuerier registry, config?.HideEpochs ?? false, config?.HideV ?? false); private static IEnumerable GetGUIMods(IRegistryQuerier registry, - RepositoryDataManager repoData, - GameInstance inst, - GameVersionCriteria versionCriteria, - HashSet installedIdents, - bool hideEpochs, - bool hideV) + RepositoryDataManager repoData, + GameInstance inst, + GameVersionCriteria versionCriteria, + HashSet installedIdents, + bool hideEpochs, + bool hideV) => registry.CheckUpgradeable(inst, ModuleLabelList.ModuleLabels.HeldIdentifiers(inst) + .ToHashSet(), + ModuleLabelList.ModuleLabels.IgnoreMissingIdentifiers(inst) .ToHashSet()) .SelectMany(kvp => kvp.Value .Select(mod => registry.IsAutodetected(mod.identifier) diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 4a496cb43..deb956b23 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -417,6 +417,7 @@ Are you sure you want to skip this change? If checked, the change set screen will alert you if this mod is about to be installed If checked, modules will be removed from this label if they are installed If checked, modules will not be upgraded + If checked, you will not be prompted to re-install missing files for modules with this label Move up Move down Some of your watched mods have updated: diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index 277f76352..a590d8861 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -271,7 +271,7 @@ public void HasUpdate_WithUpgradeableManuallyInstalledMod_ReturnsTrue() }); // Act - bool has = registry.HasUpdate(mod.identifier, gameInst, out _); + bool has = registry.HasUpdate(mod.identifier, gameInst, new HashSet(), false, out _); // Assert Assert.IsTrue(has, "Can't upgrade manually installed DLL"); @@ -327,7 +327,8 @@ public void HasUpdate_OtherModDependsOnCurrent_ReturnsFalse() GameVersionCriteria crit = new GameVersionCriteria(olderDepMod?.ksp_version); // Act - bool has = registry.HasUpdate(olderDepMod?.identifier!, gameInst, out _, + bool has = registry.HasUpdate(olderDepMod?.identifier!, gameInst, new HashSet(), false, + out _, registry.InstalledModules .Select(im => im.Module) .ToList());