diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 2feac6f5c9..d11e3c5f1f 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -1048,7 +1048,7 @@ public void Upgrade(IEnumerable modules, var resolver = new RelationshipResolver( modules, modules.Select(m => registry.InstalledModule(m.identifier)?.Module).Where(m => m != null), - RelationshipResolver.DependsOnlyOpts(), + RelationshipResolverOptions.DependsOnlyOpts(), registry, ksp.VersionCriteria() ); @@ -1292,20 +1292,6 @@ private void DownloadModules(IEnumerable mods, IDownloader downloade } } - private static readonly RelationshipResolverOptions RecommenderOptions = new RelationshipResolverOptions() - { - // Only look at depends - with_recommends = false, - - // Don't throw anything - without_toomanyprovides_kraken = true, - without_enforce_consistency = true, - proceed_with_inconsistencies = true, - - // Skip relationships with suppress_recommendations==true - get_recommenders = true, - }; - /// /// Looks for optional related modules that could be installed alongside the given modules /// @@ -1317,148 +1303,50 @@ private void DownloadModules(IEnumerable mods, IDownloader downloade /// /// true if anything found, false otherwise /// - public bool FindRecommendations( - HashSet sourceModules, - List toInstall, - Registry registry, - out Dictionary>> recommendations, - out Dictionary> suggestions, - out Dictionary> supporters) + public bool FindRecommendations(HashSet sourceModules, + List toInstall, + Registry registry, + out Dictionary>> recommendations, + out Dictionary> suggestions, + out Dictionary> supporters) { - // Get all dependencies except where suppress_recommendations==true - var resolver = new RelationshipResolver(sourceModules, null, RecommenderOptions, - registry, ksp.VersionCriteria()); - var recommenders = resolver.ModList().ToList(); - - var dependersIndex = getDependersIndex(recommenders, registry, toInstall); - var instList = toInstall.ToList(); - recommendations = new Dictionary>>(); - suggestions = new Dictionary>(); - supporters = new Dictionary>(); - foreach (CkanModule mod in recommenders.Where(m => m.recommends != null)) - { - foreach (RelationshipDescriptor rel in mod.recommends) - { - List providers = rel.LatestAvailableWithProvides( - registry, ksp.VersionCriteria()); - int i = 0; - foreach (CkanModule provider in providers) - { - if (!registry.IsInstalled(provider.identifier) - && !toInstall.Any(m => m.identifier == provider.identifier) - && dependersIndex.TryGetValue(provider, out List dependers) - && (provider.IsDLC || CanInstall(RelationshipResolver.DependsOnlyOpts(), - instList.Concat(new List() { provider }).ToList(), registry))) - { - dependersIndex.Remove(provider); - recommendations.Add( - provider, - new Tuple>( - !provider.IsDLC && (i == 0 || provider.identifier == (rel as ModuleRelationshipDescriptor)?.name), - dependers)); - ++i; - } - } - } - } - foreach (CkanModule mod in recommenders.Where(m => m.suggests != null)) - { - foreach (RelationshipDescriptor rel in mod.suggests) - { - List providers = rel.LatestAvailableWithProvides( - registry, ksp.VersionCriteria()); - foreach (CkanModule provider in providers) - { - if (!registry.IsInstalled(provider.identifier) - && !toInstall.Any(m => m.identifier == provider.identifier) - && dependersIndex.TryGetValue(provider, out List dependers) - && (provider.IsDLC || CanInstall(RelationshipResolver.DependsOnlyOpts(), - instList.Concat(new List() { provider }).ToList(), registry))) - { - dependersIndex.Remove(provider); - suggestions.Add(provider, dependers); - } - } - } - } - - // Find installable modules with "supports" relationships - var candidates = registry.CompatibleModules(ksp.VersionCriteria()) - .Where(mod => !registry.IsInstalled(mod.identifier) - && !toInstall.Any(m => m.identifier == mod.identifier)) - .Where(m => m?.supports != null) - .Except(recommendations.Keys) - .Except(suggestions.Keys); - // Find each module that "supports" something we're installing - foreach (CkanModule mod in candidates) - { - foreach (RelationshipDescriptor rel in mod.supports) - { - if (rel.MatchesAny(recommenders, null, null)) - { - var name = (rel as ModuleRelationshipDescriptor)?.name; - if (!string.IsNullOrEmpty(name)) - { - if (supporters.TryGetValue(mod, out HashSet others)) - { - others.Add(name); - } - else - { - supporters.Add(mod, new HashSet() { name }); - } - } - } - } - } - supporters.RemoveWhere(kvp => - !CanInstall( - RelationshipResolver.DependsOnlyOpts(), - instList.Concat(new List() { kvp.Key }).ToList(), - registry)); - - return recommendations.Any() || suggestions.Any() || supporters.Any(); - } - - // Build up the list of who recommends what - private Dictionary> getDependersIndex( - IEnumerable sourceModules, - IRegistryQuerier registry, - List toExclude) - { - Dictionary> dependersIndex = new Dictionary>(); - foreach (CkanModule mod in sourceModules) - { - foreach (List relations in new List>() { mod.recommends, mod.suggests }) - { - if (relations != null) - { - foreach (RelationshipDescriptor rel in relations) - { - List providers = rel.LatestAvailableWithProvides( - registry, ksp.VersionCriteria()); - foreach (CkanModule provider in providers) - { - if (!registry.IsInstalled(provider.identifier) - && !toExclude.Any(m => m.identifier == provider.identifier)) - { - if (dependersIndex.TryGetValue(provider, out List dependers)) - { - // Add the dependent mod to the list of reasons this dependency is shown. - dependers.Add(mod.identifier); - } - else - { - // Add a new entry if this provider isn't listed yet. - dependersIndex.Add(provider, new List() { mod.identifier }); - } - } - } - } - } - } - } - return dependersIndex; + var crit = ksp.VersionCriteria(); + var resolver = new RelationshipResolver(sourceModules, null, + RelationshipResolverOptions.KitchenSinkOpts(), + registry, crit); + var recommenders = resolver.Dependencies().ToHashSet(); + + recommendations = resolver.Recommendations(recommenders) + .ToDictionary(m => m, + m => new Tuple>( + resolver.ReasonsFor(m) + .Any(r => (r as SelectionReason.Recommended) + ?.ProvidesIndex == 0), + resolver.ReasonsFor(m) + .Where(r => r is SelectionReason.Recommended rec + && recommenders.Contains(rec.Parent)) + .Select(r => r.Parent.identifier) + .ToList())); + suggestions = resolver.Suggestions(recommenders, + recommendations.Keys.ToList()) + .ToDictionary(m => m, + m => resolver.ReasonsFor(m) + .Where(r => r is SelectionReason.Suggested sug + && recommenders.Contains(sug.Parent)) + .Select(r => r.Parent.identifier) + .ToList()); + + var opts = RelationshipResolverOptions.DependsOnlyOpts(); + supporters = resolver.Supporters(recommenders, + toInstall.Concat(recommendations.Keys) + .Concat(suggestions.Keys)) + .Where(kvp => CanInstall(toInstall.Append(kvp.Key).ToList(), + opts, registry, crit)) + .ToDictionary(); + + return recommendations.Count > 0 + || suggestions.Count > 0 + || supporters.Count > 0; } /// @@ -1471,27 +1359,23 @@ private Dictionary> getDependersIndex( /// /// True if it's possible to install these mods, false otherwise /// - public bool CanInstall( - RelationshipResolverOptions opts, - List toInstall, - IRegistryQuerier registry - ) + public bool CanInstall(List toInstall, + RelationshipResolverOptions opts, + IRegistryQuerier registry, + GameVersionCriteria crit) { - string request = toInstall.Select(m => m.identifier).Aggregate((a, b) => $"{a}, {b}"); + string request = string.Join(", ", toInstall.Select(m => m.identifier)); try { - RelationshipResolver resolver = new RelationshipResolver( - toInstall, - toInstall.Select(m => registry.InstalledModule(m.identifier)?.Module).Where(m => m != null), - opts, registry, ksp.VersionCriteria() - ); + var installed = toInstall.Select(m => registry.InstalledModule(m.identifier)?.Module) + .Where(m => m != null); + var resolver = new RelationshipResolver(toInstall, installed, opts, registry, crit); - if (resolver.ModList().Count() >= toInstall.Count(m => !m.IsMetapackage)) + var resolverModList = resolver.ModList().ToList(); + if (resolverModList.Count >= toInstall.Count(m => !m.IsMetapackage)) { // We can install with no further dependencies - string recipe = resolver.ModList() - .Select(m => m.identifier) - .Aggregate((a, b) => $"{a}, {b}"); + string recipe = string.Join(", ", resolverModList.Select(m => m.identifier)); log.Debug($"Installable: {request}: {recipe}"); return true; } @@ -1504,10 +1388,10 @@ IRegistryQuerier registry catch (TooManyModsProvideKraken k) { // One of the dependencies is virtual - foreach (CkanModule mod in k.modules) + foreach (var mod in k.modules) { // Try each option recursively to see if any are successful - if (CanInstall(opts, toInstall.Concat(new List() { mod }).ToList(), registry)) + if (CanInstall(toInstall.Append(mod).ToList(), opts, registry, crit)) { // Child call will emit debug output, so we don't need to here return true; diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 683aa34529..fa3b56ee20 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -362,7 +362,7 @@ private static IEnumerable FindRemovableAutoInstalled( var autoInstIds = autoInstMods.Select(im => im.Module.identifier).ToHashSet(); // Need to get the full changeset for this to work as intended - RelationshipResolverOptions opts = RelationshipResolver.DependsOnlyOpts(); + RelationshipResolverOptions opts = RelationshipResolverOptions.DependsOnlyOpts(); opts.without_toomanyprovides_kraken = true; opts.without_enforce_consistency = true; opts.proceed_with_inconsistencies = true; diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index ebc8d4ea99..7756af6bd5 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -69,30 +69,6 @@ private RelationshipResolver(RelationshipResolverOptions options, } } - /// - /// Returns the default options for relationship resolution. - /// - public static RelationshipResolverOptions DefaultOpts() - // TODO: This should just be able to return a new RelationshipResolverOptions - // and the defaults in the class definition should do the right thing. - => new RelationshipResolverOptions - { - with_recommends = true, - with_suggests = false, - with_all_suggests = false, - }; - - /// - /// Options to install without recommendations. - /// - public static RelationshipResolverOptions DependsOnlyOpts() - => new RelationshipResolverOptions - { - with_recommends = false, - with_suggests = false, - with_all_suggests = false, - }; - /// /// Add modules to consideration of the relationship resolver. /// @@ -190,15 +166,10 @@ private void Resolve(CkanModule module, alreadyResolved.Add(module); } - // Even though we may resolve top-level suggests for our module, - // we don't install suggestions all the way down unless with_all_suggests - // is true. - var sub_options = options.WithoutSuggestions(); - old_stanza = old_stanza?.Memoize(); log.DebugFormat("Resolving dependencies for {0}", module.identifier); - ResolveStanza(module.depends, new SelectionReason.Depends(module), sub_options, false, old_stanza); + ResolveStanza(module.depends, new SelectionReason.Depends(module), options, false, old_stanza); // TODO: RR currently conducts a depth-first resolution of requirements. While we do the // right thing in processing all dependencies first, then recommends, and then suggests, @@ -211,13 +182,19 @@ private void Resolve(CkanModule module, if (options.with_recommends) { log.DebugFormat("Resolving recommends for {0}", module.identifier); - ResolveStanza(module.recommends, new SelectionReason.Recommended(module), sub_options, true, old_stanza); + ResolveStanza(module.recommends, new SelectionReason.Recommended(module, 0), + options.get_recommenders ? options.WithoutRecommendations() + : options.WithoutSuggestions(), + true, old_stanza); } if (options.with_suggests || options.with_all_suggests) { log.DebugFormat("Resolving suggests for {0}", module.identifier); - ResolveStanza(module.suggests, new SelectionReason.Suggested(module), sub_options, true, old_stanza); + ResolveStanza(module.suggests, new SelectionReason.Suggested(module), + options.get_recommenders ? options.WithoutRecommendations() + : options.WithoutSuggestions(), + true, old_stanza); } } @@ -252,7 +229,7 @@ private void ResolveStanza(List stanza, if (options.get_recommenders && descriptor.suppress_recommendations) { - log.DebugFormat("Skipping {0} because get_recommenders option is set"); + log.DebugFormat("Skipping {0} because get_recommenders option is set", descriptor.ToString()); continue; } options = orig_options.OptionsFor(descriptor); @@ -339,10 +316,10 @@ private void ResolveStanza(List stanza, { if (!soft_resolve) { - log.InfoFormat("Dependency on {0} found but it is not listed in the index, or not available for your version of KSP.", descriptor.ToString()); + 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(reason.Parent, descriptor.ToString()); } - log.InfoFormat("{0} is recommended/suggested but it is not listed in the index, or not available for your version of KSP.", descriptor.ToString()); + 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) @@ -350,6 +327,16 @@ private void ResolveStanza(List stanza, // Oh no, too many to pick from! if (options.without_toomanyprovides_kraken) { + if (options.get_recommenders) + { + for (int i = 0; i < candidates.Count; ++i) + { + var cand = candidates[i]; + Add(cand, reason is SelectionReason.Recommended rec + ? rec.WithIndex(i) + : reason); + } + } continue; } @@ -388,25 +375,22 @@ private void ResolveStanza(List stanza, Add(candidate, reason); Resolve(candidate, options, stanza); } + else if (options.proceed_with_inconsistencies) + { + Add(candidate, reason); + conflicts.Add(new ModPair(conflicting_mod, candidate)); + conflicts.Add(new ModPair(candidate, conflicting_mod)); + } else if (soft_resolve) { log.InfoFormat("{0} would cause conflicts, excluding it from consideration", candidate); } else { - if (options.proceed_with_inconsistencies) - { - Add(candidate, reason); - conflicts.Add(new ModPair(conflicting_mod, candidate)); - conflicts.Add(new ModPair(candidate, conflicting_mod)); - } - else - { - throw new InconsistentKraken(string.Format( - Properties.Resources.RelationshipResolverConflictsWith, - conflictingModDescription(conflicting_mod, null), - conflictingModDescription(candidate, reason.Parent))); - } + throw new InconsistentKraken(string.Format( + Properties.Resources.RelationshipResolverConflictsWith, + conflictingModDescription(conflicting_mod, null), + conflictingModDescription(candidate, reason.Parent))); } } } @@ -581,6 +565,47 @@ private IEnumerable allDependers(CkanModule module, .Select(r => r.Parent) .Except(found)); + public IEnumerable Dependencies() + => BreadthFirstSearch(user_requested_mods, + (searching, found) => + modlist.Values + .Except(found) + .Where(m => ReasonsFor(m).Any(r => r is SelectionReason.Depends + && r.Parent == searching))); + + public IEnumerable Recommendations(HashSet dependencies) + => modlist.Values.Except(dependencies) + .Where(m => ReasonsFor(m).Any(r => r is SelectionReason.Recommended + && dependencies.Contains(r.Parent))) + .OrderByDescending(totalDependers); + + public IEnumerable Suggestions(HashSet dependencies, + List recommendations) + => modlist.Values.Except(dependencies) + .Except(recommendations) + .Where(m => ReasonsFor(m).Any(r => r is SelectionReason.Suggested + && dependencies.Contains(r.Parent))) + .OrderByDescending(totalDependers); + + public ParallelQuery>> Supporters( + HashSet supported, + IEnumerable toExclude) + => registry.CompatibleModules(versionCrit) + .Except(toExclude) + .AsParallel() + // Find installable modules with "supports" relationships + .Where(mod => !registry.IsInstalled(mod.identifier) + && mod.supports != null) + // Find each module that "supports" something we're installing + .Select(mod => new KeyValuePair>( + mod, + mod.supports + .Where(rel => rel.MatchesAny(supported, null, null)) + .Select(rel => (rel as ModuleRelationshipDescriptor)?.name) + .Where(name => !string.IsNullOrEmpty(name)) + .ToHashSet())) + .Where(kvp => kvp.Value.Count > 0); + /// /// Returns a dictionary consisting of KeyValuePairs containing conflicting mods. /// The keys are the mods that the user chose that led to the conflict being in the list! @@ -679,6 +704,40 @@ private void AddReason(CkanModule module, SelectionReason reason) // 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, + }; + /// /// If true, add recommended mods, and their recommendations. /// @@ -736,7 +795,7 @@ public class RelationshipResolverOptions public RelationshipResolverOptions OptionsFor(RelationshipDescriptor descr) => descr.suppress_recommendations ? WithoutRecommendations() : this; - private RelationshipResolverOptions WithoutRecommendations() + public RelationshipResolverOptions WithoutRecommendations() { if (with_recommends || with_all_suggests || with_suggests) { @@ -770,8 +829,7 @@ public abstract class SelectionReason { // Currently assumed to exist for any relationship other than UserRequested or Installed public virtual CkanModule Parent { get; protected set; } - public abstract string Reason { get; } - public virtual string DescribeWith(IEnumerable others) => Reason; + public virtual string DescribeWith(IEnumerable others) => ToString(); public class Installed : SelectionReason { @@ -784,7 +842,7 @@ public override CkanModule Parent } } - public override string Reason + public override string ToString() => Properties.Resources.RelationshipResolverInstalledReason; } @@ -799,19 +857,19 @@ public override CkanModule Parent } } - public override string Reason + public override string ToString() => Properties.Resources.RelationshipResolverUserReason; } public class DependencyRemoved : SelectionReason { - public override string Reason + public override string ToString() => Properties.Resources.RelationshipResolverDependencyRemoved; } public class NoLongerUsed : SelectionReason { - public override string Reason + public override string ToString() => Properties.Resources.RelationshipResolverNoLongerUsedReason; } @@ -823,7 +881,7 @@ public Replacement(CkanModule module) Parent = module; } - public override string Reason + public override string ToString() => string.Format(Properties.Resources.RelationshipResolverReplacementReason, Parent.name); } @@ -835,7 +893,7 @@ public Suggested(CkanModule module) Parent = module; } - public override string Reason + public override string ToString() => string.Format(Properties.Resources.RelationshipResolverSuggestedReason, Parent.name); } @@ -847,7 +905,7 @@ public Depends(CkanModule module) Parent = module; } - public override string Reason + public override string ToString() => string.Format(Properties.Resources.RelationshipResolverDependsReason, Parent.name); public override string DescribeWith(IEnumerable others) @@ -857,13 +915,22 @@ public override string DescribeWith(IEnumerable others) public sealed class Recommended : SelectionReason { - public Recommended(CkanModule module) + public Recommended(CkanModule module, int providesIndex) { - if (module == null) throw new ArgumentNullException(); - Parent = module; + if (module == null) + { + throw new ArgumentNullException(); + } + Parent = module; + ProvidesIndex = providesIndex; } - public override string Reason + 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/GUI/Controls/ChooseRecommendedMods.Designer.cs b/GUI/Controls/ChooseRecommendedMods.Designer.cs index 3e4a6ac06e..8032a65f24 100644 --- a/GUI/Controls/ChooseRecommendedMods.Designer.cs +++ b/GUI/Controls/ChooseRecommendedMods.Designer.cs @@ -73,7 +73,6 @@ private void InitializeComponent() this.RecommendedModsListView.UseCompatibleStateImageBehavior = false; this.RecommendedModsListView.View = System.Windows.Forms.View.Details; this.RecommendedModsListView.SelectedIndexChanged += new System.EventHandler(RecommendedModsListView_SelectedIndexChanged); - this.RecommendedModsListView.ItemChecked += new System.Windows.Forms.ItemCheckedEventHandler(RecommendedModsListView_ItemChecked); this.RecommendedModsListView.Groups.Add(this.RecommendationsGroup); this.RecommendedModsListView.Groups.Add(this.SuggestionsGroup); this.RecommendedModsListView.Groups.Add(this.SupportedByGroup); diff --git a/GUI/Controls/ChooseRecommendedMods.cs b/GUI/Controls/ChooseRecommendedMods.cs index cafafb25b3..a1ef0cbc77 100644 --- a/GUI/Controls/ChooseRecommendedMods.cs +++ b/GUI/Controls/ChooseRecommendedMods.cs @@ -19,25 +19,30 @@ public ChooseRecommendedMods() } [ForbidGUICalls] - public void LoadRecommendations( - IRegistryQuerier registry, - List toInstall, HashSet toUninstall, - GameVersionCriteria GameVersion, NetModuleCache cache, - Dictionary>> recommendations, - Dictionary> suggestions, - Dictionary> supporters - ) + public void LoadRecommendations(IRegistryQuerier registry, + List toInstall, + HashSet toUninstall, + GameVersionCriteria versionCrit, + NetModuleCache cache, + Dictionary>> recommendations, + Dictionary> suggestions, + Dictionary> supporters) { this.registry = registry; this.toInstall = toInstall; this.toUninstall = toUninstall; - this.GameVersion = GameVersion; + this.versionCrit = versionCrit; Util.Invoke(this, () => { RecommendedModsToggleCheckbox.Checked = true; + RecommendedModsListView.BeginUpdate(); + RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; RecommendedModsListView.Items.AddRange( getRecSugRows(cache, recommendations, suggestions, supporters).ToArray()); MarkConflicts(); + RecommendedModsListView.EndUpdate(); + // Don't set this before AddRange, it will fire for every row we add! + RecommendedModsListView.ItemChecked += RecommendedModsListView_ItemChecked; }); } @@ -54,12 +59,7 @@ public HashSet Wait() } public ListView.SelectedListViewItemCollection SelectedItems - { - get - { - return RecommendedModsListView.SelectedItems; - } - } + => RecommendedModsListView.SelectedItems; public event Action OnSelectedItemsChanged; @@ -67,10 +67,7 @@ public ListView.SelectedListViewItemCollection SelectedItems private void RecommendedModsListView_SelectedIndexChanged(object sender, EventArgs e) { - if (OnSelectedItemsChanged != null) - { - OnSelectedItemsChanged(RecommendedModsListView.SelectedItems); - } + OnSelectedItemsChanged?.Invoke(RecommendedModsListView.SelectedItems); } private void RecommendedModsListView_ItemChecked(object sender, ItemCheckedEventArgs e) @@ -93,7 +90,15 @@ private void MarkConflicts() { try { - var conflicts = FindConflicts(); + var resolver = new RelationshipResolver( + RecommendedModsListView.CheckedItems + .Cast() + .Select(item => item.Tag as CkanModule) + .Concat(toInstall) + .Distinct(), + toUninstall, + conflictOptions, registry, versionCrit); + var conflicts = resolver.ConflictList; foreach (var item in RecommendedModsListView.Items.Cast() // Apparently ListView handes AddRange by: // 1. Expanding the Items list to the new size by filling it with nulls @@ -106,15 +111,13 @@ private void MarkConflicts() : Color.Empty; } RecommendedModsContinueButton.Enabled = !conflicts.Any(); - if (OnConflictFound != null) - { - OnConflictFound(conflicts.Any() ? conflicts.First().Value : ""); - } + OnConflictFound?.Invoke(string.Join("; ", resolver.ConflictDescriptions)); } catch (DependencyNotSatisfiedKraken k) { - var row = RecommendedModsListView.Items.Cast() - .FirstOrDefault(it => (it?.Tag as CkanModule) == k.parent); + var row = RecommendedModsListView.Items + .Cast() + .FirstOrDefault(it => (it?.Tag as CkanModule) == k.parent); if (row != null) { row.BackColor = Color.LightCoral; @@ -132,47 +135,36 @@ private void MarkConflicts() with_recommends = false }; - private Dictionary FindConflicts() - => new RelationshipResolver( - RecommendedModsListView.CheckedItems.Cast() - .Select(item => item.Tag as CkanModule) - .Concat(toInstall) - .Distinct(), - toUninstall, - conflictOptions, registry, GameVersion - ).ConflictList; - private IEnumerable getRecSugRows( - NetModuleCache cache, + NetModuleCache cache, Dictionary>> recommendations, - Dictionary> suggestions, - Dictionary> supporters) - { - foreach (var kvp in recommendations) - { - yield return getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value.Item2), - RecommendationsGroup, kvp.Value.Item1); - } - - foreach (var kvp in suggestions) - { - yield return getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value), - SuggestionsGroup, false); - } - - foreach (var kvp in supporters - .ToDictionary(kvp => kvp.Key, kvp => string.Join(", ", kvp.Value.OrderBy(s => s))) - .OrderBy(kvp => kvp.Value) - ) - { - yield return getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value), - SupportedByGroup, false); - } - } - - private ListViewItem getRecSugItem(NetModuleCache cache, CkanModule module, string descrip, ListViewGroup group, bool check) - { - return new ListViewItem(new string[] + Dictionary> suggestions, + Dictionary> supporters) + => recommendations.Select(kvp => getRecSugItem(cache, + kvp.Key, + string.Join(", ", kvp.Value.Item2), + RecommendationsGroup, + kvp.Value.Item1)) + .OrderBy(item => item.SubItems[1].Text) + .Concat(suggestions.Select(kvp => getRecSugItem(cache, + kvp.Key, + string.Join(", ", kvp.Value), + SuggestionsGroup, + false)) + .OrderBy(item => item.SubItems[1].Text)) + .Concat(supporters.Select(kvp => getRecSugItem(cache, + kvp.Key, + string.Join(", ", kvp.Value.OrderBy(s => s)), + SupportedByGroup, + false)) + .OrderBy(item => item.SubItems[1].Text)); + + private ListViewItem getRecSugItem(NetModuleCache cache, + CkanModule module, + string descrip, + ListViewGroup group, + bool check) + => new ListViewItem(new string[] { module.IsDLC ? module.name : cache.DescribeAvailability(module), descrip, @@ -183,40 +175,43 @@ private ListViewItem getRecSugItem(NetModuleCache cache, CkanModule module, stri Checked = check, Group = group }; - } private void RecommendedModsToggleCheckbox_CheckedChanged(object sender, EventArgs e) { var state = ((CheckBox)sender).Checked; RecommendedModsListView.BeginUpdate(); + RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; foreach (ListViewItem item in RecommendedModsListView.Items) { if (item.Checked != state) item.Checked = state; } + MarkConflicts(); RecommendedModsListView.EndUpdate(); + RecommendedModsListView.ItemChecked += RecommendedModsListView_ItemChecked; } private void RecommendedModsCancelButton_Click(object sender, EventArgs e) { task?.SetResult(null); RecommendedModsListView.Items.Clear(); + RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; } private void RecommendedModsContinueButton_Click(object sender, EventArgs e) { - task?.SetResult( - RecommendedModsListView.CheckedItems.Cast() - .Select(item => item.Tag as CkanModule) - .ToHashSet() - ); + task?.SetResult(RecommendedModsListView.CheckedItems + .Cast() + .Select(item => item.Tag as CkanModule) + .ToHashSet()); RecommendedModsListView.Items.Clear(); + RecommendedModsListView.ItemChecked -= RecommendedModsListView_ItemChecked; } private IRegistryQuerier registry; private List toInstall; private HashSet toUninstall; - private GameVersionCriteria GameVersion; + private GameVersionCriteria versionCrit; private TaskCompletionSource> task; } diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index bc5b89bdc7..e8f87a395d 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -95,11 +95,20 @@ private void VersionsListView_ItemCheck(object sender, ItemCheckEventArgs e) } [ForbidGUICalls] - private bool installable(ModuleInstaller installer, CkanModule module, IRegistryQuerier registry) - => module.IsCompatible(Main.Instance.CurrentInstance.VersionCriteria()) - && installer.CanInstall(RelationshipResolver.DependsOnlyOpts(), - new List() { module }, - registry); + private static bool installable(ModuleInstaller installer, + CkanModule module, + IRegistryQuerier registry) + => installable(installer, module, registry, Main.Instance.CurrentInstance.VersionCriteria()); + + [ForbidGUICalls] + private static bool installable(ModuleInstaller installer, + CkanModule module, + IRegistryQuerier registry, + GameVersionCriteria crit) + => module.IsCompatible(crit) + && installer.CanInstall(new List() { module }, + RelationshipResolverOptions.DependsOnlyOpts(), + registry, crit); private bool allowInstall(CkanModule module) { diff --git a/GUI/Main/MainChangeset.cs b/GUI/Main/MainChangeset.cs index e878c2ba2a..0f472d2776 100644 --- a/GUI/Main/MainChangeset.cs +++ b/GUI/Main/MainChangeset.cs @@ -47,7 +47,7 @@ private void Changeset_OnConfirmChanges(List changeset) // Include all removes and upgrades || ch.ChangeType != GUIModChangeType.Install) .ToList(), - RelationshipResolver.DependsOnlyOpts())); + RelationshipResolverOptions.DependsOnlyOpts())); } catch (InvalidOperationException) { diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 1fbebdced2..937e22a212 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -55,7 +55,7 @@ public void InstallModuleDriver(IRegistryQuerier registry, IEnumerable> supporters new InstallArgument( result.Select(mod => new ModChange(mod, GUIModChangeType.Install)) .ToList(), - RelationshipResolver.DependsOnlyOpts())); + RelationshipResolverOptions.DependsOnlyOpts())); } } else diff --git a/GUI/Main/MainTrayIcon.cs b/GUI/Main/MainTrayIcon.cs index f7a2e244b8..5548ff607b 100644 --- a/GUI/Main/MainTrayIcon.cs +++ b/GUI/Main/MainTrayIcon.cs @@ -147,7 +147,7 @@ private void minimizeNotifyIcon_BalloonTipClicked(object sender, EventArgs e) RegistryManager.Instance(CurrentInstance, repoData).registry, CurrentInstance.VersionCriteria()) .ToList(), - RelationshipResolver.DependsOnlyOpts()) + RelationshipResolverOptions.DependsOnlyOpts()) ); } #endregion diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolver.cs index da01c83447..cfe6111efc 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolver.cs @@ -23,7 +23,7 @@ public class RelationshipResolverTests [SetUp] public void Setup() { - options = RelationshipResolver.DefaultOpts(); + options = RelationshipResolverOptions.DefaultOpts(); generator = new RandomModuleGenerator(new Random(0451)); //Sanity checker means even incorrect RelationshipResolver logic was passing options.without_enforce_consistency = true; @@ -33,7 +33,7 @@ public void Setup() public void Constructor_WithoutModules_AlwaysReturns() { var registry = CKAN.Registry.Empty(); - options = RelationshipResolver.DefaultOpts(); + options = RelationshipResolverOptions.DefaultOpts(); Assert.DoesNotThrow(() => new RelationshipResolver(new List(), null, options, registry, null)); } @@ -984,7 +984,7 @@ public void AutodetectedCanSatisfyRelationships() CkanModule mod = generator.GeneratorRandomModule(depends: depends); new RelationshipResolver( - new CkanModule[] { mod }, null, RelationshipResolver.DefaultOpts(), + new CkanModule[] { mod }, null, RelationshipResolverOptions.DefaultOpts(), registry, new GameVersionCriteria(GameVersion.Parse("1.0.0"))); } }