diff --git a/CHANGELOG.md b/CHANGELOG.md index 212f07c19..9d4086f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ All notable changes to this project will be documented in this file. - [ConsoleUI] Add downloads column for ConsoleUI (#4063 by: HebaruSan) - [ConsoleUI] Play game option for ConsoleUI (#4064 by: HebaruSan) - [ConsoleUI] ConsoleUI prompt to delete non-empty folders after uninstall (#4066 by: HebaruSan) -- [Multiple] Treat mods with missing files as upgradeable/reinstallable (#4067 by: HebaruSan) +- [Multiple] Treat mods with missing files as upgradeable/reinstallable (#4067, #4195 by: HebaruSan) - [ConsoleUI] Conflicting recommendations check for ConsoleUI (#4085 by: HebaruSan) - [Build] Linux: Improve desktop entries (#4092 by: mmvanheusden; reviewed: HebaruSan) - [ConsoleUI] Install from .ckan file option for ConsoleUI (#4103 by: HebaruSan) diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 6163b2c24..de8d2047f 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -584,21 +584,21 @@ public static List FindInstallableFiles(CkanModule module, stri /// /// Returns the module contents if and only if we have it - /// available in our cache. Returns null, otherwise. + /// available in our cache, empty sequence otherwise. /// /// Intended for previews. /// - public static IEnumerable GetModuleContentsList(NetModuleCache Cache, - GameInstance instance, - - CkanModule module) + public static IEnumerable GetModuleContents(NetModuleCache Cache, + GameInstance instance, + CkanModule module, + HashSet filters) => (Cache.GetCachedFilename(module) is string filename ? Utilities.DefaultIfThrows(() => FindInstallableFiles(module, filename, instance) - // Skip folders - .Where(f => !f.source.IsDirectory) - .Select(f => instance.ToRelativeGameDir(f.destination))) + .Where(instF => !filters.Any(filt => + instF.destination != null + && instF.destination.Contains(filt)))) : null) - ?? Enumerable.Empty(); + ?? Enumerable.Empty(); #endregion diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index db4398b96..c8e34ad6f 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -274,7 +274,7 @@ public static Dictionary> CheckUpgradeable(this IRegistry }; } - private static bool MetadataChanged(this IRegistryQuerier querier, string identifier) + public static bool MetadataChanged(this IRegistryQuerier querier, string identifier) { try { diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 61a3df718..80cb6207c 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -1291,7 +1291,7 @@ private void reinstallToolStripMenuItem_Click(object? sender, EventArgs? e) registry.GetModuleByVersion(module.identifier, module.version) ?? module, - true) + true, false) }, null); } } diff --git a/GUI/Controls/ModInfoTabs/Contents.Designer.cs b/GUI/Controls/ModInfoTabs/Contents.Designer.cs index 2b9edfa93..c7c79aa1e 100644 --- a/GUI/Controls/ModInfoTabs/Contents.Designer.cs +++ b/GUI/Controls/ModInfoTabs/Contents.Designer.cs @@ -85,6 +85,7 @@ private void InitializeComponent() this.ContentsPreviewTree.ImageList.Images.Add("file", global::CKAN.GUI.EmbeddedImages.file); this.ContentsPreviewTree.ShowPlusMinus = true; this.ContentsPreviewTree.ShowRootLines = false; + this.ContentsPreviewTree.ShowNodeToolTips = true; this.ContentsPreviewTree.Location = new System.Drawing.Point(3, 65); this.ContentsPreviewTree.Name = "ContentsPreviewTree"; this.ContentsPreviewTree.Size = new System.Drawing.Size(494, 431); diff --git a/GUI/Controls/ModInfoTabs/Contents.cs b/GUI/Controls/ModInfoTabs/Contents.cs index 59e787f79..61c29eb23 100644 --- a/GUI/Controls/ModInfoTabs/Contents.cs +++ b/GUI/Controls/ModInfoTabs/Contents.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.IO; @@ -9,6 +8,9 @@ using System.Runtime.Versioning; #endif +using Autofac; + +using CKAN.Configuration; using CKAN.GUI.Attributes; namespace CKAN.GUI @@ -53,9 +55,9 @@ public void RefreshModContentsTree() private void ContentsPreviewTree_NodeMouseDoubleClick(object? sender, TreeNodeMouseClickEventArgs? e) { - if (e != null) + if (e != null && manager?.CurrentInstance is GameInstance inst) { - Utilities.OpenFileBrowser(e.Node.Name); + Utilities.OpenFileBrowser(inst.ToAbsoluteGameDir(e.Node.Name)); } } @@ -119,9 +121,9 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) ContentsOpenButton.Enabled = false; ContentsPreviewTree.Enabled = false; } - else if (manager != null - && manager?.CurrentInstance != null + else if (manager?.CurrentInstance is GameInstance inst && manager?.Cache != null + && selectedModule != null && Main.Instance?.currentUser != null) { rootNode.Text = Path.GetFileName( @@ -134,23 +136,33 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) UseWaitCursor = true; Task.Factory.StartNew(() => { - var paths = ModuleInstaller.GetModuleContentsList(manager.Cache, manager.CurrentInstance, module) - // Load fully in bg - .ToArray(); + 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(); // Stop if user switched to another mod if (rootNode.TreeView != null) { Util.Invoke(this, () => { ContentsPreviewTree.BeginUpdate(); - foreach (string path in paths) + foreach ((string path, bool dir, bool exists) in tuples) { - AddContentPieces( - rootNode, - path.Split(new char[] {'/'})); + AddContentPieces(inst, rootNode, + path.Split(new char[] {'/'}), + dir, exists); } rootNode.ExpandAll(); - rootNode.EnsureVisible(); + var initialFocus = FirstMatching(rootNode, + n => n.ForeColor == Color.Red) + ?? rootNode; + initialFocus.EnsureVisible(); ContentsPreviewTree.EndUpdate(); UseWaitCursor = false; }); @@ -161,24 +173,45 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) } } - private static void AddContentPieces(TreeNode parent, IEnumerable pieces) + private static void AddContentPieces(GameInstance inst, + TreeNode parent, + string[] pieces, + bool dir, + bool exists) { var firstPiece = pieces.FirstOrDefault(); if (firstPiece != null) { - if (parent.ImageKey == "file") - { - parent.SelectedImageKey = parent.ImageKey = "folder"; - } // Key/Name needs to be the full relative path for double click to work var key = string.IsNullOrEmpty(parent.Name) - ? firstPiece - : $"{parent.Name}/{firstPiece}"; - var node = parent.Nodes[key] - ?? parent.Nodes.Add(key, firstPiece, "file", "file"); - AddContentPieces(node, pieces.Skip(1)); + ? firstPiece + : $"{parent.Name}/{firstPiece}"; + var node = parent.Nodes[key]; + if (node == null) + { + var iconKey = dir || pieces.Length > 1 ? "folder" : "file"; + node = parent.Nodes.Add(key, firstPiece, iconKey, iconKey); + if (!exists && (pieces.Length == 1 || !Directory.Exists(inst.ToAbsoluteGameDir(key)))) + { + node.ForeColor = Color.Red; + node.ToolTipText = iconKey == "folder" + ? Properties.Resources.ModInfoFolderNotFound + : Properties.Resources.ModInfoFileNotFound; + } + } + if (pieces.Length > 1) + { + AddContentPieces(inst, node, pieces.Skip(1).ToArray(), dir, exists); + } } } + private static TreeNode? FirstMatching(TreeNode root, Func predicate) + => predicate(root) ? root + : root.Nodes.OfType() + .Select(n => FirstMatching(n, predicate)) + .OfType() + .FirstOrDefault(); + } } diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index 947494855..8bae0914b 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -291,7 +291,9 @@ public CkanModule ToCkanModule() /// The CkanModule associated with this GUIMod or null if there is none public CkanModule ToModule() => Mod; - public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChecked) + public IEnumerable GetModChanges(bool upgradeChecked, + bool replaceChecked, + bool metadataChanged) { if (replaceChecked) { @@ -309,7 +311,7 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe yield return new ModUpgrade(Mod, GUIModChangeType.Update, SelectedMod, - false); + false, false); } else { @@ -329,7 +331,7 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe yield return new ModUpgrade(Mod, GUIModChangeType.Update, SelectedMod, - false); + false, metadataChanged); } } diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 9229e563f..412990824 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -138,11 +138,13 @@ public class ModUpgrade : ModChange public ModUpgrade(CkanModule mod, GUIModChangeType changeType, CkanModule targetMod, - bool userReinstall) + bool userReinstall, + bool metadataChanged) : base(mod, changeType) { - this.targetMod = targetMod; - this.userReinstall = userReinstall; + this.targetMod = targetMod; + this.userReinstall = userReinstall; + this.metadataChanged = metadataChanged; } public override string? NameAndStatus @@ -150,8 +152,9 @@ public override string? NameAndStatus public override string Description => IsReinstall - ? userReinstall ? Properties.Resources.MainChangesetUserReinstall - : Properties.Resources.MainChangesetReinstall + ? userReinstall ? Properties.Resources.MainChangesetReinstallUser + : metadataChanged ? Properties.Resources.MainChangesetReinstallMetadataChanged + : Properties.Resources.MainChangesetReinstallMissing : string.Format(Properties.Resources.MainChangesetUpdateSelected, targetMod.version); @@ -165,5 +168,6 @@ private bool IsReinstall && targetMod.version == Mod.version; private readonly bool userReinstall; + private readonly bool metadataChanged; } } diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index a2cf963fc..526582944 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -434,19 +434,21 @@ public string StripEpoch(string version) private static readonly Regex ContainsEpoch = new Regex(@"^[0-9][0-9]*:[^:]+$", RegexOptions.Compiled); private static readonly Regex RemoveEpoch = new Regex(@"^([^:]+):([^:]+)$", RegexOptions.Compiled); - private static IEnumerable rowChanges(DataGridViewRow row, - DataGridViewColumn? upgradeCol, - DataGridViewColumn? replaceCol) - => (row.Tag as GUIMod)?.GetModChanges( + private static IEnumerable rowChanges(IRegistryQuerier registry, + DataGridViewRow row, + DataGridViewColumn? upgradeCol, + DataGridViewColumn? replaceCol) + => row.Tag is GUIMod gmod ? gmod.GetModChanges( upgradeCol != null && upgradeCol.Visible && row.Cells[upgradeCol.Index] is DataGridViewCheckBoxCell upgradeCell && (bool)upgradeCell.Value, replaceCol != null && replaceCol.Visible && row.Cells[replaceCol.Index] is DataGridViewCheckBoxCell replaceCell - && (bool)replaceCell.Value) - ?? Enumerable.Empty(); + && (bool)replaceCell.Value, + registry.MetadataChanged(gmod.Identifier)) + : Enumerable.Empty(); - public HashSet ComputeUserChangeSet(IRegistryQuerier? registry, + public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVersionCriteria? crit, GameInstance? instance, DataGridViewColumn? upgradeCol, @@ -454,7 +456,7 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier? registry, { log.Debug("Computing user changeset"); var modChanges = full_list_of_mod_rows?.Values - .SelectMany(row => rowChanges(row, upgradeCol, replaceCol)) + .SelectMany(row => rowChanges(registry, row, upgradeCol, replaceCol)) .ToList() ?? new List(); diff --git a/GUI/Properties/Resources.de-DE.resx b/GUI/Properties/Resources.de-DE.resx index c431083ee..d088d2b4b 100644 --- a/GUI/Properties/Resources.de-DE.resx +++ b/GUI/Properties/Resources.de-DE.resx @@ -184,7 +184,7 @@ Möchten Sie es wirklich installieren? Installieren Abbrechen Mod-Update ausgewählt vom Nutzer {0}. - Neu installieren (Metadaten geändert) + Neu installieren (Metadaten geändert) Mod-Import Statuslog Statuslog diff --git a/GUI/Properties/Resources.fr-FR.resx b/GUI/Properties/Resources.fr-FR.resx index 4e6ff0785..04ec0f89d 100644 --- a/GUI/Properties/Resources.fr-FR.resx +++ b/GUI/Properties/Resources.fr-FR.resx @@ -359,10 +359,10 @@ Voulez-vous vraiment les installer ? L'annulation annulera toute l'installation. Mise à jour demandée vers la version {0}. - + Réinstaller (métadonnées modifiées) - + Réinstallation (à la demande de l'utilisateur) diff --git a/GUI/Properties/Resources.it-IT.resx b/GUI/Properties/Resources.it-IT.resx index bce145028..58437f9f7 100644 --- a/GUI/Properties/Resources.it-IT.resx +++ b/GUI/Properties/Resources.it-IT.resx @@ -323,7 +323,7 @@ Sei sicuro di volerli installare? L'annullamento interromperà l'intera installa Aggiorna la versione selezionata dall'utente alla versione {0}. - + Reinstallazione (Metadati cambiati) diff --git a/GUI/Properties/Resources.ja-JP.resx b/GUI/Properties/Resources.ja-JP.resx index 4bdafdb38..0c473a191 100644 --- a/GUI/Properties/Resources.ja-JP.resx +++ b/GUI/Properties/Resources.ja-JP.resx @@ -187,7 +187,7 @@ Try to move {2} out of {3} and restart CKAN. インストール 取消 選択されたものをバージョン{0}にアップデートする。 - () + () Modインポート Mod (*.zip)|*.zip ステータスログ diff --git a/GUI/Properties/Resources.ko-KR.resx b/GUI/Properties/Resources.ko-KR.resx index 93373400a..5f8df15d3 100644 --- a/GUI/Properties/Resources.ko-KR.resx +++ b/GUI/Properties/Resources.ko-KR.resx @@ -188,7 +188,7 @@ 설치 취소하기 유저가 선택한 것을 버전 {0}으로 업데이트 하기. - () + () 유저가 선택한 모드를 설치하기. 모드들 가져오기 모드들 (*.zip)|*.zip diff --git a/GUI/Properties/Resources.pl-PL.resx b/GUI/Properties/Resources.pl-PL.resx index 2db827349..07008f219 100644 --- a/GUI/Properties/Resources.pl-PL.resx +++ b/GUI/Properties/Resources.pl-PL.resx @@ -330,7 +330,7 @@ Na pewno chcesz je zainstalować? Anulowanie przerwie całą instalację. Aktualizacja wybrana przez użytkownika do wersji {0}. - + Zainstaluj ponownie (Metadane zmienione) diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 4e225eb1f..4a496cb43 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -215,8 +215,9 @@ Do you want to add the additional versions to this game instance's compatibility Are you sure you want to install them? Cancelling will abort the entire installation. Update selected by user to version {0}. - Re-install (metadata changed) - Re-install (user requested) + Re-install (metadata changed) + Re-install (missing folders or files) + Re-install (user requested) Import Mods Mods (*.zip)|*.zip Status log @@ -280,6 +281,8 @@ If you suspect a bug in the client: https://github.com/KSP-CKAN/CKAN/issues/new/ This mod is not in the cache, click 'Download' to preview contents Module is cached, preview available Module has no download + Folder not found! + File not found! Home page: SpaceDock: Curse: diff --git a/GUI/Properties/Resources.ru-RU.resx b/GUI/Properties/Resources.ru-RU.resx index 878aba48a..523b2e5d1 100644 --- a/GUI/Properties/Resources.ru-RU.resx +++ b/GUI/Properties/Resources.ru-RU.resx @@ -187,7 +187,7 @@ Установить Отмена Обновить выбранное пользователем до версии {0}. - Переустановка (Метаданные изменены) + Переустановка (Метаданные изменены) Импортировать модификации Модификации (*.zip)|*.zip Журнал diff --git a/GUI/Properties/Resources.zh-CN.resx b/GUI/Properties/Resources.zh-CN.resx index a0b627e71..738f49d3a 100644 --- a/GUI/Properties/Resources.zh-CN.resx +++ b/GUI/Properties/Resources.zh-CN.resx @@ -354,10 +354,10 @@ 用户选择更新到 {0} 版本. - + 重新安装 (元数据已更改) - + 重新安装 (用户请求) diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index e01cd8677..36b6a1e9b 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -27,7 +27,7 @@ public class ModListTests public void ComputeFullChangeSetFromUserChangeSet_WithEmptyList_HasEmptyChangeSet() { var item = new ModList(); - Assert.That(item.ComputeUserChangeSet(null, null, null, null, null), Is.Empty); + Assert.That(item.ComputeUserChangeSet(Registry.Empty(), null, null, null, null), Is.Empty); } [Test] @@ -211,7 +211,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() { // Install the "other" module installer.InstallList( - modList.ComputeUserChangeSet(null, null, null, null, null).Select(change => change.Mod).ToList(), + modList.ComputeUserChangeSet(Registry.Empty(), null, null, null, null).Select(change => change.Mod).ToList(), new RelationshipResolverOptions(), registryManager, ref possibleConfigOnlyDirs,