From f12593a33c233ceabcd20a1e281e082d8ca91857 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Wed, 2 Oct 2024 14:29:42 -0500 Subject: [PATCH 1/5] Remove incorrect suggestion about no-recommends flag --- Cmdline/Properties/Resources.fr-FR.resx | 1 - Cmdline/Properties/Resources.it-IT.resx | 1 - Cmdline/Properties/Resources.pl-PL.resx | 1 - Cmdline/Properties/Resources.resx | 7 ++++--- Cmdline/Properties/Resources.ru-RU.resx | 1 - 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cmdline/Properties/Resources.fr-FR.resx b/Cmdline/Properties/Resources.fr-FR.resx index 53cdb7ccc6..8b388e1cc2 100644 --- a/Cmdline/Properties/Resources.fr-FR.resx +++ b/Cmdline/Properties/Resources.fr-FR.resx @@ -390,7 +390,6 @@ Soyez sûr d'avoir saisi au moins les valeurs de version majeure et mineure au f Si vous êtes chanceux, vous pouvez taper `ckan update` et réessayer. -Essayez `ckan install --no-recommends` pour ignorer l'installation des modules recommandés. Ou `ckan install --allow-incompatible` pour ignorer la compatibilité des modules. diff --git a/Cmdline/Properties/Resources.it-IT.resx b/Cmdline/Properties/Resources.it-IT.resx index b80d5bdb77..3485a47532 100644 --- a/Cmdline/Properties/Resources.it-IT.resx +++ b/Cmdline/Properties/Resources.it-IT.resx @@ -380,7 +380,6 @@ Assicurati di inserire almeno i valori di versione maggiore e minore nella forma Se sei fortunato, puoi fare un `ckan update` e riprovare. -Prova `ckan install --no-recommends` per saltare l'installazione dei moduli consigliati. Oppure `ckan install --allow-incompatible` per ignorare la compatibilità dei moduli. diff --git a/Cmdline/Properties/Resources.pl-PL.resx b/Cmdline/Properties/Resources.pl-PL.resx index 9ef7ef4aa2..d4de7f3209 100644 --- a/Cmdline/Properties/Resources.pl-PL.resx +++ b/Cmdline/Properties/Resources.pl-PL.resx @@ -367,7 +367,6 @@ Upewnij się, że wprowadziłeś wersję w postaci Maj.Min - np. 1.5 Jeśli masz szczęście, możesz wykonać `ckan update` i spróbować ponownie. -Spróbuj `ckan install --no-recommends` aby pominąć instalację zalecanych modułów. Lub `ckan install --allow-niecompatible` aby zignorować kompatybilność modułów. diff --git a/Cmdline/Properties/Resources.resx b/Cmdline/Properties/Resources.resx index c740a7eb3a..8b32b47705 100644 --- a/Cmdline/Properties/Resources.resx +++ b/Cmdline/Properties/Resources.resx @@ -218,9 +218,10 @@ Make sure to enter at least the version major and minor values in the form Maj.M Import error: {0} File not found: {0} File not found, exiting: {0} - If you're lucky, you can do a `ckan update` and try again. -Try `ckan install --no-recommends` to skip installation of recommended modules. -Or `ckan install --allow-incompatible` to ignore module compatibility. + +You can use the `ckan compat` commands to change which game versions are treated as compatible. +If a newly compatible version was just released, you can do a `ckan update` and try again. +Or `ckan install --allow-incompatible` to ignore compatibility of the modules on the command line (but not dependencies). Module {0} required but it is not listed in the index, or not available for your version of {1} Module {0} {1} required but it is not listed in the index, or not available for your version of {2} Bad metadata detected for module {0}: {1} diff --git a/Cmdline/Properties/Resources.ru-RU.resx b/Cmdline/Properties/Resources.ru-RU.resx index 8197cf96bc..7c03e6d773 100644 --- a/Cmdline/Properties/Resources.ru-RU.resx +++ b/Cmdline/Properties/Resources.ru-RU.resx @@ -206,7 +206,6 @@ Файл не найден: {0} Файл не найден, выход: {0} Попробуйте совершить `ckan update` и попытайтесь заново. -Используйте `ckan install --no-recommends`, чтобы пропустить установку рекомендуемых модулей. Либо используйте `ckan install --allow-incompatible`, чтобы игнорировать проблемы совместимости. Необходим модуль {0}, но он отсутствует в указателе либо недоступен для вашей версии {1} Необходим модуль {0} {1}, но он отсутствует в указателе либо недоступен для вашей версии {2} From 3e75c24d1a53e344c0edce3e8bcfc9824fb9204f Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Wed, 2 Oct 2024 14:32:16 -0500 Subject: [PATCH 2/5] Fix Untagged search --- GUI/Model/ModSearch.cs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/GUI/Model/ModSearch.cs b/GUI/Model/ModSearch.cs index 38b0e3cc08..6cd81735d5 100644 --- a/GUI/Model/ModSearch.cs +++ b/GUI/Model/ModSearch.cs @@ -80,27 +80,17 @@ public ModSearch(GUIModFilter filter, { switch (filter) { - case GUIModFilter.Compatible: Compatible = true; break; - case GUIModFilter.Incompatible: Compatible = false; break; - case GUIModFilter.Installed: Installed = true; break; - case GUIModFilter.NotInstalled: Installed = false; break; - case GUIModFilter.InstalledUpdateAvailable: Upgradeable = true; break; - case GUIModFilter.Replaceable: Replaceable = true; break; - case GUIModFilter.Cached: Cached = true; break; - case GUIModFilter.Uncached: Cached = false; break; - case GUIModFilter.NewInRepository: NewlyCompatible = true; break; - case GUIModFilter.Tag: - if (tag?.Name is string tn) - { - TagNames.Add(tn); - } - break; - case GUIModFilter.CustomLabel: - if (label?.Name is string ln) - { - LabelNames.Add(ln); - } - break; + case GUIModFilter.Compatible: Compatible = true; break; + case GUIModFilter.Incompatible: Compatible = false; break; + case GUIModFilter.Installed: Installed = true; break; + case GUIModFilter.NotInstalled: Installed = false; break; + case GUIModFilter.InstalledUpdateAvailable: Upgradeable = true; break; + case GUIModFilter.Replaceable: Replaceable = true; break; + case GUIModFilter.Cached: Cached = true; break; + case GUIModFilter.Uncached: Cached = false; break; + case GUIModFilter.NewInRepository: NewlyCompatible = true; break; + case GUIModFilter.Tag: TagNames.Add(tag?.Name ?? ""); break; + case GUIModFilter.CustomLabel: LabelNames.Add(label?.Name ?? ""); break; default: case GUIModFilter.All: break; From cd7fbd2923b1be253ab5a0a62e18b609cede1765 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sun, 13 Oct 2024 18:11:09 -0500 Subject: [PATCH 3/5] Fix show in folder button in Unmanaged Files --- GUI/Controls/UnmanagedFiles.cs | 12 ++++++++---- GUI/Main/MainHistory.cs | 2 -- GUI/Main/MainUnmanaged.cs | 2 -- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/GUI/Controls/UnmanagedFiles.cs b/GUI/Controls/UnmanagedFiles.cs index 9ab3655c0d..2ddf08f5e7 100644 --- a/GUI/Controls/UnmanagedFiles.cs +++ b/GUI/Controls/UnmanagedFiles.cs @@ -104,7 +104,7 @@ private void ExpandDefaultModDir(IGame game) } } - private void AddContentPieces(TreeNode parent, IEnumerable pieces) + private static void AddContentPieces(TreeNode parent, IEnumerable pieces) { var firstPiece = pieces.FirstOrDefault(); if (firstPiece != null) @@ -116,7 +116,7 @@ private void AddContentPieces(TreeNode parent, IEnumerable pieces) // Key/Name needs to be the full relative path for double click to work var key = string.IsNullOrEmpty(parent.Name) ? firstPiece - : $"{parent.Name}/{firstPiece}"; + : Path.Combine(parent.Name, firstPiece); var node = parent.Nodes[key] ?? parent.Nodes.Add(key, firstPiece, "file", "file"); AddContentPieces(node, pieces.Skip(1)); @@ -175,8 +175,12 @@ private void ResetCollapseButton_Click(object? sender, EventArgs? e) private void ShowInFolderButton_Click(object? sender, EventArgs? e) { - Utilities.OpenFileBrowser(GameFolderTree.SelectedNode.Name); - GameFolderTree.Focus(); + if (GameFolderTree.SelectedNode is TreeNode node + && inst != null) + { + Utilities.OpenFileBrowser(inst.ToAbsoluteGameDir(node.Name)); + GameFolderTree.Focus(); + } } private void DeleteButton_Click(object? sender, EventArgs? e) diff --git a/GUI/Main/MainHistory.cs b/GUI/Main/MainHistory.cs index 7bcba73044..3f14c0cbc9 100644 --- a/GUI/Main/MainHistory.cs +++ b/GUI/Main/MainHistory.cs @@ -14,7 +14,6 @@ private void installationHistoryStripMenuItem_Click(object? sender, EventArgs? e { InstallationHistory.LoadHistory(CurrentInstance, configuration, repoData); tabController.ShowTab("InstallationHistoryTabPage", 2); - DisableMainWindow(); } } @@ -38,7 +37,6 @@ private void InstallationHistory_Done() UpdateStatusBar(); tabController.ShowTab("ManageModsTabPage"); tabController.HideTab("InstallationHistoryTabPage"); - EnableMainWindow(); } private void InstallationHistory_OnSelectedModuleChanged(CkanModule m) diff --git a/GUI/Main/MainUnmanaged.cs b/GUI/Main/MainUnmanaged.cs index 18527267fd..285a0e15ee 100644 --- a/GUI/Main/MainUnmanaged.cs +++ b/GUI/Main/MainUnmanaged.cs @@ -13,7 +13,6 @@ private void viewUnmanagedFilesStripMenuItem_Click(object? sender, EventArgs? e) { UnmanagedFiles.LoadFiles(Manager.CurrentInstance, repoData, currentUser); tabController.ShowTab("UnmanagedFilesTabPage", 2); - DisableMainWindow(); } } @@ -22,7 +21,6 @@ private void UnmanagedFiles_Done() UpdateStatusBar(); tabController.ShowTab("ManageModsTabPage"); tabController.HideTab("UnmanagedFilesTabPage"); - EnableMainWindow(); } } } From 589e2ddee0e86300176125adf8c14209a2d3989b Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Wed, 2 Oct 2024 15:18:58 -0500 Subject: [PATCH 4/5] Refactor registry algorithms and parameter nullability --- ConsoleUI/DependencyScreen.cs | 13 +- Core/ModuleInstaller.cs | 6 +- Core/Registry/IRegistryQuerier.cs | 4 +- Core/Registry/Registry.cs | 160 ++++++------------ Core/Relationships/RelationshipResolver.cs | 6 +- GUI/Model/ModList.cs | 2 +- .../Relationships/RelationshipResolver.cs | 79 ++++----- Tests/GUI/Model/ModList.cs | 7 +- 8 files changed, 110 insertions(+), 167 deletions(-) diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index a0594e015c..e8d3b63f6a 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -155,12 +155,13 @@ public DependencyScreen(ConsoleTheme theme, private void generateList(HashSet inst) { - if (ModuleInstaller.FindRecommendations( - manager.CurrentInstance, - inst, new List(inst), registry, - out Dictionary>> recommendations, - out Dictionary> suggestions, - out Dictionary> supporters + if (manager.CurrentInstance is GameInstance instance + && ModuleInstaller.FindRecommendations( + instance, + inst, new List(inst), registry, + out Dictionary>> recommendations, + out Dictionary> suggestions, + out Dictionary> supporters )) { foreach ((CkanModule mod, Tuple> checkedAndDependents) in recommendations) { dependencies.Add(mod, new Dependency( diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 6b99ea9aa3..f48fc99e47 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -1433,7 +1433,7 @@ public static IEnumerable PrioritizedHosts(IEnumerable? urls) /// /// true if anything found, false otherwise /// - public static bool FindRecommendations(GameInstance? instance, + public static bool FindRecommendations(GameInstance instance, HashSet sourceModules, List toInstall, Registry registry, @@ -1441,7 +1441,7 @@ public static bool FindRecommendations(GameInstance? out Dictionary> suggestions, out Dictionary> supporters) { - var crit = instance?.VersionCriteria(); + var crit = instance.VersionCriteria(); var resolver = new RelationshipResolver(sourceModules.Where(m => !m.IsDLC), null, RelationshipResolverOptions.KitchenSinkOpts(), @@ -1507,7 +1507,7 @@ public static bool FindRecommendations(GameInstance? public static bool CanInstall(List toInstall, RelationshipResolverOptions opts, IRegistryQuerier registry, - GameVersionCriteria? crit) + GameVersionCriteria crit) { string request = string.Join(", ", toInstall.Select(m => m.identifier)); try diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 7e31111d65..7e9a759193 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -432,7 +432,7 @@ private static IEnumerable FindRemovableAutoInstalled( List installedModules, HashSet dlls, IDictionary dlc, - GameVersionCriteria? crit) + GameVersionCriteria crit) { log.DebugFormat("Finding removable autoInstalled for: {0}", string.Join(", ", installedModules.Select(im => im.identifier))); @@ -474,7 +474,7 @@ private static IEnumerable FindRemovableAutoInstalled( public static IEnumerable FindRemovableAutoInstalled( this IRegistryQuerier querier, List installedModules, - GameVersionCriteria? crit) + GameVersionCriteria crit) => querier?.FindRemovableAutoInstalled(installedModules, querier.InstalledDlls.ToHashSet(), querier.InstalledDlc, diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index dbfe47aca9..0e538ab55d 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -556,7 +556,7 @@ private void InvalidateInstalledCaches() private void RepositoriesUpdated(Repository[] which) { - if (Repositories?.Values.Any(r => which.Contains(r)) ?? false) + if (Repositories.Values.Any(r => which.Contains(r))) { // One of our repos changed, old cached data is now junk EnlistWithTransaction(); @@ -583,7 +583,7 @@ public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) } sorter = new CompatibilitySorter( versCrit, - repoDataMgr?.GetAllAvailDicts(repositories?.Values.OrderBy(r => r.priority) + repoDataMgr?.GetAllAvailDicts(Repositories.Values.OrderBy(r => r.priority) // Break ties alphanumerically .ThenBy(r => r.name)) ?? Enumerable.Empty>(), @@ -682,7 +682,7 @@ public string GetAvailableMetadata(string identifier) => repoDataMgr == null ? "" : string.Join("", - repoDataMgr.GetAvailableModules(repositories?.Values, identifier) + repoDataMgr.GetAvailableModules(Repositories.Values, identifier) .Select(am => am.FullMetadata())); /// @@ -699,39 +699,17 @@ public string GetAvailableMetadata(string identifier) /// Generate the providers index so we can find providing modules quicker /// [MemberNotNull(nameof(providers))] - private void BuildProvidesIndex() - { - providers = new Dictionary>(); - if (repoDataMgr != null) - { - foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories?.Values)) - { - BuildProvidesIndexFor(am); - } - } - } - - /// - /// Ensure one AvailableModule is present in the right spots in the providers index - /// - private void BuildProvidesIndexFor(AvailableModule am) - { - providers ??= new Dictionary>(); - foreach (CkanModule m in am.AllAvailable()) - { - foreach (string provided in m.ProvidesList) - { - if (providers.TryGetValue(provided, out HashSet? provs)) - { - provs.Add(am); - } - else - { - providers.Add(provided, new HashSet() { am }); - } - } - } - } + private Dictionary> BuildProvidesIndex() + => providers = (repoDataMgr?.GetAllAvailableModules(Repositories.Values) + ?? Enumerable.Empty()) + .SelectMany(am => am.AllAvailable() + .SelectMany(m => m.ProvidesList) + .Distinct() + .Select(provided => (provided, am))) + .GroupBy(tuple => tuple.provided, + tuple => tuple.am) + .ToDictionary(grp => grp.Key, + grp => grp.ToHashSet()); [JsonIgnore] public Dictionary Tags @@ -773,83 +751,43 @@ public HashSet Untagged [MemberNotNull(nameof(tags), nameof(untagged))] private void BuildTagIndex() { - tags = new Dictionary(); - untagged = new HashSet(); - if (repoDataMgr != null) - { - foreach (AvailableModule am in repoDataMgr.GetAllAvailableModules(repositories?.Values)) - { - BuildTagIndexFor(am); - } - } - } - - private void BuildTagIndexFor(AvailableModule am) - { - bool tagged = false; - foreach (CkanModule m in am.AllAvailable()) - { - if (m.Tags != null) - { - tags ??= new Dictionary(); - tagged = true; - foreach (string tagName in m.Tags) - { - if (tags.TryGetValue(tagName, out ModuleTag? tag)) - { - tag.Add(m.identifier); - } - else - { - tags.Add(tagName, new ModuleTag(tagName) - { - ModuleIdentifiers = new HashSet() { m.identifier }, - }); - } - } - } - } - if (!tagged) - { - untagged ??= new HashSet(); - untagged.Add(am.AllAvailable().First().identifier); - } + tags = (repoDataMgr?.GetAllAvailableModules(Repositories.Values) + ?? Enumerable.Empty()) + .SelectMany(am => am.AllAvailable() + .SelectMany(m => m.Tags ?? Enumerable.Empty()) + .Select(tag => (tag, ident: am.AllAvailable().First().identifier)) + .DefaultIfEmpty((tag: "", ident: am.AllAvailable().First().identifier))) + .GroupBy(tuple => tuple.tag, + tuple => tuple.ident) + .ToDictionary(grp => grp.Key, + grp => new ModuleTag(grp.Key) { ModuleIdentifiers = grp.ToHashSet() }); + untagged = tags.TryGetValue("", out ModuleTag? t) ? t.ModuleIdentifiers + : new HashSet(); + tags.Remove(""); } /// /// /// - public List LatestAvailableWithProvides( - string identifier, - GameVersionCriteria? gameVersion, - RelationshipDescriptor? relationship_descriptor = null, - ICollection? installed = null, - ICollection? toInstall = null) - { - if (providers == null) - { - BuildProvidesIndex(); - } - if (providers.TryGetValue(identifier, out HashSet? provs)) - { - // For each AvailableModule, we want the latest one matching our constraints - return provs - .Select(am => am.Latest(gameVersion, relationship_descriptor, - installed, toInstall)) - .OfType() - .Where(m => m?.ProvidesList?.Contains(identifier) ?? false) - // Put the most popular one on top - .OrderByDescending(m => repoDataMgr?.GetDownloadCount(Repositories?.Values, - m.identifier) - ?? 0) - .ToList(); - } - else - { - // Nothing provides this, return empty list - return new List(); - } - } + public List LatestAvailableWithProvides(string identifier, + GameVersionCriteria? gameVersion, + RelationshipDescriptor? relationship = null, + ICollection? installed = null, + ICollection? toInstall = null) + => ((providers ?? BuildProvidesIndex()) + is Dictionary> allProvs + && Repositories.Values.ToArray() is Repository[] repos + && allProvs.TryGetValue(identifier, out HashSet? provs) + // For each AvailableModule, we want the latest one matching our constraints + ? provs.Select(am => am.Latest(gameVersion, relationship, installed, toInstall)) + .OfType() + .Where(m => m.ProvidesList.Contains(identifier)) + // Put the most popular one on top + .OrderByDescending(m => repoDataMgr?.GetDownloadCount(repos, m.identifier) + ?? 0) + // Nothing provides this + : Enumerable.Empty()) + .ToList(); #endregion @@ -1250,7 +1188,7 @@ public IEnumerable FindReverseDependencies( /// public Dictionary> GetDownloadHashesIndex() => downloadHashesIndex ??= - (repoDataMgr?.GetAllAvailableModules(repositories?.Values) + (repoDataMgr?.GetAllAvailableModules(Repositories.Values) .SelectMany(availMod => availMod.module_version.Values) ?? Enumerable.Empty()) .SelectMany(ModWithDownloadHashes) @@ -1283,7 +1221,7 @@ private IEnumerable> ModWithDownloadHashes(CkanModule /// public Dictionary> GetDownloadUrlHashIndex() => downloadUrlHashIndex ??= - (repoDataMgr?.GetAllAvailableModules(repositories?.Values) + (repoDataMgr?.GetAllAvailableModules(Repositories.Values) ?? Enumerable.Empty()) .SelectMany(am => am.module_version.Values) .SelectMany(m => m.download?.Select(url => new Tuple(url, m)) @@ -1299,7 +1237,7 @@ public Dictionary> GetDownloadUrlHashIndex() /// /// Host strings without duplicates public IEnumerable GetAllHosts() - => repoDataMgr?.GetAllAvailableModules(repositories?.Values) + => repoDataMgr?.GetAllAvailableModules(Repositories.Values) // Pick all latest modules where download is not null // Merge all the URLs into one sequence .SelectMany(availMod => (availMod?.Latest()?.download diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index a79d954896..c78275d9ae 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -31,7 +31,7 @@ public RelationshipResolver(IEnumerable modulesToInstall, IEnumerable? modulesToRemove, RelationshipResolverOptions options, IRegistryQuerier registry, - GameVersionCriteria? versionCrit) + GameVersionCriteria versionCrit) : this(options, registry, versionCrit) { if (modulesToRemove != null) @@ -52,7 +52,7 @@ public RelationshipResolver(IEnumerable modulesToInstall, /// The current KSP version criteria to consider private RelationshipResolver(RelationshipResolverOptions options, IRegistryQuerier registry, - GameVersionCriteria? versionCrit) + GameVersionCriteria versionCrit) { this.options = options; this.registry = registry; @@ -698,7 +698,7 @@ private void AddReason(CkanModule module, SelectionReason reason) private readonly HashSet suppressedRecommenders = new HashSet(); private readonly IRegistryQuerier registry; - private readonly GameVersionCriteria? versionCrit; + private readonly GameVersionCriteria versionCrit; private readonly RelationshipResolverOptions options; /// diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 2a12b8bc7f..2a67cdee0c 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -451,7 +451,7 @@ private static IEnumerable rowChanges(IRegistryQuerier registry, : Enumerable.Empty(); public HashSet ComputeUserChangeSet(IRegistryQuerier registry, - GameVersionCriteria? crit, + GameVersionCriteria crit, GameInstance? instance, DataGridViewColumn? upgradeCol, DataGridViewColumn? replaceCol) diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolver.cs index cc882606f1..3d59339d7c 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolver.cs @@ -18,6 +18,7 @@ public class RelationshipResolverTests { private RelationshipResolverOptions? options; private RandomModuleGenerator? generator; + private static readonly GameVersionCriteria crit = new GameVersionCriteria(null); [SetUp] public void Setup() @@ -34,7 +35,7 @@ public void Constructor_WithoutModules_AlwaysReturns() var registry = CKAN.Registry.Empty(); options = RelationshipResolverOptions.DefaultOpts(); Assert.DoesNotThrow(() => new RelationshipResolver(new List(), - null, options, registry, null)); + null, options, registry, crit)); } [Test] @@ -55,10 +56,10 @@ public void Constructor_WithConflictingModules() var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); options!.proceed_with_inconsistencies = true; - var resolver = new RelationshipResolver(list, null, options, registry, null); + var resolver = new RelationshipResolver(list, null, options, registry, crit); Assert.That(resolver.ConflictList.Any(s => Equals(s.Key, mod_a))); Assert.That(resolver.ConflictList.Any(s => Equals(s.Key, mod_b))); @@ -85,7 +86,7 @@ public void Constructor_WithConflictingModulesVersion_Throws() var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -110,7 +111,7 @@ public void Constructor_WithConflictingModulesVersionMin_Throws(string ver, stri var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -135,7 +136,7 @@ public void Constructor_WithConflictingModulesVersionMax_Throws(string ver, stri var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -166,7 +167,7 @@ public void Constructor_WithConflictingModulesVersionMinMax_Throws(string ver, s var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -191,7 +192,7 @@ public void Constructor_WithNonConflictingModulesVersion_DoesNotThrow(string ver var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -215,7 +216,7 @@ public void Constructor_WithConflictingModulesVersionMin_DoesNotThrow(string ver var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -239,7 +240,7 @@ public void Constructor_WithConflictingModulesVersionMax_DoesNotThrow(string ver var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -264,7 +265,7 @@ public void Constructor_WithConflictingModulesVersionMinMax_DoesNotThrow(string var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -297,7 +298,7 @@ public void Constructor_WithMultipleModulesProviding_Throws() var list = new List { mod_d }; Assert.Throws(() => new RelationshipResolver( - list, null, options, registry, null)); + list, null, options, registry, crit)); } } @@ -316,7 +317,7 @@ public void ModList_WithInstalledModules_ContainsThemWithReasonInstalled() registry.RegisterModule(mod_a, new List(), ksp.KSP, false); var relationship_resolver = new RelationshipResolver( - list, null, options!, registry, null); + list, null, options!, registry, crit); CollectionAssert.Contains(relationship_resolver.ModList(), mod_a); CollectionAssert.AreEquivalent(new List { @@ -346,7 +347,7 @@ public void ModList_WithInstalledModulesSuggested_DoesNotContainThem() registry.Installed().Add(suggested.identifier, suggested.version); var list = new List { suggester }; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); CollectionAssert.Contains(relationship_resolver.ModList(), suggested); } } @@ -374,7 +375,7 @@ public void ModList_WithSuggestedModulesThatWouldConflict_DoesNotContainThem() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { suggester, mod }; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); CollectionAssert.DoesNotContain(relationship_resolver.ModList(), suggested); } } @@ -402,7 +403,7 @@ public void Constructor_WithConflictingModulesInDependencies_ThrowUnderDefaultSe var list = new List { depender, conflicts_with_dependant }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -424,7 +425,7 @@ public void Constructor_WithSuggests_HasSuggestedInModlist() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { suggester }; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); CollectionAssert.Contains(relationship_resolver.ModList(), suggested); } } @@ -456,12 +457,12 @@ public void Constructor_ContainsSugestedOfSuggested_When_With_all_suggests() var list = new List { suggester }; options!.with_all_suggests = true; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); CollectionAssert.Contains(relationship_resolver.ModList(), suggested2); options.with_all_suggests = false; - relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); CollectionAssert.DoesNotContain(relationship_resolver.ModList(), suggested2); } } @@ -486,7 +487,7 @@ public void Constructor_ProvidesSatisfyDependencies() { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { @@ -513,7 +514,7 @@ public void Constructor_WithMissingDependants_Throws() Assert.Throws(() => new RelationshipResolver(new List { depender }, - null, options!, registry, null)); + null, options!, registry, crit)); } } @@ -541,7 +542,7 @@ public void Constructor_WithMissingDependantsVersion_Throws(string ver, string d var list = new List { depender }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -565,10 +566,10 @@ public void Constructor_WithMissingDependantsVersionMin_Throws(string ver, strin var list = new List() { depender }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); list.Add(dependant); Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -592,7 +593,7 @@ public void Constructor_WithMissingDependantsVersionMax_Throws(string ver, strin var list = new List { depender, dependant }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -622,7 +623,7 @@ public void Constructor_WithMissingDependantsVersionMinMax_Throws(string ver, st var list = new List { depender, dependant }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, null)); + list, null, options!, registry, crit)); } } @@ -649,7 +650,7 @@ public void Constructor_WithDependantVersion_ChooseCorrectly(string ver, string var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { dependant, @@ -682,7 +683,7 @@ public void Constructor_WithDependantVersionMin_ChooseCorrectly(string ver, stri var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { dependant, @@ -715,7 +716,7 @@ public void Constructor_WithDependantVersionMax_ChooseCorrectly(string ver, stri var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { dependant, @@ -753,7 +754,7 @@ public void Constructor_WithDependantVersionMinMax_ChooseCorrectly(string ver, s var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { dependant, @@ -810,7 +811,7 @@ public void Constructor_ReverseDependencyDoesntMatchLatest_ChoosesOlderVersion() // Act RelationshipResolver rr = new RelationshipResolver( new CkanModule[] { depender }, null, - options!, registry, null); + options!, registry, crit); // Assert CollectionAssert.Contains( rr.ModList(), olderDependency); @@ -861,7 +862,7 @@ public void Constructor_ReverseDependencyConflictsLatest_ChoosesOlderVersion() // Act RelationshipResolver rr = new RelationshipResolver( new CkanModule[] { depender }, null, - options!, registry, null); + options!, registry, crit); // Assert CollectionAssert.Contains( rr.ModList(), olderDependency); @@ -880,7 +881,7 @@ public void ReasonFor_WithModsNotInList_Empty() { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { mod }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); var mod_not_in_resolver_list = generator.GeneratorRandomModule(); CollectionAssert.DoesNotContain(relationship_resolver.ModList(), mod_not_in_resolver_list); @@ -900,7 +901,7 @@ public void ReasonFor_WithUserAddedMods_GivesReasonUserAdded() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { mod }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); var reasons = relationship_resolver.ReasonsFor(mod); Assert.That(reasons[0], Is.AssignableTo()); } @@ -923,7 +924,7 @@ public void ReasonFor_WithSuggestedMods_GivesCorrectParent() var list = new List { mod }; options!.with_all_suggests = true; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); var reasons = relationship_resolver.ReasonsFor(suggested); Assert.IsTrue(reasons[0] is SelectionReason.Suggested sug @@ -961,7 +962,7 @@ public void ReasonFor_WithTreeOfMods_GivesCorrectParents() options!.with_all_suggests = true; options.with_recommends = true; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, null); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); var reasons = relationship_resolver.ReasonsFor(recommendedA); Assert.IsTrue(reasons[0] is SelectionReason.Recommended rec && rec.Parent.Equals(suggested)); @@ -1066,7 +1067,7 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv options!.proceed_with_inconsistencies = false; var exception = Assert.Throws(() => { - resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, null); + resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, crit); }); Assert.AreEqual($"{avp} conflicts with {eveDefaultConfig}", exception?.ShortDescription); @@ -1077,7 +1078,7 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv options.proceed_with_inconsistencies = true; Assert.DoesNotThrow(() => { - resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, null); + resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, crit); }); CollectionAssert.AreEquivalent(modulesToInstall, resolver?.ConflictList.Keys); @@ -1093,7 +1094,7 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv options.proceed_with_inconsistencies = false; Assert.DoesNotThrow(() => { - resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, null); + resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, crit); }); Assert.IsEmpty(resolver!.ConflictList); CollectionAssert.AreEquivalent(new List {avp, avp2kTextures}, resolver.ModList()); diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index 36b6a1e9bf..d15c250bdf 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -13,6 +13,7 @@ using Tests.Data; using CKAN; +using CKAN.Versioning; using CKAN.GUI; namespace Tests.GUI @@ -23,11 +24,13 @@ namespace Tests.GUI [TestFixture] public class ModListTests { + private static readonly GameVersionCriteria crit = new GameVersionCriteria(null); + [Test] public void ComputeFullChangeSetFromUserChangeSet_WithEmptyList_HasEmptyChangeSet() { var item = new ModList(); - Assert.That(item.ComputeUserChangeSet(Registry.Empty(), null, null, null, null), Is.Empty); + Assert.That(item.ComputeUserChangeSet(Registry.Empty(), crit, null, null, null), Is.Empty); } [Test] @@ -211,7 +214,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() { // Install the "other" module installer.InstallList( - modList.ComputeUserChangeSet(Registry.Empty(), null, null, null, null).Select(change => change.Mod).ToList(), + modList.ComputeUserChangeSet(Registry.Empty(), crit, null, null, null).Select(change => change.Mod).ToList(), new RelationshipResolverOptions(), registryManager, ref possibleConfigOnlyDirs, From 7fb97085ea3c217c19606722ec93ae6b7c6b8279 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Fri, 4 Oct 2024 10:54:12 -0500 Subject: [PATCH 5/5] Refactor relationship resolver to capture full resolved tree --- Cmdline/Action/Install.cs | 2 +- Cmdline/Action/Replace.cs | 5 +- ConsoleUI/DependencyScreen.cs | 3 +- ConsoleUI/InstallScreen.cs | 4 +- Core/CKAN-core.csproj | 1 + Core/Extensions/EnumerableExtensions.cs | 14 + Core/ModuleInstaller.cs | 20 +- Core/Properties/Resources.resx | 3 +- Core/Registry/CompatibilitySorter.cs | 6 +- Core/Registry/IRegistryQuerier.cs | 42 +- Core/Registry/Registry.cs | 44 +- Core/Relationships/RelationshipResolver.cs | 490 +++-------------- .../RelationshipResolverOptions.cs | 141 +++++ Core/Relationships/ResolvedRelationship.cs | 195 +++++++ .../ResolvedRelationshipsTree.cs | 165 ++++++ Core/Relationships/SanityChecker.cs | 26 +- Core/Relationships/SelectionReason.cs | 154 ++++++ Core/Repositories/AvailableModule.cs | 12 +- Core/Types/CkanModule.cs | 4 +- Core/Types/Kraken.cs | 75 ++- Core/Types/RelationshipDescriptor.cs | 62 ++- GUI/Controls/ChooseRecommendedMods.cs | 19 +- GUI/Controls/ManageMods.cs | 20 +- GUI/Controls/ModInfoTabs/Relationships.cs | 2 +- GUI/Controls/ModInfoTabs/Versions.cs | 5 +- GUI/Main/MainHistory.cs | 1 + GUI/Main/MainInstall.cs | 6 +- GUI/Model/ModList.cs | 15 +- Tests/Core/Registry/CompatibilitySorter.cs | 2 +- ...solver.cs => RelationshipResolverTests.cs} | 503 ++++++++++++++---- Tests/Core/Relationships/SanityChecker.cs | 20 +- Tests/GUI/Model/ModList.cs | 10 +- 32 files changed, 1400 insertions(+), 671 deletions(-) create mode 100644 Core/Relationships/RelationshipResolverOptions.cs create mode 100644 Core/Relationships/ResolvedRelationship.cs create mode 100644 Core/Relationships/ResolvedRelationshipsTree.cs create mode 100644 Core/Relationships/SelectionReason.cs rename Tests/Core/Relationships/{RelationshipResolver.cs => RelationshipResolverTests.cs} (69%) diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index 142222dc26..4297f7d1ae 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -123,7 +123,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) user.RaiseMessage(""); done = true; } - catch (DependencyNotSatisfiedKraken ex) + catch (DependenciesNotSatisfiedKraken ex) { user.RaiseError("{0}", ex.Message); user.RaiseMessage(Properties.Resources.InstallTryAgain); diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs index 314b395edd..c1597b9687 100644 --- a/Cmdline/Action/Replace.cs +++ b/Cmdline/Action/Replace.cs @@ -163,10 +163,9 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) regMgr); user.RaiseMessage(""); } - catch (DependencyNotSatisfiedKraken ex) + catch (DependenciesNotSatisfiedKraken ex) { - user.RaiseMessage(Properties.Resources.ReplaceDependencyNotSatisfied, - ex.parent, ex.module, ex.version ?? "", instance.game.ShortName); + user.RaiseMessage("{0}", ex.Message); } } else diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index e8d3b63f6a..10b89f0380 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -216,11 +216,12 @@ private bool HasConflicts(IEnumerable toAdd, plan.Remove.Select(ident => registry.InstalledModule(ident)?.Module) .OfType(), RelationshipResolverOptions.ConflictsOpts(), registry, + manager.CurrentInstance.game, manager.CurrentInstance.VersionCriteria()); descriptions = resolver.ConflictDescriptions.ToList(); return descriptions.Count > 0; } - catch (DependencyNotSatisfiedKraken k) + catch (DependenciesNotSatisfiedKraken k) { descriptions = new List() { k.Message }; return true; diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index 014f28521d..f879f5ba74 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -167,8 +167,8 @@ public override void Run(Action? process = null) } catch (BadMetadataKraken ex) { RaiseError(Properties.Resources.InstallBadMetadata, ex.module?.ToString() ?? "", ex.Message); - } catch (DependencyNotSatisfiedKraken ex) { - RaiseError(Properties.Resources.InstallUnsatisfiedDependency, ex.parent, ex.module, ex.Message); + } catch (DependenciesNotSatisfiedKraken ex) { + RaiseError("{0}", ex.Message); } catch (ModuleNotFoundKraken ex) { RaiseError(Properties.Resources.InstallModuleNotFound, ex.module, ex.Message); } catch (ModNotInstalledKraken ex) { diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj index 1b706592a0..ff516cd54d 100644 --- a/Core/CKAN-core.csproj +++ b/Core/CKAN-core.csproj @@ -60,6 +60,7 @@ + diff --git a/Core/Extensions/EnumerableExtensions.cs b/Core/Extensions/EnumerableExtensions.cs index f5da93761a..78f3ed8442 100644 --- a/Core/Extensions/EnumerableExtensions.cs +++ b/Core/Extensions/EnumerableExtensions.cs @@ -279,6 +279,20 @@ public static IEnumerable WithMatches(this IEnumerable source, Re => source.Select(item => Utilities.DefaultIfThrows(() => func(item), exc => onThrow(item, exc))); + /// + /// Get a hash code for a sequence with a variable number of elements + /// + /// Type of the elements in the sequence + /// The sequence + /// + public static int ToSequenceHashCode(this IEnumerable source) + => source.Aggregate(new HashCode(), + (hc, item) => + { + hc.Add(item); + return hc; + }, + hc => hc.ToHashCode()); } /// diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index f48fc99e47..4f4cd2a297 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -146,6 +146,7 @@ public void InstallList(ICollection modules, } var resolver = new RelationshipResolver(modules, null, options, registry_manager.registry, + instance.game, instance.VersionCriteria()); var modsToInstall = resolver.ModList().ToList(); var downloads = new List(); @@ -774,6 +775,7 @@ public void UninstallList(IEnumerable mods, .Where(im => !revdep.Contains(im.identifier)) .Concat(installing?.Select(m => new InstalledModule(null, m, Array.Empty(), false)) ?? Array.Empty()) .ToList(), + instance.game, instance.VersionCriteria()) .Select(im => im.identifier)) .ToList(); @@ -1185,6 +1187,7 @@ public void Upgrade(ICollection modules, .OfType(), RelationshipResolverOptions.DependsOnlyOpts(), registry, + instance.game, instance.VersionCriteria()); modules = resolver.ModList().ToArray(); autoInstalled = modules.ToDictionary(m => m, resolver.IsAutoInstalled); @@ -1285,6 +1288,7 @@ public void Upgrade(ICollection modules, .Where(im => !removingIdents.Contains(im.identifier)) .Concat(modules.Select(m => new InstalledModule(null, m, Array.Empty(), false))) .ToList(), + instance.game, instance.VersionCriteria()) .ToList(); if (autoRemoving.Count > 0) @@ -1318,7 +1322,7 @@ public void Upgrade(ICollection modules, /// Enacts listed Module Replacements to the specified versions for the user's KSP. /// Will *re-install* or *downgrade* (with a warning) as well as upgrade. /// - /// Thrown if a dependency for a replacing module couldn't be satisfied. + /// Thrown if a dependency for a replacing module couldn't be satisfied. /// Thrown if a module that should be replaced is not installed. public void Replace(IEnumerable replacements, RelationshipResolverOptions options, @@ -1401,7 +1405,8 @@ public void Replace(IEnumerable replacements, } } } - var resolver = new RelationshipResolver(modsToInstall, null, options, registry_manager.registry, instance.VersionCriteria()); + var resolver = new RelationshipResolver(modsToInstall, null, options, registry_manager.registry, + instance.game, instance.VersionCriteria()); var resolvedModsToInstall = resolver.ModList().ToList(); AddRemove(ref possibleConfigOnlyDirs, registry_manager, @@ -1445,7 +1450,7 @@ public static bool FindRecommendations(GameInstance var resolver = new RelationshipResolver(sourceModules.Where(m => !m.IsDLC), null, RelationshipResolverOptions.KitchenSinkOpts(), - registry, crit); + registry, instance.game, crit); var recommenders = resolver.Dependencies().ToHashSet(); var checkedRecs = resolver.Recommendations(recommenders) @@ -1454,7 +1459,7 @@ public static bool FindRecommendations(GameInstance .ToHashSet(); var conflicting = new RelationshipResolver(toInstall.Concat(checkedRecs), null, RelationshipResolverOptions.ConflictsOpts(), - registry, crit) + registry, instance.game, crit) .ConflictList.Keys; // Don't check recommendations that conflict with installed or installing mods checkedRecs.ExceptWith(conflicting); @@ -1486,7 +1491,7 @@ public static bool FindRecommendations(GameInstance toInstall.Concat(recommendations.Keys) .Concat(suggestions.Keys)) .Where(kvp => CanInstall(toInstall.Append(kvp.Key).ToList(), - opts, registry, crit)) + opts, registry, instance.game, crit)) .ToDictionary(); return recommendations.Count > 0 @@ -1507,6 +1512,7 @@ public static bool FindRecommendations(GameInstance public static bool CanInstall(List toInstall, RelationshipResolverOptions opts, IRegistryQuerier registry, + IGame game, GameVersionCriteria crit) { string request = string.Join(", ", toInstall.Select(m => m.identifier)); @@ -1514,7 +1520,7 @@ public static bool CanInstall(List toInstall, { var installed = toInstall.Select(m => registry.InstalledModule(m.identifier)?.Module) .OfType(); - var resolver = new RelationshipResolver(toInstall, installed, opts, registry, crit); + var resolver = new RelationshipResolver(toInstall, installed, opts, registry, game, crit); var resolverModList = resolver.ModList(false).ToList(); if (resolverModList.Count >= toInstall.Count(m => !m.IsMetapackage)) @@ -1536,7 +1542,7 @@ public static bool CanInstall(List toInstall, foreach (var mod in k.modules) { // Try each option recursively to see if any are successful - if (CanInstall(toInstall.Append(mod).ToList(), opts, registry, crit)) + if (CanInstall(toInstall.Append(mod).ToList(), opts, registry, game, crit)) { // Child call will emit debug output, so we don't need to here return true; diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index 244d9eb245..cb1c435912 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -275,7 +275,8 @@ Which {0} provider would you like to install? {1} If the game is still running, close it and try again. Otherwise check the permissions. - {0} missing dependency {1} + Unsatisfied dependency {1} needed for: {0} + needed for {0} {0} conflicts with {1} Uh oh, the following things went wrong when downloading... One or more downloads were unsuccessful: diff --git a/Core/Registry/CompatibilitySorter.cs b/Core/Registry/CompatibilitySorter.cs index 35b6b28419..d779cfad36 100644 --- a/Core/Registry/CompatibilitySorter.cs +++ b/Core/Registry/CompatibilitySorter.cs @@ -28,7 +28,7 @@ public CompatibilitySorter(GameVersionCriteria crit IEnumerable> available, Dictionary> providers, Dictionary installed, - HashSet dlls, + ICollection dlls, IDictionary dlc) { CompatibleVersions = crit; @@ -104,8 +104,8 @@ public ICollection LatestIncompatible } private readonly Dictionary installed; - private readonly HashSet dlls; - private readonly IDictionary dlc; + private readonly ICollection dlls; + private readonly IDictionary dlc; private List? latestCompatible; private List? latestIncompatible; diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 7e9a759193..9c568ea4a8 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -22,7 +22,7 @@ public interface IRegistryQuerier { ReadOnlyDictionary Repositories { get; } IEnumerable InstalledModules { get; } - IEnumerable InstalledDlls { get; } + ICollection InstalledDlls { get; } IDictionary InstalledDlc { get; } /// @@ -64,6 +64,8 @@ public interface IRegistryQuerier /// IEnumerable AvailableByIdentifier(string identifier); + IEnumerable AllAvailableByProvides(string identifier); + /// /// Returns the latest available version of a module that satisfies the specified version and /// optionally a RelationshipDescriptor. Takes into account module 'provides', which may @@ -427,12 +429,11 @@ public static string CompatibleGameVersions(this CkanModule module, IGame game) /// /// Sequence of removable auto-installed modules, if any /// - private static IEnumerable FindRemovableAutoInstalled( - this IRegistryQuerier querier, - List installedModules, - HashSet dlls, - IDictionary dlc, - GameVersionCriteria crit) + public static IEnumerable FindRemovableAutoInstalled( + this IRegistryQuerier querier, + List installedModules, + IGame game, + GameVersionCriteria crit) { log.DebugFormat("Finding removable autoInstalled for: {0}", string.Join(", ", installedModules.Select(im => im.identifier))); @@ -450,37 +451,18 @@ private static IEnumerable FindRemovableAutoInstalled( installedModules.Where(im => !im.Module.IsDLC) .Select(im => im.Module), null, - opts, querier, crit); + opts, querier, game, crit); var mods = resolver.ModList().ToHashSet(); return autoInstMods.Where( im => autoInstIds.IsSupersetOf( Registry.FindReverseDependencies(new List { im.identifier }, new List(), - mods, dlls, dlc))); + mods, + querier.InstalledDlls, + querier.InstalledDlc))); } - /// - /// Find auto-installed modules that have no depending modules - /// or only auto-installed depending modules. - /// installedModules is a parameter so we can experiment with - /// changes that have not yet been made, such as removing other modules. - /// - /// The modules currently installed - /// Version criteria for resolving relationships - /// - /// Sequence of removable auto-installed modules, if any - /// - public static IEnumerable FindRemovableAutoInstalled( - this IRegistryQuerier querier, - List installedModules, - GameVersionCriteria crit) - => querier?.FindRemovableAutoInstalled(installedModules, - querier.InstalledDlls.ToHashSet(), - querier.InstalledDlc, - crit) - ?? Enumerable.Empty(); - private static readonly ILog log = LogManager.GetLogger(typeof(IRegistryQuerierHelpers)); } } diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 0e538ab55d..fe321b80b2 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -38,7 +38,7 @@ public class Registry : IEnlistmentNotification, IRegistryQuerier // name => relative path [JsonProperty] - private Dictionary? installed_dlls; + private Dictionary installed_dlls; [JsonProperty] [JsonConverter(typeof(JsonParallelDictionaryConverter))] @@ -118,25 +118,23 @@ [JsonIgnore] public IEnumerable InstalledModules /// /// Returns the names of installed DLLs. /// - [JsonIgnore] public IEnumerable InstalledDlls - => installed_dlls?.Keys ?? Enumerable.Empty(); + [JsonIgnore] public ICollection InstalledDlls + => installed_dlls.Keys; /// /// Returns the file path of a DLL. /// null if not found. /// public string? DllPath(string identifier) - => installed_dlls == null - ? null - : installed_dlls.TryGetValue(identifier, out string? path) - ? path - : null; + => installed_dlls.TryGetValue(identifier, out string? path) + ? path + : null; /// /// A map between module identifiers and versions for official DLC that are installed. /// [JsonIgnore] public IDictionary InstalledDlc - => installed_modules.Values + => installedDlc ??= installed_modules.Values .Where(im => im.Module.IsDLC) .ToDictionary(im => im.Module.identifier, im => im.Module.version); @@ -332,6 +330,7 @@ private Registry(RepositoryDataManager? repoData) } installed_modules = new Dictionary(); installed_files = new Dictionary(); + installed_dlls = new Dictionary(); } ~Registry() @@ -531,6 +530,9 @@ private void EnlistWithTransaction() [JsonIgnore] private Dictionary>? providers; + [JsonIgnore] + private IDictionary? installedDlc; + private void InvalidateAvailableModCaches() { log.Debug("Invalidating available mod caches"); @@ -550,8 +552,9 @@ private void InvalidateInstalledCaches() log.Debug("Invalidating installed mod caches"); // These member variables hold references to data that depends on installed modules. // Clear them when the installed modules have changed. - sorter = null; + sorter = null; installedProvides = null; + installedDlc = null; } private void RepositoriesUpdated(Repository[] which) @@ -588,7 +591,7 @@ public CompatibilitySorter SetCompatibleVersion(GameVersionCriteria versCrit) .ThenBy(r => r.name)) ?? Enumerable.Empty>(), providers, - installed_modules, InstalledDlls.ToHashSet(), InstalledDlc); + installed_modules, InstalledDlls, InstalledDlc); } return sorter; } @@ -766,6 +769,13 @@ private void BuildTagIndex() tags.Remove(""); } + public IEnumerable AllAvailableByProvides(string identifier) + => (providers ?? BuildProvidesIndex()) + is Dictionary> allProvs + && allProvs.TryGetValue(identifier, out HashSet? provs) + ? provs + : Enumerable.Empty(); + /// /// /// @@ -848,7 +858,7 @@ public void RegisterModule(CkanModule mod, } // Make sure mod-owned files aren't in the manually installed DLL dict - installed_dlls?.RemoveWhere(kvp => relativeFiles.Contains(kvp.Value)); + installed_dlls.RemoveWhere(kvp => relativeFiles.Contains(kvp.Value)); // Finally register our module proper installed_modules.Add(mod.identifier, @@ -964,7 +974,7 @@ public Dictionary Installed(bool withProvides = true, boo { var installed = new Dictionary(); - if (withDLLs && installed_dlls != null) + if (withDLLs) { // Index our DLLs, as much as we dislike them. foreach (var dllinfo in installed_dlls) @@ -1046,7 +1056,7 @@ internal Dictionary ProvidedByInstalled() out InstalledModule? installedModule) ? installedModule.Module.version // If it's in our autodetected registry, return that. - : installed_dlls != null && installed_dlls.ContainsKey(modIdentifier) + : installed_dlls.ContainsKey(modIdentifier) ? new UnmanagedModuleVersion(null) // Finally we have our provided checks. We'll skip these if // withProvides is false. @@ -1075,7 +1085,7 @@ internal Dictionary ProvidedByInstalled() public void CheckSanity() { SanityChecker.EnforceConsistency(installed_modules.Select(pair => pair.Value.Module), - installed_dlls?.Keys, InstalledDlc); + installed_dlls.Keys, InstalledDlc); } /// @@ -1092,7 +1102,7 @@ public static IEnumerable FindReverseDependencies( List modulesToRemove, List? modulesToInstall, HashSet origInstalled, - HashSet? dlls, + ICollection dlls, IDictionary? dlc, Func? satisfiedFilter = null) { @@ -1175,7 +1185,7 @@ public IEnumerable FindReverseDependencies( => FindReverseDependencies(modulesToRemove, modulesToInstall, new HashSet(installed_modules.Values.Select(x => x.Module)), - installed_dlls?.Keys.ToHashSet(), + installed_dlls.Keys, InstalledDlc, satisfiedFilter); diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index c78275d9ae..8c516add48 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -4,6 +4,7 @@ using log4net; +using CKAN.Games; using CKAN.Versioning; using CKAN.Extensions; @@ -26,36 +27,17 @@ public class RelationshipResolver /// Modules to remove /// Options for the RelationshipResolver /// CKAN registry object for current game instance - /// The current KSP version criteria to consider + /// The current game version criteria to consider public RelationshipResolver(IEnumerable modulesToInstall, IEnumerable? modulesToRemove, RelationshipResolverOptions options, IRegistryQuerier registry, + IGame game, GameVersionCriteria versionCrit) - : this(options, registry, versionCrit) - { - if (modulesToRemove != null) - { - RemoveModsFromInstalledList(modulesToRemove); - } - if (modulesToInstall != null) - { - AddModulesToInstall(modulesToInstall.ToArray()); - } - } - - /// - /// Creates a new Relationship resolver. - /// - /// Options for the RelationshipResolver - /// CKAN registry object for current game instance - /// The current KSP version criteria to consider - private RelationshipResolver(RelationshipResolverOptions options, - IRegistryQuerier registry, - GameVersionCriteria versionCrit) { this.options = options; this.registry = registry; + this.game = game; this.versionCrit = versionCrit; installed_modules = registry.InstalledModules @@ -66,6 +48,29 @@ private RelationshipResolver(RelationshipResolverOptions options, { AddReason(module, installed_relationship); } + + var toInst = modulesToInstall.ToArray(); + resolved = new ResolvedRelationshipsTree(toInst, registry, + installed_modules, versionCrit, + options.OptionalHandling()); + if (!options.proceed_with_inconsistencies) + { + var unsatisfied = resolved.Unsatisfied().ToArray(); + if (unsatisfied.Length > 0) + { + log.DebugFormat("Dependencies failed!{0}{0}{1}{0}{0}{2}", + Environment.NewLine, + Environment.StackTrace, + resolved); + throw new DependenciesNotSatisfiedKraken(unsatisfied, registry, game, resolved); + } + } + + if (modulesToRemove != null) + { + RemoveModsFromInstalledList(modulesToRemove); + } + AddModulesToInstall(toInst); } /// @@ -159,11 +164,8 @@ private void Resolve(CkanModule module, { return; } - else - { - // Mark this module as resolved so we don't recurse here again - alreadyResolved.Add(module); - } + // Mark this module as resolved so we don't recurse here again + alreadyResolved.Add(module); old_stanza = old_stanza?.Memoize(); @@ -213,8 +215,8 @@ private void Resolve(CkanModule module, private void ResolveStanza(List? stanza, SelectionReason reason, RelationshipResolverOptions options, - bool soft_resolve = false, - IEnumerable? old_stanza = null) + bool soft_resolve, + IEnumerable? old_stanza) { if (stanza == null) { @@ -228,31 +230,33 @@ private void ResolveStanza(List? stanza, if (options.get_recommenders && descriptor.suppress_recommendations) { - log.DebugFormat("Skipping {0} because get_recommenders option is set", descriptor.ToString()); + log.DebugFormat("Skipping {0} because get_recommenders option is set", + descriptor.ToString()); suppressedRecommenders.Add(descriptor); continue; } options = orig_options.OptionsFor(descriptor); // If we already have this dependency covered, - // resolve its relationships if we haven't already. - if (descriptor.MatchesAny(modlist.Values, null, null, out CkanModule? installingCandidate)) + // resolve its relationships if we haven't already + if (descriptor.MatchesAny(modlist.Values, null, null, + out CkanModule? installingCandidate) + && installingCandidate != null) { - if (installingCandidate != null) - { - log.DebugFormat("Match already in changeset: {0}, adding reason {1}", installingCandidate, reason); - // Resolve the relationships of the matching module here - // because that's when it would happen if non-virtual - AddReason(installingCandidate, reason); - Resolve(installingCandidate, options, stanza); - } - // If null, it's a DLL or DLC, which we can't resolve + log.DebugFormat("Match already in changeset: {0}, adding reason {1}", + installingCandidate, reason); + AddReason(installingCandidate, reason); + // Resolve the relationships of the matching module here + // because that's when it would happen if non-virtual + Resolve(installingCandidate, options, stanza); continue; } - else if (descriptor.ContainsAny(modlist.Keys)) + if (descriptor.ContainsAny(modlist.Keys)) { // Two installing mods depend on different versions of this dependency var module = modlist.Values.FirstOrDefault(m => descriptor.ContainsAny(new string[] { m.identifier })); + log.DebugFormat("Changeset contains {0}, which doesn't match {1}", + module, descriptor.ToString()); if (options.proceed_with_inconsistencies) { if (module != null && reason is SelectionReason.RelationshipReason rel) @@ -272,15 +276,28 @@ private void ResolveStanza(List? stanza, // If it's already installed, skip. if (descriptor.MatchesAny(installed_modules, - registry.InstalledDlls.ToHashSet(), - registry.InstalledDlc)) + registry.InstalledDlls, + registry.InstalledDlc, + out CkanModule? installedCandidate)) { + if (installedCandidate != null) + { + log.DebugFormat("Match already installed: {0}, adding reason {1}", + installedCandidate, reason); + AddReason(installedCandidate, reason); + } + else + { + log.DebugFormat("Matches installed DLL or DLC"); + } continue; } - else if (descriptor.ContainsAny(installed_modules.Select(im => im.identifier))) + if (descriptor.ContainsAny(installed_modules.Select(im => im.identifier))) { // We need a different version of the mod than is already installed var module = installed_modules.FirstOrDefault(m => descriptor.ContainsAny(new string[] { m.identifier })); + log.DebugFormat("Found installed mod {0}, which doesn't match {1}", + module, descriptor.ToString()); if (options.proceed_with_inconsistencies) { if (module != null && reason is SelectionReason.RelationshipReason rel) @@ -298,45 +315,35 @@ private void ResolveStanza(List? stanza, } } - // Pass mod list in case an older version of a module is conflict-free while later versions have conflicts - var descriptor1 = descriptor; - List candidates = descriptor - .LatestAvailableWithProvides(registry, versionCrit, installed_modules, modlist.Values) - .Where(mod => !modlist.ContainsKey(mod.identifier) - && descriptor1.WithinBounds(mod) - && reason is SelectionReason.RelationshipReason rel - && MightBeInstallable(mod, rel.Parent, installed_modules)) - .ToList(); - if (candidates.Count == 0) - { - // Nothing found, try again while simulating an empty mod list - // Necessary for e.g. proceed_with_inconsistencies, conflicts will still be caught below - candidates = descriptor - .LatestAvailableWithProvides(registry, versionCrit, Array.Empty()) - .Where(mod => !modlist.ContainsKey(mod.identifier) - && descriptor1.WithinBounds(mod) - && MightBeInstallable(mod)) - .ToList(); - } - - if (candidates.Count == 0) + var candidates = resolved.Candidates(descriptor, + modlist.Values.Except(user_requested_mods) + .ToArray()) + .ToArray(); + log.DebugFormat("Got {0} candidates for {1}", + candidates.Length, descriptor.ToString()); + if (candidates.Length == 0) { - if (!soft_resolve && reason is SelectionReason.RelationshipReason rel) + if (!soft_resolve + && !options.proceed_with_inconsistencies + && reason is SelectionReason.RelationshipReason rel) { log.InfoFormat("Dependency on {0} found but it is not listed in the index, or not available for your game version.", descriptor.ToString()); - throw new DependencyNotSatisfiedKraken(rel.Parent, descriptor.ToString() ?? ""); + + throw new DependenciesNotSatisfiedKraken( + new ResolvedByNew(rel.Parent, descriptor, reason), + registry, game, resolved); } log.InfoFormat("{0} is recommended/suggested but it is not listed in the index, or not available for your game version.", descriptor.ToString()); continue; } - if (candidates.Count > 1) + if (candidates.Length > 1) { // Oh no, too many to pick from! if (options.without_toomanyprovides_kraken) { if (options.get_recommenders && reason is not SelectionReason.Depends) { - for (int i = 0; i < candidates.Count; ++i) + for (int i = 0; i < candidates.Length; ++i) { Add(candidates[i], reason is SelectionReason.Recommended rec ? rec.WithIndex(i) @@ -350,22 +357,23 @@ private void ResolveStanza(List? stanza, // we need, then select that. if (old_stanza != null) { - List provide = candidates - .Where(cand => old_stanza.Any(rel => rel.WithinBounds(cand))) - .ToList(); - if (provide.Count != 1 && reason is SelectionReason.RelationshipReason rel) + var provide = candidates.Where(c => old_stanza.Any(rel => rel.WithinBounds(c))) + .ToArray(); + if (provide.Length != 1 && reason is SelectionReason.RelationshipReason rel) { // We still have either nothing, or too many to pick from // Just throw the TMP now throw new TooManyModsProvideKraken(rel.Parent, descriptor.ToString() ?? "", - candidates, descriptor.choice_help_text); + candidates.ToList(), + descriptor.choice_help_text); } candidates[0] = provide.First(); } else if (reason is SelectionReason.RelationshipReason rel) { throw new TooManyModsProvideKraken(rel.Parent, descriptor.ToString() ?? "", - candidates, descriptor.choice_help_text); + candidates.ToList(), + descriptor.choice_help_text); } } @@ -464,60 +472,6 @@ private void Add(CkanModule module, SelectionReason reason) } } - private bool MightBeInstallable(CkanModule module, - CkanModule? stanzaSource = null, - ICollection? installed = null) - => MightBeInstallable(module, stanzaSource, - installed ?? new List(), - new List()); - - /// - /// Tests that a module might be able to be installed via checking if dependencies - /// exist for current version. - /// - /// The module to consider - /// The source of the relationship stanza we're investigating the candidate for - /// The list of installed modules in the current resolver state - /// For internal use - /// Whether its dependencies are compatible with the current game version - private bool MightBeInstallable(CkanModule module, - CkanModule? stanzaSource, - ICollection installed, - List parentCompat) - { - if (module.IsDLC) - { - return false; - } - if (module.depends == null) - { - return true; - } - - // When checking the dependencies we assume that this module is installable - // in case a dependent depends on it - var compatible = parentCompat.Append(module.identifier).ToList(); - var dlls = registry.InstalledDlls.ToHashSet(); - var dlcs = registry.InstalledDlc; - - var toInstall = stanzaSource != null - ? new List { stanzaSource } - : null; - - // Note, .AsParallel() breaks this, too many threads for recursion - return (parentCompat.Count > 0 ? (IEnumerable)module.depends - : module.depends.AsParallel()) - // Skip dependencies satisfied by installed modules - .Where(rel => !rel.MatchesAny(installed, dlls, dlcs)) - // ... or by modules that are about to be installed - .Select(rel => rel.LatestAvailableWithProvides(registry, versionCrit, - installed, toInstall)) - // We need every dependency to have at least one possible module - .All(need => need.Any(mod => compatible.Contains(mod.identifier) - || MightBeInstallable(mod, stanzaSource, - installed, compatible))); - } - /// /// Returns a list of all modules to install to satisfy the changes required. /// Each mod is after its dependencies and before its reverse dependencies. @@ -681,6 +635,8 @@ private void AddReason(CkanModule module, SelectionReason reason) } } + private readonly ResolvedRelationshipsTree resolved; + /// /// The list of all additional mods that need to be installed to satisfy all relationships. /// @@ -698,6 +654,7 @@ private void AddReason(CkanModule module, SelectionReason reason) private readonly HashSet suppressedRecommenders = new HashSet(); private readonly IRegistryQuerier registry; + private readonly IGame game; private readonly GameVersionCriteria versionCrit; private readonly RelationshipResolverOptions options; @@ -711,275 +668,4 @@ private void AddReason(CkanModule module, SelectionReason reason) private static readonly ILog log = LogManager.GetLogger(typeof(RelationshipResolver)); } - // TODO: It would be lovely to get rid of the `without` fields, - // and replace them with `with` fields. Humans suck at inverting - // cases in their heads. - public class RelationshipResolverOptions - { - /// - /// Default options for relationship resolution. - /// - public static RelationshipResolverOptions DefaultOpts() - => new RelationshipResolverOptions(); - - /// - /// Options to install without recommendations. - /// - public static RelationshipResolverOptions DependsOnlyOpts() - => new RelationshipResolverOptions() - { - with_recommends = false, - with_suggests = false, - with_all_suggests = false, - }; - - /// - /// Options to find all dependencies, recommendations, and suggestions - /// of anything in the changeset (except when suppress_recommendations==true), - /// without throwing exceptions, so the calling code can decide what to do about conflicts - /// - public static RelationshipResolverOptions KitchenSinkOpts() - => new RelationshipResolverOptions() - { - with_recommends = true, - with_suggests = true, - with_all_suggests = true, - without_toomanyprovides_kraken = true, - without_enforce_consistency = true, - proceed_with_inconsistencies = true, - get_recommenders = true, - }; - - public static RelationshipResolverOptions ConflictsOpts() - => new RelationshipResolverOptions() - { - without_toomanyprovides_kraken = true, - proceed_with_inconsistencies = true, - without_enforce_consistency = true, - with_recommends = false, - }; - - /// - /// If true, add recommended mods, and their recommendations. - /// - public bool with_recommends = true; - - /// - /// If true, add suggests, but not suggested suggests. :) - /// - public bool with_suggests = false; - - /// - /// If true, add suggested modules, and *their* suggested modules, too! - /// - public bool with_all_suggests = false; - - /// - /// If true, surpresses the TooManyProvides kraken when resolving - /// relationships. Otherwise, we just pick the first. - /// - public bool without_toomanyprovides_kraken = false; - - /// - /// If true, we skip our sanity check at the end of our relationship - /// resolution. Note that non-sane resolutions can't actually be - /// installed, so this is mostly useful for giving the user feedback - /// on failed resolutions. - /// - public bool without_enforce_consistency = false; - - /// - /// If true, we'll populate the `conflicts` field, rather than immediately - /// throwing a kraken when inconsistencies are detected. Again, these - /// solutions are non-installable, so mostly of use to provide user - /// feedback when things go wrong. - /// - public bool proceed_with_inconsistencies = false; - - /// - /// If true, then if a module has no versions that are compatible with - /// the current game version, then we will consider incompatible versions - /// of that module. - /// This replaces the former behavior of ignoring compatibility for - /// `install identifier=version` commands. - /// - public bool allow_incompatible = false; - - /// - /// If true, get the list of mods that should be checked for - /// recommendations and suggestions. - /// Differs from normal resolution in that it stops when - /// ModuleRelationshipDescriptor.suppress_recommendations==true - /// - public bool get_recommenders = false; - - public RelationshipResolverOptions OptionsFor(RelationshipDescriptor descr) - => descr.suppress_recommendations ? WithoutRecommendations() : this; - - public RelationshipResolverOptions WithoutRecommendations() - { - if (with_recommends || with_all_suggests || with_suggests) - { - var newOptions = (RelationshipResolverOptions)MemberwiseClone(); - newOptions.with_recommends = false; - newOptions.with_all_suggests = false; - newOptions.with_suggests = false; - return newOptions; - } - return this; - } - - public RelationshipResolverOptions WithoutSuggestions() - { - if (with_suggests) - { - var newOptions = (RelationshipResolverOptions)MemberwiseClone(); - newOptions.with_suggests = false; - return newOptions; - } - return this; - } - - } - - /// - /// Used to keep track of the relationships between modules in the resolver. - /// Intended to be used for displaying messages to the user. - /// - public abstract class SelectionReason : IEquatable - { - // Currently assumed to exist for any relationship other than UserRequested or Installed - public virtual string DescribeWith(IEnumerable others) - => ToString() ?? ""; - - public override bool Equals(object? obj) - => Equals(obj as SelectionReason); - - public bool Equals(SelectionReason? rsn) - => GetType() == rsn?.GetType(); - - public override int GetHashCode() - => GetType().GetHashCode(); - - public class Installed : SelectionReason - { - public override string ToString() - => Properties.Resources.RelationshipResolverInstalledReason; - } - - public class UserRequested : SelectionReason - { - public override string ToString() - => Properties.Resources.RelationshipResolverUserReason; - } - - public class DependencyRemoved : SelectionReason - { - public override string ToString() - => Properties.Resources.RelationshipResolverDependencyRemoved; - } - - public class NoLongerUsed : SelectionReason - { - public override string ToString() - => Properties.Resources.RelationshipResolverNoLongerUsedReason; - } - - public abstract class RelationshipReason : SelectionReason, IEquatable - { - public RelationshipReason(CkanModule parent) - { - Parent = parent; - } - - public CkanModule Parent; - - public bool Equals(RelationshipReason? rsn) - => GetType() == rsn?.GetType() - && Parent == rsn?.Parent; - - public override int GetHashCode() - { - var type = GetType(); - #if NET5_0_OR_GREATER - return HashCode.Combine(type, Parent); - #else - unchecked - { - return (type, Parent).GetHashCode(); - } - #endif - } - } - - public class Replacement : RelationshipReason - { - public Replacement(CkanModule module) - : base(module) - { - } - - public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverReplacementReason, - Parent.name); - - public override string DescribeWith(IEnumerable others) - => string.Format(Properties.Resources.RelationshipResolverReplacementReason, - string.Join(", ", - Enumerable.Repeat(this, 1) - .Concat(others) - .OfType() - .Select(r => r.Parent.name))); - } - - public sealed class Suggested : RelationshipReason - { - public Suggested(CkanModule module) - : base(module) - { - } - - public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverSuggestedReason, - Parent.name); - } - - public sealed class Depends : RelationshipReason - { - public Depends(CkanModule module) - : base(module) - { - } - - public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverDependsReason, - Parent.name); - - public override string DescribeWith(IEnumerable others) - => string.Format(Properties.Resources.RelationshipResolverDependsReason, - string.Join(", ", - Enumerable.Repeat(this, 1) - .Concat(others) - .OfType() - .Select(r => r.Parent.name))); - } - - public sealed class Recommended : RelationshipReason - { - public Recommended(CkanModule module, int providesIndex) - : base(module) - { - ProvidesIndex = providesIndex; - } - - public readonly int ProvidesIndex; - - public Recommended WithIndex(int providesIndex) - => new Recommended(Parent, providesIndex); - - public override string ToString() - => string.Format(Properties.Resources.RelationshipResolverRecommendedReason, - Parent.name); - } - } } diff --git a/Core/Relationships/RelationshipResolverOptions.cs b/Core/Relationships/RelationshipResolverOptions.cs new file mode 100644 index 0000000000..1edc1aa523 --- /dev/null +++ b/Core/Relationships/RelationshipResolverOptions.cs @@ -0,0 +1,141 @@ +namespace CKAN +{ + // TODO: It would be lovely to get rid of the `without` fields, + // and replace them with `with` fields. Humans suck at inverting + // cases in their heads. + public class RelationshipResolverOptions + { + /// + /// Default options for relationship resolution. + /// + public static RelationshipResolverOptions DefaultOpts() + => new RelationshipResolverOptions(); + + /// + /// Options to install without recommendations. + /// + public static RelationshipResolverOptions DependsOnlyOpts() + => new RelationshipResolverOptions() + { + with_recommends = false, + with_suggests = false, + with_all_suggests = false, + }; + + /// + /// Options to find all dependencies, recommendations, and suggestions + /// of anything in the changeset (except when suppress_recommendations==true), + /// without throwing exceptions, so the calling code can decide what to do about conflicts + /// + public static RelationshipResolverOptions KitchenSinkOpts() + => new RelationshipResolverOptions() + { + with_recommends = true, + with_suggests = true, + without_toomanyprovides_kraken = true, + without_enforce_consistency = true, + proceed_with_inconsistencies = true, + get_recommenders = true, + }; + + public static RelationshipResolverOptions ConflictsOpts() + => new RelationshipResolverOptions() + { + without_toomanyprovides_kraken = true, + proceed_with_inconsistencies = true, + without_enforce_consistency = true, + with_recommends = false, + }; + + /// + /// If true, add recommended mods, and their recommendations. + /// + public bool with_recommends = true; + + /// + /// If true, add suggests, but not suggested suggests. :) + /// + public bool with_suggests = false; + + /// + /// If true, add suggested modules, and *their* suggested modules, too! + /// + public bool with_all_suggests = false; + + /// + /// If true, surpresses the TooManyProvides kraken when resolving + /// relationships. Otherwise, we just pick the first. + /// + public bool without_toomanyprovides_kraken = false; + + /// + /// If true, we skip our sanity check at the end of our relationship + /// resolution. Note that non-sane resolutions can't actually be + /// installed, so this is mostly useful for giving the user feedback + /// on failed resolutions. + /// + public bool without_enforce_consistency = false; + + /// + /// If true, we'll populate the `conflicts` field, rather than immediately + /// throwing a kraken when inconsistencies are detected. Again, these + /// solutions are non-installable, so mostly of use to provide user + /// feedback when things go wrong. + /// + public bool proceed_with_inconsistencies = false; + + /// + /// If true, then if a module has no versions that are compatible with + /// the current game version, then we will consider incompatible versions + /// of that module. + /// This replaces the former behavior of ignoring compatibility for + /// `install identifier=version` commands. + /// + public bool allow_incompatible = false; + + /// + /// If true, get the list of mods that should be checked for + /// recommendations and suggestions. + /// Differs from normal resolution in that it stops when + /// ModuleRelationshipDescriptor.suppress_recommendations==true + /// + public bool get_recommenders = false; + + public RelationshipResolverOptions OptionsFor(RelationshipDescriptor descr) + => descr.suppress_recommendations ? WithoutRecommendations() : this; + + public RelationshipResolverOptions WithoutRecommendations() + { + if (with_recommends || with_all_suggests || with_suggests) + { + var newOptions = (RelationshipResolverOptions)MemberwiseClone(); + newOptions.with_recommends = false; + newOptions.with_all_suggests = false; + newOptions.with_suggests = false; + return newOptions; + } + return this; + } + + public RelationshipResolverOptions WithoutSuggestions() + { + if (with_suggests) + { + var newOptions = (RelationshipResolverOptions)MemberwiseClone(); + newOptions.with_suggests = false; + return newOptions; + } + return this; + } + + public OptionalRelationships OptionalHandling() + => (with_all_suggests ? OptionalRelationships.AllSuggestions | OptionalRelationships.Suggestions + : OptionalRelationships.None) + | (with_suggests ? OptionalRelationships.Suggestions + : OptionalRelationships.None) + | (with_recommends ? OptionalRelationships.Recommendations + : OptionalRelationships.None); + + } + +} diff --git a/Core/Relationships/ResolvedRelationship.cs b/Core/Relationships/ResolvedRelationship.cs new file mode 100644 index 0000000000..0c054db125 --- /dev/null +++ b/Core/Relationships/ResolvedRelationship.cs @@ -0,0 +1,195 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Concurrent; + +using CKAN.Versioning; + +namespace CKAN +{ + using RelationshipCache = ConcurrentDictionary; + + public abstract class ResolvedRelationship : IEquatable + { + public ResolvedRelationship(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason) + { + this.source = source; + this.relationship = relationship; + this.reason = reason; + } + + public readonly CkanModule source; + public readonly RelationshipDescriptor relationship; + public readonly SelectionReason reason; + + public virtual bool Contains(CkanModule mod) + => false; + + public virtual bool Unsatisfied() + => false; + + public virtual bool Unsatisfied(ICollection installing) + => false; + + public override string ToString() + => $"{source} {reason.GetType().Name} {relationship}"; + + public virtual IEnumerable ToLines() + => Enumerable.Repeat(ToString(), 1); + + public abstract ResolvedRelationship WithSource(CkanModule newSrc, + SelectionReason newRsn); + + public override bool Equals(object? other) + => Equals(other as ResolvedRelationship); + + public bool Equals(ResolvedRelationship? other) + => source.Equals(other?.source) + && relationship.Equals(other?.relationship) + && reason.Equals(other?.reason); + + public override int GetHashCode() + => (source, relationship, reason).GetHashCode(); + } + + public class ResolvedByInstalled : ResolvedRelationship + { + public ResolvedByInstalled(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + CkanModule installed) + : base(source, relationship, reason) + { + this.installed = installed; + } + + public readonly CkanModule installed; + + public override bool Contains(CkanModule mod) + => installed == mod; + + public override string ToString() + => $"{source} {relationship}: Installed {installed}"; + + public override ResolvedRelationship WithSource(CkanModule newSrc, SelectionReason newRsn) + => new ResolvedByInstalled(newSrc, relationship, newRsn, installed); + } + + public class ResolvedByInstalling : ResolvedRelationship + { + public ResolvedByInstalling(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + CkanModule installing) + : base(source, relationship, reason) + { + this.installing = installing; + } + + public readonly CkanModule installing; + + public override bool Contains(CkanModule mod) + => installing == mod; + + public override string ToString() + => $"{base.ToString()}: Installing {installing}"; + + public override ResolvedRelationship WithSource(CkanModule newSrc, SelectionReason newRsn) + => new ResolvedByInstalling(newSrc, relationship, newRsn, installing); + } + + public class ResolvedByDLL : ResolvedRelationship + { + public ResolvedByDLL(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason) + : base(source, relationship, reason) + { + } + + public override string ToString() + => $"{base.ToString()}: DLL"; + + public override ResolvedRelationship WithSource(CkanModule newSrc, SelectionReason newRsn) + => new ResolvedByDLL(newSrc, relationship, newRsn); + } + + public class ResolvedByNew : ResolvedRelationship + { + public ResolvedByNew(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + IReadOnlyDictionary resolved) + : base(source, relationship, reason) + { + this.resolved = resolved; + } + + public ResolvedByNew(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason) + : this(source, relationship, reason, + new Dictionary()) + { + } + + public ResolvedByNew(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + IEnumerable providers, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection installed, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) + : this(source, relationship, reason, + providers.ToDictionary(prov => prov, + prov => ResolvedRelationshipsTree.ResolveModule( + prov, definitelyInstalling, allInstalling, registry, installed, crit, + (optRels & OptionalRelationships.AllSuggestions) == 0 + ? optRels & ~OptionalRelationships.Suggestions + : optRels, + relationshipCache) + .ToArray())) + { + } + + /// + /// The modules that can satisfy this relationship and their own relationships. + /// If this is empty, then the relationship cannot be satisfied. + /// + public readonly IReadOnlyDictionary resolved; + + public override bool Contains(CkanModule mod) + => resolved.Any(rr => rr.Key == mod || rr.Value.Any(rrr => rrr.Contains(mod))); + + public override bool Unsatisfied() + => reason is SelectionReason.Depends && resolved.Count == 0; + + public override bool Unsatisfied(ICollection installing) + => reason is SelectionReason.Depends + && !resolved.Any(kvp => AvailableModule.DependsAndConflictsOK(kvp.Key, installing) + && kvp.Value.All(rr => !rr.Unsatisfied(installing))); + + public override string ToString() + => string.Join(Environment.NewLine, ToLines()); + + public override IEnumerable ToLines() + => Enumerable.Repeat(resolved.Count > 0 ? $"{base.ToString()}:" + : $"UNRESOLVED {base.ToString()}", 1) + .Concat(resolved.SelectMany(kvp => Enumerable.Repeat(kvp.Value.Length > 0 + ? $"Module {kvp.Key}:" + : $"Module {kvp.Key}", 1) + .Concat(kvp.Value + .SelectMany(rr => rr.ToLines()) + .Select(line => $"\t{line}"))) + .Select(line => $"\t{line}")); + + public override ResolvedRelationship WithSource(CkanModule newSrc, SelectionReason newRsn) + => new ResolvedByNew(newSrc, relationship, newRsn, resolved); + } +} diff --git a/Core/Relationships/ResolvedRelationshipsTree.cs b/Core/Relationships/ResolvedRelationshipsTree.cs new file mode 100644 index 0000000000..5bf1e228e7 --- /dev/null +++ b/Core/Relationships/ResolvedRelationshipsTree.cs @@ -0,0 +1,165 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Concurrent; + +using CKAN.Versioning; +using CKAN.Extensions; + +namespace CKAN +{ + using RelationshipCache = ConcurrentDictionary; + + [Flags] + public enum OptionalRelationships + { + None = 0, + Recommendations = 1, + Suggestions = 2, + AllSuggestions = 4, + } + + public class ResolvedRelationshipsTree + { + public ResolvedRelationshipsTree(ICollection modules, + IRegistryQuerier registry, + ICollection installed, + GameVersionCriteria crit, + OptionalRelationships optRels) + { + resolved = ResolveManyCached(modules, registry, installed, crit, optRels, relationshipCache).ToArray(); + } + + public static IEnumerable ResolveModule(CkanModule module, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection installed, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) + => ResolveRelationships(module, module.depends, new SelectionReason.Depends(module), + definitelyInstalling, allInstalling, registry, installed, crit, optRels, relationshipCache) + .Concat((optRels & OptionalRelationships.Recommendations) == 0 + ? Enumerable.Empty() + : ResolveRelationships(module, module.recommends, new SelectionReason.Recommended(module, 0), + definitelyInstalling, allInstalling, registry, installed, crit, optRels, relationshipCache)) + .Concat((optRels & OptionalRelationships.Suggestions) == 0 + ? Enumerable.Empty() + : ResolveRelationships(module, module.suggests, new SelectionReason.Suggested(module), + definitelyInstalling, allInstalling, registry, installed, crit, optRels, relationshipCache)); + + public IEnumerable Unsatisfied() + => resolved.SelectMany(UnsatisfiedFrom); + + private static IEnumerable UnsatisfiedFrom(ResolvedRelationship rr) + { + // Our goal here is to return an array of ResolvedRelationships for each full + // trace from rr to a relationship we can't satisfy. + // First we need to make sure we even care about this one, i.e. that it's required. + if (rr.reason is SelectionReason.Depends) + { + // Now if this relationship itself can't be resolved directly, return it. + if (rr.Unsatisfied()) + { + return Enumerable.Repeat(new ResolvedRelationship[] { rr }, 1); + } + // Now we know it's a dependency that has at least one option for satisfying it, + // but those options may or may not be fully satisfied when considering _their_ dependencies. + + // If any of these options works, then we want to return nothing. + // Otherwise we want to return all of the descriptions of why everything failed, + // with rr prepended to the start of each array. + if (rr is ResolvedByNew rbn) + { + var unsats = rbn.resolved.Values + .Select(modsRels => modsRels.SelectMany(UnsatisfiedFrom) + .ToArray()) + .Memoize(); + + return unsats.Any(u => u.Length == 0) + // One of the dependencies is fully satisfied + ? Enumerable.Empty() + : unsats.SelectMany(uns => uns.Select(u => u.Prepend(rr).ToArray())); + } + } + return Enumerable.Empty(); + } + + public IEnumerable Candidates(RelationshipDescriptor rel, + ICollection installing) + => relationshipCache.TryGetValue(rel, out ResolvedRelationship? rr) + && rr is ResolvedByNew resRel + ? resRel.resolved + .Where(kvp => AvailableModule.DependsAndConflictsOK(kvp.Key, installing) + && kvp.Value.All(subRR => !subRR.Unsatisfied(installing))) + .Select(kvp => kvp.Key) + : Enumerable.Empty(); + + public override string ToString() + => string.Join(Environment.NewLine, + resolved.Select(rr => rr.ToString())); + + private static IEnumerable ResolveManyCached(ICollection modules, + IRegistryQuerier registry, + ICollection installed, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) + => modules.SelectMany(m => ResolveModule(m, modules, modules, registry, installed, crit, optRels, + relationshipCache)); + + private static IEnumerable ResolveRelationships(CkanModule module, + List? relationships, + SelectionReason reason, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection installed, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) + => relationships?.Select(dep => Resolve(module, dep, reason, + definitelyInstalling, allInstalling, registry, installed, + crit, optRels, relationshipCache)) + ?? Enumerable.Empty(); + + private static ResolvedRelationship Resolve(CkanModule source, + RelationshipDescriptor relationship, + SelectionReason reason, + ICollection definitelyInstalling, + ICollection allInstalling, + IRegistryQuerier registry, + ICollection installed, + GameVersionCriteria crit, + OptionalRelationships optRels, + RelationshipCache relationshipCache) + => relationshipCache.TryGetValue(relationship, + out ResolvedRelationship? cachedRel) + ? cachedRel.WithSource(source, reason) + : relationship.MatchesAny(installed, registry.InstalledDlls, registry.InstalledDlc, + out CkanModule? installedMatch) + ? relationshipCache.GetOrAdd( + relationship, + installedMatch == null ? new ResolvedByDLL(source, relationship, reason) + : new ResolvedByInstalled(source, relationship, reason, + installedMatch)) + : relationship.MatchesAny(allInstalling, null, null, + out CkanModule? installingMatch) + && installingMatch != null + // Installing mods are branch-specific, so don't cache them + ? new ResolvedByInstalling(source, relationship, reason, installingMatch) + : relationshipCache.GetOrAdd( + relationship, + new ResolvedByNew(source, relationship, reason, + relationship.LatestAvailableWithProvides(registry, crit, + installed, definitelyInstalling), + definitelyInstalling, + allInstalling.Append(source).ToArray(), + registry, installed, crit, optRels, + relationshipCache)); + + private readonly ResolvedRelationship[] resolved; + private readonly RelationshipCache relationshipCache = new RelationshipCache(); + } +} diff --git a/Core/Relationships/SanityChecker.cs b/Core/Relationships/SanityChecker.cs index ff0d4766fe..8e5b9fb841 100644 --- a/Core/Relationships/SanityChecker.cs +++ b/Core/Relationships/SanityChecker.cs @@ -3,9 +3,6 @@ using System.Linq; using CKAN.Versioning; -#if NETSTANDARD2_0 -using CKAN.Extensions; -#endif namespace CKAN { @@ -23,7 +20,7 @@ public static class SanityChecker /// Does nothing if the modules can happily co-exist. /// public static void EnforceConsistency(IEnumerable modules, - IEnumerable? dlls = null, + ICollection dlls, IDictionary? dlc = null) { if (!CheckConsistency(modules, dlls, dlc, @@ -39,21 +36,20 @@ public static void EnforceConsistency(IEnumerable module /// This is only used by tests! /// public static bool IsConsistent(IEnumerable modules, - IEnumerable? dlls = null, + ICollection? dlls = null, IDictionary? dlc = null) => CheckConsistency(modules, dlls, dlc, out var _, out var _); - private static bool CheckConsistency(IEnumerable modules, - IEnumerable? dlls, - IDictionary? dlc, + private static bool CheckConsistency(IEnumerable modules, + ICollection? dlls, + IDictionary? dlc, out List> UnmetDepends, - out modRelList Conflicts) + out modRelList Conflicts) { var modList = modules.ToList(); - var dllSet = dlls?.ToHashSet(); - UnmetDepends = FindUnsatisfiedDepends(modList, dllSet, dlc).ToList(); - Conflicts = FindConflicting(modList, dllSet, dlc); + UnmetDepends = FindUnsatisfiedDepends(modList, dlls, dlc).ToList(); + Conflicts = FindConflicting(modList, dlls, dlc); return UnmetDepends.Count == 0 && Conflicts.Count == 0; } @@ -69,7 +65,7 @@ private static bool CheckConsistency(IEnumerable modules /// public static IEnumerable> FindUnsatisfiedDepends( ICollection modules, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc) => (modules?.Where(m => m.depends != null) .SelectMany(m => (m.depends ?? Enumerable.Empty()) @@ -90,7 +86,7 @@ public static IEnumerable> FindUnsatis /// Each Key is the depending module, and each Value is the relationship. /// private static modRelList FindConflicting(List modules, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc) => modules.Where(m => m.conflicts != null) .SelectMany(m => FindConflictingWith( @@ -102,7 +98,7 @@ private static modRelList FindConflicting(List mo private static IEnumerable FindConflictingWith(CkanModule module, List otherMods, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc) => module.conflicts?.Select(rel => rel.MatchesAny(otherMods, dlls, dlc, out CkanModule? other) ? new modRelPair(module, rel, other) diff --git a/Core/Relationships/SelectionReason.cs b/Core/Relationships/SelectionReason.cs new file mode 100644 index 0000000000..b6385c8abf --- /dev/null +++ b/Core/Relationships/SelectionReason.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CKAN +{ + /// + /// Used to keep track of the relationships between modules in the resolver. + /// Intended to be used for displaying messages to the user. + /// + public abstract class SelectionReason : IEquatable + { + // Currently assumed to exist for any relationship other than UserRequested or Installed + public virtual string DescribeWith(IEnumerable others) + => ToString() ?? ""; + + public override bool Equals(object? obj) + => Equals(obj as SelectionReason); + + public bool Equals(SelectionReason? rsn) + => GetType() == rsn?.GetType(); + + public override int GetHashCode() + => GetType().GetHashCode(); + + public virtual SelectionReason WithIndex(int providesIndex) + => this; + + public class Installed : SelectionReason + { + public override string ToString() + => Properties.Resources.RelationshipResolverInstalledReason; + } + + public class UserRequested : SelectionReason + { + public override string ToString() + => Properties.Resources.RelationshipResolverUserReason; + } + + public class DependencyRemoved : SelectionReason + { + public override string ToString() + => Properties.Resources.RelationshipResolverDependencyRemoved; + } + + public class NoLongerUsed : SelectionReason + { + public override string ToString() + => Properties.Resources.RelationshipResolverNoLongerUsedReason; + } + + public abstract class RelationshipReason : SelectionReason, IEquatable + { + public RelationshipReason(CkanModule parent) + { + Parent = parent; + } + + public CkanModule Parent; + + public bool Equals(RelationshipReason? rsn) + => GetType() == rsn?.GetType() + && Parent == rsn?.Parent; + + public override bool Equals(object? obj) + => Equals(obj as RelationshipReason); + + public override int GetHashCode() + { + var type = GetType(); + #if NET5_0_OR_GREATER + return HashCode.Combine(type, Parent); + #else + unchecked + { + return (type, Parent).GetHashCode(); + } + #endif + } + + } + + public class Replacement : RelationshipReason + { + public Replacement(CkanModule module) + : base(module) + { + } + + public override string ToString() + => string.Format(Properties.Resources.RelationshipResolverReplacementReason, + Parent.name); + + public override string DescribeWith(IEnumerable others) + => string.Format(Properties.Resources.RelationshipResolverReplacementReason, + string.Join(", ", + Enumerable.Repeat(this, 1) + .Concat(others) + .OfType() + .Select(r => r.Parent.name))); + } + + public sealed class Suggested : RelationshipReason + { + public Suggested(CkanModule module) + : base(module) + { + } + + public override string ToString() + => string.Format(Properties.Resources.RelationshipResolverSuggestedReason, + Parent.name); + } + + public sealed class Depends : RelationshipReason + { + public Depends(CkanModule module) + : base(module) + { + } + + public override string ToString() + => string.Format(Properties.Resources.RelationshipResolverDependsReason, + Parent.name); + + public override string DescribeWith(IEnumerable others) + => string.Format(Properties.Resources.RelationshipResolverDependsReason, + string.Join(", ", + Enumerable.Repeat(this, 1) + .Concat(others) + .OfType() + .Select(r => r.Parent.name))); + } + + public sealed class Recommended : RelationshipReason + { + public Recommended(CkanModule module, int providesIndex) + : base(module) + { + ProvidesIndex = providesIndex; + } + + public readonly int ProvidesIndex; + + public override SelectionReason WithIndex(int providesIndex) + => new Recommended(Parent, providesIndex); + + public override string ToString() + => string.Format(Properties.Resources.RelationshipResolverRecommendedReason, + Parent.name); + } + } +} diff --git a/Core/Repositories/AvailableModule.cs b/Core/Repositories/AvailableModule.cs index 7769770d12..30523b9f65 100644 --- a/Core/Repositories/AvailableModule.cs +++ b/Core/Repositories/AvailableModule.cs @@ -100,11 +100,10 @@ private void Add(CkanModule module) /// Modules that are already installed /// Modules that are planned to be installed /// - public CkanModule? Latest( - GameVersionCriteria? ksp_version = null, - RelationshipDescriptor? relationship = null, - ICollection? installed = null, - ICollection? toInstall = null) + public CkanModule? Latest(GameVersionCriteria? ksp_version = null, + RelationshipDescriptor? relationship = null, + ICollection? installed = null, + ICollection? toInstall = null) { IEnumerable modules = module_version.Values.Reverse(); if (relationship != null) @@ -126,7 +125,8 @@ private void Add(CkanModule module) return modules.FirstOrDefault(); } - private static bool DependsAndConflictsOK(CkanModule module, ICollection others) + public static bool DependsAndConflictsOK(CkanModule module, + ICollection others) { if (module.depends != null) { diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index c2c68ea847..81c8b12442 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -496,7 +496,9 @@ public bool IsCompatible(GameVersionCriteria version) { var compat = _comparator.Compatible(version, this); log.DebugFormat("Checking compat of {0} with game versions {1}: {2}", - this, version.ToString(), compat ? "Compatible": "Incompatible"); + this, + version.ToString(), + compat ? "Compatible": "Incompatible"); return compat; } diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index 85820c0a01..71d5f6478a 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -4,7 +4,13 @@ using System.Text; using System.Collections.Generic; +using log4net; + +using CKAN.Games; using CKAN.Versioning; +#if NETFRAMEWORK || NETSTANDARD +using CKAN.Extensions; +#endif namespace CKAN { @@ -97,35 +103,60 @@ public ModuleNotFoundKraken(string module, string? version = null) /// /// Exception describing a missing dependency /// - public class DependencyNotSatisfiedKraken : ModuleNotFoundKraken + public class DependenciesNotSatisfiedKraken : Kraken { /// - /// The mod with an unmet dependency - /// - public readonly CkanModule parent; - - /// - /// Initialize the exceptions + /// Initialize the exception representing failed dependency resolution /// - /// The module with the unmet dependency - /// The name of the missing dependency - /// Message parameter for base class + /// List of chain of relationships with last one unsatisfied /// Originating exception parameter for base class - public DependencyNotSatisfiedKraken(CkanModule parentModule, - string module, - string? version = null, - string? reason = null, - Exception? innerException = null) - : base(module, version, - reason ?? string.Format( - Properties.Resources.KrakenParentDependencyNotSatisfied, - parentModule.identifier, - module, - version ?? Properties.Resources.KrakenAny), + public DependenciesNotSatisfiedKraken(ICollection unsatisfied, + IRegistryQuerier registry, + IGame game, + ResolvedRelationshipsTree resolved, + Exception? innerException = null) + : base(string.Join(Environment.NewLine + Environment.NewLine, + unsatisfied.GroupBy(rrs => rrs.Last().relationship) + .OrderByDescending(grp => grp.Count()) + .ThenBy(grp => grp.Key.ToString()) + .Select(grp => string.Format(Properties.Resources.KrakenMissingDependency, + string.Join("; ", + grp.DistinctBy(rrs => rrs.Last().source) + .Select(FormatDependsChain)), + grp.Key.ToStringWithCompat(registry, game)))), innerException) { - parent = parentModule; + this.unsatisfied = unsatisfied; + log.DebugFormat("Resolved relationships tree:\r\n{0}", + resolved); } + + public DependenciesNotSatisfiedKraken(ResolvedRelationship badOne, + IRegistryQuerier registry, + IGame game, + ResolvedRelationshipsTree resolved, + Exception? innerException = null) + : this(new ResolvedRelationship[][] { new ResolvedRelationship[] { badOne } }, + registry, game, resolved, innerException) + { + } + + public readonly ICollection unsatisfied; + + private static string FormatDependsChain(ResolvedRelationship[] dependsChain) + => dependsChain.Length == 1 + ? dependsChain.Last().source.ToString() + : string.Format("{0} ({1})", + dependsChain.Last().source, + string.Join(", ", dependsChain.Reverse() + .Skip(1) + .Select(FormatDependsLink))); + + private static string FormatDependsLink(ResolvedRelationship rr) + => string.Format(Properties.Resources.KrakenMissingDependencyNeededFor, + rr.source); + + private static readonly ILog log = LogManager.GetLogger(typeof(DependenciesNotSatisfiedKraken)); } public class NotKSPDirKraken : Kraken diff --git a/Core/Types/RelationshipDescriptor.cs b/Core/Types/RelationshipDescriptor.cs index cbe83e54c9..736f7e2f5b 100644 --- a/Core/Types/RelationshipDescriptor.cs +++ b/Core/Types/RelationshipDescriptor.cs @@ -5,19 +5,21 @@ using Newtonsoft.Json; +using CKAN.Games; using CKAN.Versioning; +using CKAN.Extensions; namespace CKAN { public abstract class RelationshipDescriptor : IEquatable { public bool MatchesAny(ICollection modules, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc) => MatchesAny(modules, dlls, dlc, out CkanModule? _); public abstract bool MatchesAny(ICollection modules, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc, out CkanModule? matched); @@ -33,12 +35,29 @@ public abstract List LatestAvailableWithProvides(IRegistryQuerier ICollection? installed = null, ICollection? toInstall = null); + public override bool Equals(object? other) + => Equals(other as RelationshipDescriptor); + public abstract bool Equals(RelationshipDescriptor? other); + public static bool operator ==(RelationshipDescriptor? left, + RelationshipDescriptor? right) + => Equals(left, right); + + public static bool operator !=(RelationshipDescriptor? left, + RelationshipDescriptor? right) + => !Equals(left, right); + + public abstract override int GetHashCode(); + public abstract bool ContainsAny(IEnumerable identifiers); public abstract bool StartsWith(string prefix); + public virtual string ToStringWithCompat(IRegistryQuerier registry, + IGame game) + => ToString() ?? ""; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string? choice_help_text; @@ -104,7 +123,7 @@ public bool WithinBounds(ModuleVersion? other) /// true if any of the modules match this descriptor, false otherwise. /// public override bool MatchesAny(ICollection modules, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc, out CkanModule? matched) { @@ -116,7 +135,7 @@ public override bool MatchesAny(ICollection modules, } // .AsParallel() makes this slower, too many threads - matched = modules?.FirstOrDefault(WithinBounds); + matched = modules.FirstOrDefault(WithinBounds); if (matched != null) { return true; @@ -148,6 +167,9 @@ protected bool Equals(ModuleRelationshipDescriptor? other) && min_version == other.min_version && max_version == other.max_version; + public override int GetHashCode() + => (name, version, min_version, max_version).GetHashCode(); + public override bool ContainsAny(IEnumerable identifiers) => identifiers.Contains(name); @@ -174,6 +196,27 @@ public override string ToString() ? string.Format(Properties.Resources.RelationshipDescriptorMaxVersionOnly, name, max_version) : name; + public override string ToStringWithCompat(IRegistryQuerier registry, + IGame game) + => Utilities.DefaultIfThrows(() => registry.AllAvailableByProvides(name) + .SelectMany(avail => avail.AllAvailable()) + .Where(WithinBounds) + .ToArray()) + is CkanModule[] { Length: >0 } modules + ? string.Format("{0} ({1})", ToString(), + DescribeCompatibility(modules, game)) + : ToString(); + + private static string DescribeCompatibility(CkanModule[] modules, + IGame game) + { + CkanModule.GetMinMaxVersions(modules, + out _, out _, + out var minKsp, out var maxKsp); + return GameVersionRange.VersionSpan(game, + minKsp ?? GameVersion.Any, + maxKsp ?? GameVersion.Any); + } } public class AnyOfRelationshipDescriptor : RelationshipDescriptor @@ -194,7 +237,7 @@ public override bool WithinBounds(CkanModule otherModule) => any_of?.Any(r => r.WithinBounds(otherModule)) ?? false; public override bool MatchesAny(ICollection modules, - HashSet? dlls, + ICollection? dlls, IDictionary? dlc, out CkanModule? matched) { @@ -230,6 +273,9 @@ protected bool Equals(AnyOfRelationshipDescriptor? other) && (any_of?.SequenceEqual(other.any_of ?? Enumerable.Empty()) ?? other.any_of == null); + public override int GetHashCode() + => any_of?.ToSequenceHashCode() ?? 0; + public override bool ContainsAny(IEnumerable identifiers) => any_of?.Any(r => r.ContainsAny(identifiers)) ?? false; @@ -241,5 +287,11 @@ public override string ToString() .Aggregate((a, b) => string.Format(Properties.Resources.RelationshipDescriptorAnyOfJoiner, a, b)) ?? ""; + + public override string ToStringWithCompat(IRegistryQuerier registry, IGame game) + => any_of?.Select(r => r.ToStringWithCompat(registry, game)) + .Aggregate((a, b) => + string.Format(Properties.Resources.RelationshipDescriptorAnyOfJoiner, a, b)) + ?? ""; } } diff --git a/GUI/Controls/ChooseRecommendedMods.cs b/GUI/Controls/ChooseRecommendedMods.cs index 2f5e358d89..b97ce181db 100644 --- a/GUI/Controls/ChooseRecommendedMods.cs +++ b/GUI/Controls/ChooseRecommendedMods.cs @@ -42,6 +42,7 @@ public void LoadRecommendations(IRegistryQuerier registry, this.toUninstall = toUninstall; this.versionCrit = versionCrit; this.config = config; + this.game = game; Util.Invoke(this, () => { AlwaysUncheckAllButton.Checked = config?.SuppressRecommendations ?? false; @@ -132,7 +133,7 @@ private void RecommendedModsListView_ItemChecked(object? sender, ItemCheckedEven private void MarkConflicts() { - if (registry != null && versionCrit != null) + if (registry != null && versionCrit != null && game != null) { try { @@ -144,7 +145,7 @@ private void MarkConflicts() .Concat(toInstall) .Distinct(), toUninstall, - RelationshipResolverOptions.ConflictsOpts(), registry, versionCrit); + RelationshipResolverOptions.ConflictsOpts(), registry, game, versionCrit); var conflicts = resolver.ConflictList; foreach (var item in RecommendedModsListView.Items.Cast() // Apparently ListView handes AddRange by: @@ -160,12 +161,14 @@ private void MarkConflicts() RecommendedModsContinueButton.Enabled = conflicts.Count == 0; OnConflictFound?.Invoke(string.Join("; ", resolver.ConflictDescriptions)); } - catch (DependencyNotSatisfiedKraken k) + catch (DependenciesNotSatisfiedKraken k) { - var row = RecommendedModsListView.Items - .Cast() - .FirstOrDefault(it => (it?.Tag as CkanModule) == k.parent); - if (row != null) + var rows = RecommendedModsListView.Items + .OfType() + .Where(item => item.Tag is CkanModule mod + && k.unsatisfied.Any(stack => + stack.Any(rr => rr.Contains(mod)))); + foreach (var row in rows) { row.BackColor = Color.LightCoral; } @@ -307,7 +310,7 @@ private void RecommendedModsContinueButton_Click(object? sender, EventArgs? e) private HashSet toUninstall = new HashSet(); private GameVersionCriteria? versionCrit; private GUIConfiguration? config; - + private IGame? game; private TaskCompletionSource?>? task; } } diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 9f7d2ae6ef..9accd3eb49 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -1997,7 +1997,7 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi gmod.SelectedMod = ch.targetMod; } } - var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, gameVersion); + var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, inst.game, gameVersion); full_change_set = tuple.Item1.ToList(); new_conflicts = tuple.Item2.ToDictionary( item => new GUIMod(item.Key, repoData, registry, gameVersion, null, @@ -2014,14 +2014,20 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi ClearStatusBar?.Invoke(); } } - catch (DependencyNotSatisfiedKraken k) + catch (DependenciesNotSatisfiedKraken k) { - RaiseError?.Invoke(string.Format(Properties.Resources.MainDepNotSatisfied, - k.parent, k.module)); - // Uncheck the box - if (mainModList.full_list_of_mod_rows[k.parent.identifier].Tag is GUIMod gmod) + RaiseError?.Invoke(k.Message); + var identifiers = k.unsatisfied + .SelectMany(uns => uns.Select(rr => rr.source.identifier)) + .Distinct(); + + foreach (var ident in identifiers) { - gmod.SelectedMod = null; + // Uncheck the box + if (mainModList.full_list_of_mod_rows[ident].Tag is GUIMod gmod) + { + gmod.SelectedMod = null; + } } } diff --git a/GUI/Controls/ModInfoTabs/Relationships.cs b/GUI/Controls/ModInfoTabs/Relationships.cs index 3f624a7426..5a8d8e0ebf 100644 --- a/GUI/Controls/ModInfoTabs/Relationships.cs +++ b/GUI/Controls/ModInfoTabs/Relationships.cs @@ -298,7 +298,7 @@ private IEnumerable ForwardRelationships(IRegistryQuerier registry, Ck // Check if this dependency is installed if (relDescr.MatchesAny(registry.InstalledModules.Select(im => im.Module).ToList(), - registry.InstalledDlls.ToHashSet(), + registry.InstalledDlls, // Maybe it's a DLC? registry.InstalledDlc, out CkanModule? matched)) diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index 5d9c75ebe0..d58ba4799b 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -13,6 +13,7 @@ using Autofac; +using CKAN.Games; using CKAN.Versioning; using CKAN.GUI.Attributes; @@ -107,16 +108,18 @@ private static bool installable(CkanModule module, IRegistryQuerier registry) => currentInstance != null && installable(module, registry, + currentInstance.game, currentInstance.VersionCriteria()); [ForbidGUICalls] private static bool installable(CkanModule module, IRegistryQuerier registry, + IGame game, GameVersionCriteria crit) => module.IsCompatible(crit) && ModuleInstaller.CanInstall(new List() { module }, RelationshipResolverOptions.DependsOnlyOpts(), - registry, crit); + registry, game, crit); private bool allowInstall(CkanModule module) { diff --git a/GUI/Main/MainHistory.cs b/GUI/Main/MainHistory.cs index 3f14c0cbc9..155de0fa4b 100644 --- a/GUI/Main/MainHistory.cs +++ b/GUI/Main/MainHistory.cs @@ -26,6 +26,7 @@ private void InstallationHistory_Install(CkanModule[] modules) RegistryManager.Instance(CurrentInstance, repoData).registry, modules.Select(mod => new ModChange(mod, GUIModChangeType.Install)) .ToHashSet(), + CurrentInstance.game, CurrentInstance.VersionCriteria()); UpdateChangesDialog(tuple.Item1.ToList(), tuple.Item2); tabController.ShowTab("ChangesetTabPage", 1); diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 5cacda8669..f4f2041bac 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -245,7 +245,7 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) // Get full changeset (toInstall only includes user's selections, not dependencies) var crit = CurrentInstance.VersionCriteria(); var fullChangeset = new RelationshipResolver( - toInstall.Concat(toUpgrade), toUninstall, options, registry, crit + toInstall.Concat(toUpgrade), toUninstall, options, registry, CurrentInstance.game, crit ).ModList().ToList(); DownloadsFailedDialog? dfd = null; Util.Invoke(this, () => @@ -385,8 +385,8 @@ private void PostInstallMods(object? sender, RunWorkerCompletedEventArgs? e) { switch (e.Error) { - case DependencyNotSatisfiedKraken exc: - currentUser.RaiseMessage(Properties.Resources.MainInstallDepNotSatisfied, exc.parent, exc.module); + case DependenciesNotSatisfiedKraken exc: + currentUser.RaiseMessage("{0}", exc.Message); break; case ModuleNotFoundKraken exc: diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 2a67cdee0c..052e5b8bc9 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -125,7 +125,10 @@ public static SavedSearch FilterToSavedSearch(GUIModFilter filter, /// /// The version of the current game instance public Tuple, Dictionary, List> ComputeFullChangeSetFromUserChangeSet( - IRegistryQuerier registry, HashSet changeSet, GameVersionCriteria version) + IRegistryQuerier registry, + HashSet changeSet, + IGame game, + GameVersionCriteria version) { var modules_to_install = new List(); var modules_to_remove = new HashSet(); @@ -194,7 +197,7 @@ public Tuple, Dictionary, List, Dictionary, List, Dictionary, List>( @@ -452,7 +455,7 @@ private static IEnumerable rowChanges(IRegistryQuerier registry, public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVersionCriteria crit, - GameInstance? instance, + GameInstance instance, DataGridViewColumn? upgradeCol, DataGridViewColumn? replaceCol) { @@ -470,7 +473,7 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier registry, // Skip reinstalls .Where(upg => upg.Mod != upg.targetMod) .ToArray(); - if (upgrades.Length > 0 && instance != null) + if (upgrades.Length > 0) { var upgradeable = registry.CheckUpgradeable(instance, // Hold identifiers not chosen for upgrading @@ -504,7 +507,7 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier registry, return (registry == null ? modChanges : modChanges.Union( - registry.FindRemovableAutoInstalled(registry.InstalledModules.ToList(), crit) + registry.FindRemovableAutoInstalled(registry.InstalledModules.ToList(), instance.game, crit) .Select(im => new ModChange( im.Module, GUIModChangeType.Remove, new SelectionReason.NoLongerUsed())))) diff --git a/Tests/Core/Registry/CompatibilitySorter.cs b/Tests/Core/Registry/CompatibilitySorter.cs index 68e1780de6..fc2f00fa63 100644 --- a/Tests/Core/Registry/CompatibilitySorter.cs +++ b/Tests/Core/Registry/CompatibilitySorter.cs @@ -80,7 +80,7 @@ public void Constructor_OverlappingModules_HigherPriorityOverrides(string[] modu .ToDictionary(grp => grp.Key, grp => grp.ToHashSet()); var installed = new Dictionary(); - var dlls = new HashSet(); + var dlls = new Dictionary().Keys; var dlcs = new Dictionary(); var highPrio = repoData.Manager .GetAvailableModules(Enumerable.Repeat(repo1.repo, 1), diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolverTests.cs similarity index 69% rename from Tests/Core/Relationships/RelationshipResolver.cs rename to Tests/Core/Relationships/RelationshipResolverTests.cs index 3d59339d7c..e09f7f8544 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolverTests.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json.Linq; using NUnit.Framework; using Tests.Data; using CKAN; +using CKAN.Games; +using CKAN.Games.KerbalSpaceProgram; using CKAN.Versioning; using RelationshipDescriptor = CKAN.RelationshipDescriptor; @@ -18,6 +21,7 @@ public class RelationshipResolverTests { private RelationshipResolverOptions? options; private RandomModuleGenerator? generator; + private static readonly IGame game = new KerbalSpaceProgram(); private static readonly GameVersionCriteria crit = new GameVersionCriteria(null); [SetUp] @@ -35,7 +39,7 @@ public void Constructor_WithoutModules_AlwaysReturns() var registry = CKAN.Registry.Empty(); options = RelationshipResolverOptions.DefaultOpts(); Assert.DoesNotThrow(() => new RelationshipResolver(new List(), - null, options, registry, crit)); + null, options, registry, game, crit)); } [Test] @@ -56,10 +60,10 @@ public void Constructor_WithConflictingModules() var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); options!.proceed_with_inconsistencies = true; - var resolver = new RelationshipResolver(list, null, options, registry, crit); + var resolver = new RelationshipResolver(list, null, options, registry, game, crit); Assert.That(resolver.ConflictList.Any(s => Equals(s.Key, mod_a))); Assert.That(resolver.ConflictList.Any(s => Equals(s.Key, mod_b))); @@ -74,7 +78,11 @@ public void Constructor_WithConflictingModulesVersion_Throws() var mod_a = generator!.GeneratorRandomModule(); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, version=mod_a.version} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + version = mod_a.version + } }); var user = new NullUser(); @@ -86,7 +94,7 @@ public void Constructor_WithConflictingModulesVersion_Throws() var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -99,7 +107,11 @@ public void Constructor_WithConflictingModulesVersionMin_Throws(string ver, stri var mod_a = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, min_version=new ModuleVersion(conf_min)} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + min_version = new ModuleVersion(conf_min) + } }); var user = new NullUser(); @@ -111,7 +123,7 @@ public void Constructor_WithConflictingModulesVersionMin_Throws(string ver, stri var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -124,7 +136,11 @@ public void Constructor_WithConflictingModulesVersionMax_Throws(string ver, stri var mod_a = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, max_version=new ModuleVersion(conf_max)} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + max_version = new ModuleVersion(conf_max) + } }); var user = new NullUser(); @@ -136,7 +152,7 @@ public void Constructor_WithConflictingModulesVersionMax_Throws(string ver, stri var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -167,7 +183,7 @@ public void Constructor_WithConflictingModulesVersionMinMax_Throws(string ver, s var list = new List { mod_a, mod_b }; Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -180,7 +196,11 @@ public void Constructor_WithNonConflictingModulesVersion_DoesNotThrow(string ver var mod_a = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, version=new ModuleVersion(conf)} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + version = new ModuleVersion(conf) + } }); var user = new NullUser(); @@ -192,7 +212,7 @@ public void Constructor_WithNonConflictingModulesVersion_DoesNotThrow(string ver var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -204,7 +224,11 @@ public void Constructor_WithConflictingModulesVersionMin_DoesNotThrow(string ver var mod_a = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, min_version=new ModuleVersion(conf_min)} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + min_version = new ModuleVersion(conf_min) + } }); var user = new NullUser(); @@ -216,7 +240,7 @@ public void Constructor_WithConflictingModulesVersionMin_DoesNotThrow(string ver var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -228,7 +252,11 @@ public void Constructor_WithConflictingModulesVersionMax_DoesNotThrow(string ver var mod_a = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, max_version=new ModuleVersion(conf_max)} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + max_version = new ModuleVersion(conf_max) + } }); var user = new NullUser(); @@ -240,7 +268,7 @@ public void Constructor_WithConflictingModulesVersionMax_DoesNotThrow(string ver var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -253,7 +281,12 @@ public void Constructor_WithConflictingModulesVersionMinMax_DoesNotThrow(string var mod_a = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var mod_b = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier, min_version=new ModuleVersion(conf_min), max_version=new ModuleVersion(conf_max)} + new ModuleRelationshipDescriptor + { + name = mod_a.identifier, + min_version = new ModuleVersion(conf_min), + max_version = new ModuleVersion(conf_max) + } }); var user = new NullUser(); @@ -265,7 +298,7 @@ public void Constructor_WithConflictingModulesVersionMinMax_DoesNotThrow(string var list = new List { mod_a, mod_b }; Assert.DoesNotThrow(() => new RelationshipResolver( - list, null, options!, registry, crit)); + list, null, options!, registry, game, crit)); } } @@ -285,7 +318,7 @@ public void Constructor_WithMultipleModulesProviding_Throws() }); var mod_d = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name=mod_a.identifier} + new ModuleRelationshipDescriptor {name = mod_a.identifier} }); var user = new NullUser(); @@ -298,7 +331,7 @@ public void Constructor_WithMultipleModulesProviding_Throws() var list = new List { mod_d }; Assert.Throws(() => new RelationshipResolver( - list, null, options, registry, crit)); + list, null, options, registry, game, crit)); } } @@ -317,7 +350,7 @@ public void ModList_WithInstalledModules_ContainsThemWithReasonInstalled() registry.RegisterModule(mod_a, new List(), ksp.KSP, false); var relationship_resolver = new RelationshipResolver( - list, null, options!, registry, crit); + list, null, options!, registry, game, crit); CollectionAssert.Contains(relationship_resolver.ModList(), mod_a); CollectionAssert.AreEquivalent(new List { @@ -347,7 +380,7 @@ public void ModList_WithInstalledModulesSuggested_DoesNotContainThem() registry.Installed().Add(suggested.identifier, suggested.version); var list = new List { suggester }; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); CollectionAssert.Contains(relationship_resolver.ModList(), suggested); } } @@ -375,7 +408,7 @@ public void ModList_WithSuggestedModulesThatWouldConflict_DoesNotContainThem() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { suggester, mod }; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); CollectionAssert.DoesNotContain(relationship_resolver.ModList(), suggested); } } @@ -383,27 +416,27 @@ public void ModList_WithSuggestedModulesThatWouldConflict_DoesNotContainThem() [Test] public void Constructor_WithConflictingModulesInDependencies_ThrowUnderDefaultSettings() { - var dependant = generator!.GeneratorRandomModule(); + var dependent = generator!.GeneratorRandomModule(); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier} + new ModuleRelationshipDescriptor {name = dependent.identifier} }); - var conflicts_with_dependant = generator.GeneratorRandomModule(conflicts: new List + var conflicts_with_dependent = generator.GeneratorRandomModule(conflicts: new List { - new ModuleRelationshipDescriptor {name=dependant.identifier} + new ModuleRelationshipDescriptor {name = dependent.identifier} }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant), - CkanModule.ToJson(conflicts_with_dependant))) + CkanModule.ToJson(dependent), + CkanModule.ToJson(conflicts_with_dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); - var list = new List { depender, conflicts_with_dependant }; + var list = new List { depender, conflicts_with_dependent }; - Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + Assert.Throws(() => new RelationshipResolver( + list, null, options!, registry, game, crit)); } } @@ -425,13 +458,13 @@ public void Constructor_WithSuggests_HasSuggestedInModlist() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { suggester }; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); CollectionAssert.Contains(relationship_resolver.ModList(), suggested); } } [Test] - public void Constructor_ContainsSugestedOfSuggested_When_With_all_suggests() + public void Constructor_WithAllSuggests_ContainsSuggestedOfSuggested() { var suggested2 = generator!.GeneratorRandomModule(); var suggested = generator.GeneratorRandomModule( @@ -457,12 +490,12 @@ public void Constructor_ContainsSugestedOfSuggested_When_With_all_suggests() var list = new List { suggester }; options!.with_all_suggests = true; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); CollectionAssert.Contains(relationship_resolver.ModList(), suggested2); options.with_all_suggests = false; - relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); CollectionAssert.DoesNotContain(relationship_resolver.ModList(), suggested2); } } @@ -487,7 +520,7 @@ public void Constructor_ProvidesSatisfyDependencies() { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { @@ -498,12 +531,12 @@ public void Constructor_ProvidesSatisfyDependencies() } [Test] - public void Constructor_WithMissingDependants_Throws() + public void Constructor_WithMissingDependents_Throws() { - var dependant = generator!.GeneratorRandomModule(); + var dependent = generator!.GeneratorRandomModule(); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier} + new ModuleRelationshipDescriptor {name = dependent.identifier} }); var user = new NullUser(); @@ -512,9 +545,9 @@ public void Constructor_WithMissingDependants_Throws() { var registry = new CKAN.Registry(repoData.Manager, repo.repo); - Assert.Throws(() => + Assert.Throws(() => new RelationshipResolver(new List { depender }, - null, options!, registry, crit)); + null, options!, registry, game, crit)); } } @@ -524,76 +557,88 @@ public void Constructor_WithMissingDependants_Throws() [TestCase("1.0", "0.2")] [TestCase("0", "0.2")] [TestCase("1.0", "0")] - public void Constructor_WithMissingDependantsVersion_Throws(string ver, string dep) + public void Constructor_WithMissingDependentsVersion_Throws(string ver, string dep) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier, version = new ModuleVersion(dep)} + new ModuleRelationshipDescriptor + { + name = dependent.identifier, + version = new ModuleVersion(dep) + } }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant))) + CkanModule.ToJson(dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + Assert.Throws(() => new RelationshipResolver( + list, null, options!, registry, game, crit)); } } [Test] [Category("Version")] [TestCase("1.0", "2.0")] - public void Constructor_WithMissingDependantsVersionMin_Throws(string ver, string dep_min) + public void Constructor_WithMissingDependentsVersionMin_Throws(string ver, string dep_min) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier, min_version = new ModuleVersion(dep_min)} + new ModuleRelationshipDescriptor + { + name = dependent.identifier, + min_version = new ModuleVersion(dep_min) + } }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant))) + CkanModule.ToJson(dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List() { depender }; - Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); - list.Add(dependant); - Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + Assert.Throws(() => new RelationshipResolver( + list, null, options!, registry, game, crit)); + list.Add(dependent); + Assert.Throws(() => new RelationshipResolver( + list, null, options!, registry, game, crit)); } } [Test] [Category("Version")] [TestCase("1.0", "0.5")] - public void Constructor_WithMissingDependantsVersionMax_Throws(string ver, string dep_max) + public void Constructor_WithMissingDependentsVersionMax_Throws(string ver, string dep_max) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier, max_version = new ModuleVersion(dep_max)} + new ModuleRelationshipDescriptor + { + name = dependent.identifier, + max_version = new ModuleVersion(dep_max) + } }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant))) + CkanModule.ToJson(dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); - var list = new List { depender, dependant }; + var list = new List { depender, dependent }; - Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + Assert.Throws(() => new RelationshipResolver( + list, null, options!, registry, game, crit)); } } @@ -601,14 +646,14 @@ public void Constructor_WithMissingDependantsVersionMax_Throws(string ver, strin [Category("Version")] [TestCase("1.0", "2.0", "3.0")] [TestCase("4.0", "2.0", "3.0")] - public void Constructor_WithMissingDependantsVersionMinMax_Throws(string ver, string dep_min, string dep_max) + public void Constructor_WithMissingDependentsVersionMinMax_Throws(string ver, string dep_min, string dep_max) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); var depender = generator.GeneratorRandomModule(depends: new List { new ModuleRelationshipDescriptor { - name = dependant.identifier, + name = dependent.identifier, min_version = new ModuleVersion(dep_min), max_version = new ModuleVersion(dep_max) } @@ -616,14 +661,14 @@ public void Constructor_WithMissingDependantsVersionMinMax_Throws(string ver, st var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant))) + CkanModule.ToJson(dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); - var list = new List { depender, dependant }; + var list = new List { depender, dependent }; - Assert.Throws(() => new RelationshipResolver( - list, null, options!, registry, crit)); + Assert.Throws(() => new RelationshipResolver( + list, null, options!, registry, game, crit)); } } @@ -631,29 +676,33 @@ public void Constructor_WithMissingDependantsVersionMinMax_Throws(string ver, st [Category("Version")] [TestCase("1.0", "1.0", "2.0")] [TestCase("1.0", "1.0", "0.5")]//what to do if a mod is present twice with the same version ? - public void Constructor_WithDependantVersion_ChooseCorrectly(string ver, string dep, string other) + public void Constructor_WithDependentVersion_ChooseCorrectly(string ver, string dep, string other) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); - var other_dependant = generator.GeneratorRandomModule(identifier: dependant.identifier, version: new ModuleVersion(other)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var other_dependent = generator.GeneratorRandomModule(identifier: dependent.identifier, version: new ModuleVersion(other)); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier, version = new ModuleVersion(dep)} + new ModuleRelationshipDescriptor + { + name = dependent.identifier, + version = new ModuleVersion(dep) + } }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant), - CkanModule.ToJson(other_dependant))) + CkanModule.ToJson(dependent), + CkanModule.ToJson(other_dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { - dependant, + dependent, depender }); } @@ -664,29 +713,33 @@ public void Constructor_WithDependantVersion_ChooseCorrectly(string ver, string [TestCase("2.0", "1.0", "0.5")] [TestCase("2.0", "1.0", "1.5")] [TestCase("2.0", "2.0", "0.5")] - public void Constructor_WithDependantVersionMin_ChooseCorrectly(string ver, string dep_min, string other) + public void Constructor_WithDependentVersionMin_ChooseCorrectly(string ver, string dep_min, string other) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); - var other_dependant = generator.GeneratorRandomModule(identifier: dependant.identifier, version: new ModuleVersion(other)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var other_dependent = generator.GeneratorRandomModule(identifier: dependent.identifier, version: new ModuleVersion(other)); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier, min_version = new ModuleVersion(dep_min)} + new ModuleRelationshipDescriptor + { + name = dependent.identifier, + min_version = new ModuleVersion(dep_min) + } }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant), - CkanModule.ToJson(other_dependant))) + CkanModule.ToJson(dependent), + CkanModule.ToJson(other_dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { - dependant, + dependent, depender }); } @@ -697,29 +750,33 @@ public void Constructor_WithDependantVersionMin_ChooseCorrectly(string ver, stri [TestCase("2.0", "2.0", "0.5")] [TestCase("2.0", "3.0", "0.5")] [TestCase("2.0", "3.0", "4.0")] - public void Constructor_WithDependantVersionMax_ChooseCorrectly(string ver, string dep_max, string other) + public void Constructor_WithDependentVersionMax_ChooseCorrectly(string ver, string dep_max, string other) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); - var other_dependant = generator.GeneratorRandomModule(identifier: dependant.identifier, version: new ModuleVersion(other)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var other_dependent = generator.GeneratorRandomModule(identifier: dependent.identifier, version: new ModuleVersion(other)); var depender = generator.GeneratorRandomModule(depends: new List { - new ModuleRelationshipDescriptor {name = dependant.identifier, max_version = new ModuleVersion(dep_max)} + new ModuleRelationshipDescriptor + { + name = dependent.identifier, + max_version = new ModuleVersion(dep_max) + } }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant), - CkanModule.ToJson(other_dependant))) + CkanModule.ToJson(dependent), + CkanModule.ToJson(other_dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { - dependant, + dependent, depender }); } @@ -730,16 +787,16 @@ public void Constructor_WithDependantVersionMax_ChooseCorrectly(string ver, stri [TestCase("2.0", "1.0", "3.0", "0.5")] [TestCase("2.0", "1.0", "3.0", "1.5")] [TestCase("2.0", "1.0", "3.0", "3.5")] - public void Constructor_WithDependantVersionMinMax_ChooseCorrectly(string ver, string dep_min, string dep_max, string other) + public void Constructor_WithDependentVersionMinMax_ChooseCorrectly(string ver, string dep_min, string dep_max, string other) { - var dependant = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); - var other_dependant = generator.GeneratorRandomModule(identifier: dependant.identifier, version: new ModuleVersion(other)); + var dependent = generator!.GeneratorRandomModule(version: new ModuleVersion(ver)); + var other_dependent = generator.GeneratorRandomModule(identifier: dependent.identifier, version: new ModuleVersion(other)); var depender = generator.GeneratorRandomModule(depends: new List { new ModuleRelationshipDescriptor { - name = dependant.identifier, + name = dependent.identifier, min_version = new ModuleVersion(dep_min), max_version = new ModuleVersion(dep_max) } @@ -747,17 +804,17 @@ public void Constructor_WithDependantVersionMinMax_ChooseCorrectly(string ver, s var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(depender), - CkanModule.ToJson(dependant), - CkanModule.ToJson(other_dependant))) + CkanModule.ToJson(dependent), + CkanModule.ToJson(other_dependent))) using (var repoData = new TemporaryRepositoryData(user, repo.repo)) { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { depender }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); CollectionAssert.AreEquivalent(relationship_resolver.ModList(), new List { - dependant, + dependent, depender }); } @@ -811,7 +868,7 @@ public void Constructor_ReverseDependencyDoesntMatchLatest_ChoosesOlderVersion() // Act RelationshipResolver rr = new RelationshipResolver( new CkanModule[] { depender }, null, - options!, registry, crit); + options!, registry, game, crit); // Assert CollectionAssert.Contains( rr.ModList(), olderDependency); @@ -862,7 +919,7 @@ public void Constructor_ReverseDependencyConflictsLatest_ChoosesOlderVersion() // Act RelationshipResolver rr = new RelationshipResolver( new CkanModule[] { depender }, null, - options!, registry, crit); + options!, registry, game, crit); // Assert CollectionAssert.Contains( rr.ModList(), olderDependency); @@ -881,7 +938,7 @@ public void ReasonFor_WithModsNotInList_Empty() { var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { mod }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); var mod_not_in_resolver_list = generator.GeneratorRandomModule(); CollectionAssert.DoesNotContain(relationship_resolver.ModList(), mod_not_in_resolver_list); @@ -901,7 +958,7 @@ public void ReasonFor_WithUserAddedMods_GivesReasonUserAdded() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { mod }; - var relationship_resolver = new RelationshipResolver(list, null, options!, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options!, registry, game, crit); var reasons = relationship_resolver.ReasonsFor(mod); Assert.That(reasons[0], Is.AssignableTo()); } @@ -911,9 +968,14 @@ public void ReasonFor_WithUserAddedMods_GivesReasonUserAdded() public void ReasonFor_WithSuggestedMods_GivesCorrectParent() { var suggested = generator!.GeneratorRandomModule(); - var mod = - generator.GeneratorRandomModule(suggests: - new List {new ModuleRelationshipDescriptor {name = suggested.identifier}}); + var mod = generator.GeneratorRandomModule(suggests: + new List + { + new ModuleRelationshipDescriptor + { + name = suggested.identifier + } + }); var user = new NullUser(); using (var repo = new TemporaryRepository(CkanModule.ToJson(mod), @@ -924,7 +986,7 @@ public void ReasonFor_WithSuggestedMods_GivesCorrectParent() var list = new List { mod }; options!.with_all_suggests = true; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); var reasons = relationship_resolver.ReasonsFor(suggested); Assert.IsTrue(reasons[0] is SelectionReason.Suggested sug @@ -962,7 +1024,7 @@ public void ReasonFor_WithTreeOfMods_GivesCorrectParents() options!.with_all_suggests = true; options.with_recommends = true; - var relationship_resolver = new RelationshipResolver(list, null, options, registry, crit); + var relationship_resolver = new RelationshipResolver(list, null, options, registry, game, crit); var reasons = relationship_resolver.ReasonsFor(recommendedA); Assert.IsTrue(reasons[0] is SelectionReason.Recommended rec && rec.Parent.Equals(suggested)); @@ -999,7 +1061,7 @@ public void AutodetectedCanSatisfyRelationships() new RelationshipResolver( new CkanModule[] { mod }, null, RelationshipResolverOptions.DefaultOpts(), - registry, new GameVersionCriteria(GameVersion.Parse("1.0.0"))); + registry, game, new GameVersionCriteria(GameVersion.Parse("1.0.0"))); } } @@ -1067,7 +1129,7 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv options!.proceed_with_inconsistencies = false; var exception = Assert.Throws(() => { - resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, crit); + resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, game, crit); }); Assert.AreEqual($"{avp} conflicts with {eveDefaultConfig}", exception?.ShortDescription); @@ -1078,7 +1140,7 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv options.proceed_with_inconsistencies = true; Assert.DoesNotThrow(() => { - resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, crit); + resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, game, crit); }); CollectionAssert.AreEquivalent(modulesToInstall, resolver?.ConflictList.Keys); @@ -1094,11 +1156,220 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv options.proceed_with_inconsistencies = false; Assert.DoesNotThrow(() => { - resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, crit); + resolver = new RelationshipResolver(modulesToInstall, modulesToRemove, options, registry, game, crit); }); Assert.IsEmpty(resolver!.ConflictList); CollectionAssert.AreEquivalent(new List {avp, avp2kTextures}, resolver.ModList()); } } + + [Test, + TestCase(new string[] { + @"{ + ""identifier"": ""PopularMod"", + ""ksp_version"": ""1.10.0"" + }", + }, + @"{ + ""identifier"": ""MyModpack"", + ""kind"": ""metapackage"", + ""depends"": [ { ""name"": ""PopularMod"" } ] + }", + "Unsatisfied dependency PopularMod (KSP 1.10.0) needed for: MyModpack 1.0"), + TestCase(new string[] { + @"{ + ""identifier"": ""IncompatibleDependency"", + ""ksp_version"": ""1.11.0"" + }", + @"{ + ""identifier"": ""CompatibleDepending1"", + ""ksp_version"": ""1.12"", + ""depends"": [ { ""name"": ""IncompatibleDependency"" } ] + }", + @"{ + ""identifier"": ""CompatibleDepending2"", + ""ksp_version"": ""1.12"", + ""depends"": [ { ""name"": ""IncompatibleDependency"" } ] + }", + @"{ + ""identifier"": ""CompatibleDepending3"", + ""ksp_version"": ""1.12"", + ""depends"": [ { ""name"": ""CompatibleDepending2"" } ] + }", + }, + @"{ + ""identifier"": ""MyModpack"", + ""kind"": ""metapackage"", + ""depends"": [ { ""name"": ""CompatibleDepending1"" }, + { ""name"": ""CompatibleDepending3"" } ] + }", + "Unsatisfied dependency IncompatibleDependency (KSP 1.11.0) needed for: CompatibleDepending1 1.0 (needed for MyModpack 1.0); CompatibleDepending2 1.0 (needed for CompatibleDepending3 1.0, needed for MyModpack 1.0)"), + ] + public void Constructor_ModpackWithIncompatibleDepends_Throws(string[] availableModules, + string modpackModule, + string exceptionMessage) + { + // Arrange + var user = new NullUser(); + var game = new KerbalSpaceProgram(); + var crit = new GameVersionCriteria(new GameVersion(1, 12, 5)); + var modpack = CkanModule.FromJson(MergeWithDefaults(modpackModule)); + using (var repo = new TemporaryRepository(availableModules.Select(MergeWithDefaults) + .ToArray())) + using (var repoData = new TemporaryRepositoryData(user, repo.repo)) + { + var registry = new CKAN.Registry(repoData.Manager, repo.repo); + + // Act / Assert + var exc = Assert.Throws(() => + { + var rr = new RelationshipResolver( + Enumerable.Repeat(modpack, 1), null, + RelationshipResolverOptions.DependsOnlyOpts(), + registry, game, crit); + }); + Assert.AreEqual(exceptionMessage, exc?.Message); + } + } + + [Test, + TestCase(new string[] { + @"{ + ""identifier"": ""WildBlueTools"", + ""depends"": [ { ""name"": ""WildBlue-PlayMode"" } ] + }", + @"{ + ""identifier"": ""WildBlue-PlayMode-CRP"", + ""provides"": [ ""WildBlue-PlayMode"" ], + ""conflicts"": [ { ""name"": ""WildBlue-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlueTools"" } ] + }", + @"{ + ""identifier"": ""WildBlue-PlayMode-ClassicStock"", + ""provides"": [ ""WildBlue-PlayMode"" ], + ""conflicts"": [ { ""name"": ""WildBlue-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlueTools"" } ] + }", + @"{ + ""identifier"": ""Heisenberg"", + ""depends"": [ { ""name"": ""Heisenberg-PlayMode"" }, + { ""name"": ""WildBlueTools"" } ] + }", + @"{ + ""identifier"": ""Heisenberg-PlayMode-CRP"", + ""provides"": [ ""Heisenberg-PlayMode"" ], + ""conflicts"": [ { ""name"": ""Heisenberg-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlue-PlayMode-CRP"" }, + { ""name"": ""Heisenberg"" } ] + }", + @"{ + ""identifier"": ""Heisenberg-PlayMode-ClassicStock"", + ""provides"": [ ""Heisenberg-PlayMode"" ], + ""conflicts"": [ { ""name"": ""Heisenberg-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlue-PlayMode-ClassicStock"" }, + { ""name"": ""Heisenberg"" } ] + }", + @"{ + ""identifier"": ""DSEV"", + ""depends"": [ { ""name"": ""DSEV-PlayMode"" }, + { ""name"": ""WildBlueTools"" } ] + }", + @"{ + ""identifier"": ""DSEV-PlayMode-CRP"", + ""provides"": [ ""DSEV-PlayMode"" ], + ""conflicts"": [ { ""name"": ""DSEV-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlue-PlayMode-CRP"" }, + { ""name"": ""DSEV"" } ] + }", + @"{ + ""identifier"": ""DSEV-PlayMode-ClassicStock"", + ""provides"": [ ""DSEV-PlayMode"" ], + ""conflicts"": [ { ""name"": ""DSEV-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlue-PlayMode-ClassicStock"" }, + { ""name"": ""DSEV"" } ] + }", + @"{ + ""identifier"": ""Pathfinder"", + ""depends"": [ { ""name"": ""Pathfinder-PlayMode"" }, + { ""name"": ""WildBlueTools"" } ] + }", + @"{ + ""identifier"": ""Pathfinder-PlayMode-CRP"", + ""provides"": [ ""Pathfinder-PlayMode"" ], + ""conflicts"": [ { ""name"": ""Pathfinder-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlue-PlayMode-CRP"" }, + { ""name"": ""Pathfinder"" } ] + }", + @"{ + ""identifier"": ""Pathfinder-PlayMode-ClassicStock"", + ""provides"": [ ""Pathfinder-PlayMode"" ], + ""conflicts"": [ { ""name"": ""Pathfinder-PlayMode"" } ], + ""depends"": [ { ""name"": ""WildBlue-PlayMode-ClassicStock"" }, + { ""name"": ""Pathfinder"" } ] + }", + }, + new string[] { "Heisenberg-PlayMode-ClassicStock", "DSEV", "Pathfinder" }, + new string[] { "DSEV-PlayMode-", "Pathfinder-PlayMode-", "WildBlue-PlayMode-" }, + new string[] { "CRP" }), + ] + public void Constructor_WithPlayModes_DoesNotThrow(string[] availableModules, + string[] installIdents, + string[] goodSubstrings, + string[] badSubstrings) + { + var user = new NullUser(); + var game = new KerbalSpaceProgram(); + var crit = new GameVersionCriteria(new GameVersion(1, 12, 5)); + using (var repo = new TemporaryRepository(availableModules.Select(MergeWithDefaults) + .ToArray())) + using (var repoData = new TemporaryRepositoryData(user, repo.repo)) + { + var registry = new CKAN.Registry(repoData.Manager, repo.repo); + + // Act / Assert + Assert.DoesNotThrow(() => + { + var rr = new RelationshipResolver( + installIdents.Select(ident => registry.LatestAvailable(ident, crit)) + .OfType(), + null, + RelationshipResolverOptions.DependsOnlyOpts(), + registry, + game, + crit); + var idents = rr.ModList().Select(m => m.identifier).ToArray(); + foreach (var goodSubstring in goodSubstrings) + { + Assert.IsTrue(idents.Any(ident => ident.Contains(goodSubstring)), + $"Some identifier containing {goodSubstring} must be in resolver"); + } + foreach (var ident in idents) + { + foreach (var badSubstring in badSubstrings) + { + Assert.IsFalse(ident.Contains(badSubstring), + $"No identifiers containing {badSubstring} should be in resolver"); + } + } + }); + } + } + + private static string MergeWithDefaults(string json) + { + var incoming = JObject.Parse(json); + incoming.Merge(moduleDefaults); + return incoming.ToString(); + } + + // Unimportant required fields that we don't want to duplicate + private static readonly JObject moduleDefaults = JObject.Parse( + @"{ + ""spec_version"": ""v1.34"", + ""name"": ""A mod or modpack"", + ""author"": ""An author"", + ""version"": ""1.0"", + ""download"": ""https://www.nonexistent.com/download"" + }"); } } diff --git a/Tests/Core/Relationships/SanityChecker.cs b/Tests/Core/Relationships/SanityChecker.cs index 2132225fa9..e99e6fe48b 100644 --- a/Tests/Core/Relationships/SanityChecker.cs +++ b/Tests/Core/Relationships/SanityChecker.cs @@ -83,7 +83,7 @@ public void CustomBiomes() public void CustomBiomesWithDlls() { var mods = new List(); - var dlls = new List { "CustomBiomes" }; + var dlls = new Dictionary { { "CustomBiomes", "" } }.Keys; Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, dlls), "CustomBiomes dll by itself"); @@ -100,7 +100,7 @@ public void CustomBiomesWithDlls() public void ConflictWithDll() { var mods = new List { registry?.LatestAvailable("SRL", null)! }; - var dlls = new List { "QuickRevert" }; + var dlls = new Dictionary { { "QuickRevert", "" } }.Keys; Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods), "SRL can be installed by itself"); Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods, dlls), "SRL conflicts with QuickRevert DLL"); @@ -110,7 +110,7 @@ public void ConflictWithDll() public void FindUnsatisfiedDepends() { var mods = new List(); - var dlls = Enumerable.Empty().ToHashSet(); + var dlls = new Dictionary().Keys; var dlc = new Dictionary(); Assert.IsEmpty(CKAN.SanityChecker.FindUnsatisfiedDepends(mods, dlls, dlc), "Empty list"); @@ -363,14 +363,14 @@ public void IsConsistent_MultipleVersionsOfSelfProvidesConflictingModule_Consist Assert.IsTrue(CKAN.SanityChecker.IsConsistent(modules)); } - private static void TestDepends(List to_remove, - HashSet mods, - HashSet? dlls, - Dictionary? dlc, - List expected, - string message) + private static void TestDepends(List to_remove, + HashSet mods, + ICollection? dlls, + Dictionary? dlc, + List expected, + string message) { - dlls ??= new HashSet(); + dlls ??= new Dictionary().Keys; var remove_count = to_remove.Count; var dll_count = dlls.Count; diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index d15c250bdf..a409a09183 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -15,6 +15,7 @@ using CKAN; using CKAN.Versioning; using CKAN.GUI; +using CKAN.Games.KerbalSpaceProgram; namespace Tests.GUI { @@ -30,7 +31,9 @@ public class ModListTests public void ComputeFullChangeSetFromUserChangeSet_WithEmptyList_HasEmptyChangeSet() { var item = new ModList(); - Assert.That(item.ComputeUserChangeSet(Registry.Empty(), crit, null, null, null), Is.Empty); + var game = new KerbalSpaceProgram(); + var inst = new GameInstance(game, "/", "dummy", new NullUser()); + Assert.That(item.ComputeUserChangeSet(Registry.Empty(), crit, inst, null, null), Is.Empty); } [Test] @@ -210,11 +213,14 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() Assert.IsTrue(otherModule.SelectedMod == otherModule.LatestAvailableMod); Assert.IsFalse(otherModule.IsInstalled); + var game = new KerbalSpaceProgram(); + var inst = new GameInstance(game, "/", "dummy", new NullUser()); + Assert.DoesNotThrow(() => { // Install the "other" module installer.InstallList( - modList.ComputeUserChangeSet(Registry.Empty(), crit, null, null, null).Select(change => change.Mod).ToList(), + modList.ComputeUserChangeSet(Registry.Empty(), crit, inst, null, null).Select(change => change.Mod).ToList(), new RelationshipResolverOptions(), registryManager, ref possibleConfigOnlyDirs,