From 75b127a3043a50cf5a00682724136e7b5cd21fd0 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sat, 8 Oct 2022 16:16:46 -0500 Subject: [PATCH] Fix modpack installation, install incompatible with confirmation --- Core/Properties/Resources.Designer.cs | 4 + Core/Properties/Resources.resx | 1 + Core/Registry/RegistryManager.cs | 80 ++++++++++++------- Core/Relationships/RelationshipResolver.cs | 9 +++ Core/Types/RelationshipDescriptor.cs | 61 +++++++------- Core/Versioning/GameVersion.cs | 47 +++++------ Core/Versioning/GameVersionBound.cs | 44 +++++++--- Core/Versioning/GameVersionCriteria.cs | 55 +++++-------- Core/Versioning/GameVersionRange.cs | 49 +++++------- GUI/Controls/EditModpack.cs | 12 +++ GUI/Controls/ModInfoTabs/Versions.cs | 4 +- GUI/Controls/Wait.cs | 1 + GUI/Main/Main.cs | 38 ++++++++- GUI/Main/MainInstall.cs | 21 ++--- GUI/Properties/Resources.Designer.cs | 3 + GUI/Properties/Resources.resx | 7 +- Tests/Core/Versioning/KspVersionRangeTests.cs | 2 +- 17 files changed, 256 insertions(+), 182 deletions(-) diff --git a/Core/Properties/Resources.Designer.cs b/Core/Properties/Resources.Designer.cs index 9108f6ff04..eecfc0cad8 100644 --- a/Core/Properties/Resources.Designer.cs +++ b/Core/Properties/Resources.Designer.cs @@ -591,5 +591,9 @@ internal static string SanityCheckerUnsatisfiedDependency { internal static string SanityCheckerConflictsWith { get { return (string)(ResourceManager.GetObject("SanityCheckerConflictsWith", resourceCulture)); } } + + internal static string ModpackName { + get { return (string)(ResourceManager.GetObject("ModpackName", resourceCulture)); } + } } } diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index 5112d9c76d..380a647877 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -308,4 +308,5 @@ Free up space on that device or change your settings to use another location. {0} {1} ({2}, {3} remaining) * Install: {0} {1} ({2}, {3} remaining) * Upgrade: {0} {1} to {2} ({3}, {4} remaining) + installed-{0} diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index 871c61da9b..22f2661353 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Text; using System.Runtime.Serialization; +using System.ComponentModel; +using System.Reflection; using ChinhDo.Transactions.FileManager; using log4net; @@ -462,7 +464,9 @@ private string SerializeCurrentInstall(bool recommends = false, bool with_versio public CkanModule GenerateModpack(bool recommends = false, bool with_versions = true) { string gameInstanceName = gameInstance.Name; - string name = $"installed-{gameInstanceName}"; + string name = string.Format(Properties.Resources.ModpackName, gameInstanceName); + var crit = gameInstance.VersionCriteria(); + var minAndMax = crit.MinAndMax; var module = new CkanModule( // v1.18 to allow Unlicense new ModuleVersion("v1.18"), @@ -470,51 +474,67 @@ public CkanModule GenerateModpack(bool recommends = false, bool with_versions = name, string.Format(Properties.Resources.RegistryManagerDefaultModpackAbstract, gameInstanceName), null, - new List() { Environment.UserName }, - new List() { new License("unknown") }, + new List() { Environment.UserName }, + new List() { License.UnknownLicense }, new ModuleVersion(DateTime.UtcNow.ToString("yyyy.MM.dd.hh.mm.ss")), null, - "metapackage" - ) - { - download_content_type = "application/zip", - release_date = DateTime.Now, + "metapackage") + { + ksp_version_min = minAndMax.Lower.AsInclusiveLower().WithoutBuild, + ksp_version_max = minAndMax.Upper.AsInclusiveUpper().WithoutBuild, + download_content_type = typeof(CkanModule).GetTypeInfo() + .GetDeclaredField("download_content_type") + .GetCustomAttribute() + .Value.ToString(), + release_date = DateTime.Now, }; - List mods = registry.Installed(false, false) - .Where(kvp => { - // Skip unavailable modules (custom .ckan files) - try - { - var avail = registry.LatestAvailable(kvp.Key, null, null); - return !avail.IsDLC; - } - catch - { - return false; - } - }) - // Case insensitive sort by identifier - .OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase) - .Select(kvp => (RelationshipDescriptor) new ModuleRelationshipDescriptor() - { - name = kvp.Key, - version = with_versions ? kvp.Value : null - }) + var rels = registry.InstalledModules + .Where(inst => !inst.Module.IsDLC && IsAvailable(inst)) + .OrderBy(inst => inst.identifier, StringComparer.OrdinalIgnoreCase) + .Select(with_versions ? (Func) RelationshipWithVersion + : RelationshipWithoutVersion) .ToList(); if (recommends) { - module.recommends = mods; + module.recommends = rels; } else { - module.depends = mods; + module.depends = rels; } return module; } + private bool IsAvailable(InstalledModule inst) + { + try + { + var avail = registry.LatestAvailable(inst.identifier, null, null); + return true; + } + catch + { + // Skip unavailable modules (custom .ckan files) + return false; + } + } + + private RelationshipDescriptor RelationshipWithVersion(InstalledModule inst) + => new ModuleRelationshipDescriptor() + { + name = inst.identifier, + version = inst.Module.version, + }; + + private RelationshipDescriptor RelationshipWithoutVersion(InstalledModule inst) + => new ModuleRelationshipDescriptor() + { + name = inst.identifier, + }; + /// /// Look for DLC installed in GameData /// diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index a313cd130b..9361108ee7 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -567,7 +567,10 @@ private void ResolveStanza(IEnumerable stanza, Selection private void Add(CkanModule module, SelectionReason reason) { if (module.IsMetapackage) + { + AddReason(module, reason); return; + } if (module.IsDLC) { throw new ModuleIsDLCKraken(module); @@ -698,6 +701,12 @@ public IEnumerable ModList() log.DebugFormat("Parent found: {0}, {1}", index, module); sortedDepsFirst.Insert(index, module); } + catch (ArgumentException) + { + // ReasonsFor throws this for mods without reasons, just add it to the end + log.DebugFormat("Reasons for parent not found: {0}", module); + sortedDepsFirst.Add(module); + } catch (InvalidOperationException) { // No index, just append diff --git a/Core/Types/RelationshipDescriptor.cs b/Core/Types/RelationshipDescriptor.cs index 7c7d4c2b28..34693a47e4 100644 --- a/Core/Types/RelationshipDescriptor.cs +++ b/Core/Types/RelationshipDescriptor.cs @@ -29,8 +29,11 @@ out CkanModule matched public abstract List LatestAvailableWithProvides( IRegistryQuerier registry, GameVersionCriteria crit, IEnumerable installed = null, - IEnumerable toInstall = null - ); + IEnumerable toInstall = null); + + public abstract CkanModule ExactMatch( + IRegistryQuerier registry, GameVersionCriteria crit, IEnumerable installed = null, + IEnumerable toInstall = null); public abstract bool Equals(RelationshipDescriptor other); @@ -167,11 +170,14 @@ out CkanModule matched public override List LatestAvailableWithProvides( IRegistryQuerier registry, GameVersionCriteria crit, IEnumerable installed = null, - IEnumerable toInstall = null - ) - { - return registry.LatestAvailableWithProvides(name, crit, this, installed, toInstall); - } + IEnumerable toInstall = null) + => registry.LatestAvailableWithProvides(name, crit, this, installed, toInstall); + + public override CkanModule ExactMatch( + IRegistryQuerier registry, GameVersionCriteria crit, IEnumerable installed = null, + IEnumerable toInstall = null) + => registry.LatestAvailableWithProvides(name, crit, this, installed, toInstall) + .FirstOrDefault(mod => mod.identifier == name); public override bool Equals(RelationshipDescriptor other) { @@ -183,15 +189,10 @@ public override bool Equals(RelationshipDescriptor other) && max_version == modRel.max_version; } - public override bool ContainsAny(IEnumerable identifiers) - { - return identifiers.Contains(name); - } + public override bool ContainsAny(IEnumerable identifiers) => identifiers.Contains(name); public override bool StartsWith(string prefix) - { - return name.IndexOf(prefix, StringComparison.CurrentCultureIgnoreCase) == 0; - } + => name.IndexOf(prefix, StringComparison.CurrentCultureIgnoreCase) == 0; /// /// Generate a user readable description of the relationship @@ -205,16 +206,13 @@ public override bool StartsWith(string prefix) /// name max_version or earlier /// public override string ToString() - { - return - version != null ? $"{name} {version}" + => version != null ? $"{name} {version}" : min_version != null && max_version != null ? $"{name} {min_version}–{max_version}" : min_version != null ? string.Format(Properties.Resources.RelationshipDescriptorMinVersionOnly, name, min_version) : max_version != null ? string.Format(Properties.Resources.RelationshipDescriptorMaxVersionOnly, name, max_version) : name; - } } @@ -262,11 +260,14 @@ out CkanModule matched public override List LatestAvailableWithProvides( IRegistryQuerier registry, GameVersionCriteria crit, IEnumerable installed = null, - IEnumerable toInstall = null - ) - { - return any_of?.SelectMany(r => r.LatestAvailableWithProvides(registry, crit, installed, toInstall)).Distinct().ToList(); - } + IEnumerable toInstall = null) + => any_of?.SelectMany(r => r.LatestAvailableWithProvides(registry, crit, installed, toInstall)).Distinct().ToList(); + + // Exact match is not possible for any_of + public override CkanModule ExactMatch( + IRegistryQuerier registry, GameVersionCriteria crit, IEnumerable installed = null, + IEnumerable toInstall = null) + => null; public override bool Equals(RelationshipDescriptor other) { @@ -276,22 +277,14 @@ public override bool Equals(RelationshipDescriptor other) } public override bool ContainsAny(IEnumerable identifiers) - { - return any_of?.Any(r => r.ContainsAny(identifiers)) - ?? false; - } + => any_of?.Any(r => r.ContainsAny(identifiers)) ?? false; public override bool StartsWith(string prefix) - { - return any_of?.Any(r => r.StartsWith(prefix)) - ?? false; - } + => any_of?.Any(r => r.StartsWith(prefix)) ?? false; public override string ToString() - { - return any_of?.Select(r => r.ToString()) + => any_of?.Select(r => r.ToString()) .Aggregate((a, b) => string.Format(Properties.Resources.RelationshipDescriptorAnyOfJoiner, a, b)); - } } } diff --git a/Core/Versioning/GameVersion.cs b/Core/Versioning/GameVersion.cs index 192cb8e795..c7976a742b 100644 --- a/Core/Versioning/GameVersion.cs +++ b/Core/Versioning/GameVersion.cs @@ -36,65 +36,59 @@ public sealed partial class GameVersion /// Gets the value of the major component of the version number for the current /// object. /// - public int Major { get { return _major; } } + public int Major => _major; /// /// Gets the value of the minor component of the version number for the current /// object. /// - public int Minor { get { return _minor; } } + public int Minor => _minor; /// /// Gets the value of the patch component of the version number for the current /// object. /// - public int Patch { get { return _patch; } } + public int Patch => _patch; /// /// Gets the value of the build component of the version number for the current /// object. /// - public int Build { get { return _build; } } + public int Build => _build; /// /// Gets whether or not the major component of the version number for the current /// object is defined. /// - public bool IsMajorDefined { get { return _major != Undefined; } } + public bool IsMajorDefined => _major != Undefined; /// /// Gets whether or not the minor component of the version number for the current /// object is defined. /// - public bool IsMinorDefined { get { return _minor != Undefined; } } + public bool IsMinorDefined => _minor != Undefined; /// /// Gets whether or not the patch component of the version number for the current /// object is defined. /// - public bool IsPatchDefined { get { return _patch != Undefined; } } + public bool IsPatchDefined => _patch != Undefined; /// /// Gets whether or not the build component of the version number for the current /// object is defined. /// - public bool IsBuildDefined { get { return _build != Undefined; } } + public bool IsBuildDefined => _build != Undefined; /// /// Indicates whether or not all components of the current are defined. /// - public bool IsFullyDefined - { - get { return IsMajorDefined && IsMinorDefined && IsPatchDefined && IsBuildDefined; } - } + public bool IsFullyDefined => IsMajorDefined && IsMinorDefined && IsPatchDefined && IsBuildDefined; /// /// Indicates wheter or not all the components of the current are undefined. /// - public bool IsAny - { - get { return !IsMajorDefined && !IsMinorDefined && !IsPatchDefined && !IsBuildDefined; } - } + public bool IsAny => !IsMajorDefined && !IsMinorDefined && !IsPatchDefined && !IsBuildDefined; /// /// Check whether a version is null or Any. @@ -104,10 +98,7 @@ public bool IsAny /// /// True if null or Any, false otherwise /// - public static bool IsNullOrAny(GameVersion v) - { - return v == null || v.IsAny; - } + public static bool IsNullOrAny(GameVersion v) => v == null || v.IsAny; /// /// Initialize a new instance of the class with all components unspecified. @@ -240,10 +231,7 @@ public GameVersion(int major, int minor, int patch, int build) /// If the current is totally undefined the return value will null. /// /// - public override string ToString() - { - return _string; - } + public override string ToString() => _string; private static Dictionary VersionsMax = new Dictionary(); @@ -304,6 +292,13 @@ private static int UptoNines(int num) return (int)Math.Pow(10, Math.Floor(Math.Log10(num + 1)) + 1) - 1; } + /// + /// Strip off the build number if it's defined + /// + /// A GameVersion equal to this but without a build number + public GameVersion WithoutBuild => IsBuildDefined ? new GameVersion(_major, _minor, _patch) + : this; + /// /// Converts the value of the current to its equivalent /// . @@ -491,7 +486,7 @@ public bool InBuildMap(IGame game) /// Raises a selection dialog for choosing a specific KSP version, if it is not fully defined yet. /// If a build number is specified but not known, it presents a list of all builds /// of the patch range. - /// Needs at least a Major and Minor (doesn't make sense else). + /// Needs at least a Major and Minor (doesn't make sense else). /// /// A complete GameVersion object /// A IUser instance, to raise the corresponding dialog. @@ -526,7 +521,7 @@ public GameVersion RaiseVersionSelectionDialog(IGame game, IUser user) { possibleVersions.Add(ver); } - } + } // If we also have Patch -> compare it too. else if (!IsBuildDefined) { diff --git a/Core/Versioning/GameVersionBound.cs b/Core/Versioning/GameVersionBound.cs index 4c48d20f6d..9ddbd86dcd 100644 --- a/Core/Versioning/GameVersionBound.cs +++ b/Core/Versioning/GameVersionBound.cs @@ -33,10 +33,35 @@ public GameVersionBound(GameVersion value, bool inclusive) _string = inclusive ? string.Format("[{0}]", valueStr) : string.Format("({0})", valueStr); } - public override string ToString() - { - return _string; - } + public GameVersion AsInclusiveLower() + => Inclusive + // Already inclusive? Drop all trailing 0 values + ? (Value.IsPatchDefined && Value.Patch > 0 ? new GameVersion(Value.Major, Value.Minor, Value.Patch) + : Value.IsMinorDefined && Value.Minor > 0 ? new GameVersion(Value.Major, Value.Minor) + : Value.IsMajorDefined && Value.Major > 0 ? new GameVersion(Value.Major) + : GameVersion.Any) + // 1.3.1 non-inclusive => 1.3.2 inclusive lower + : Value.IsPatchDefined ? new GameVersion(Value.Major, Value.Minor, Value.Patch + 1) + // 1.3 non-inclusive => 1.4 inclusive lower + : Value.IsMinorDefined ? new GameVersion(Value.Major, Value.Minor + 1) + // 1 non-inclusive => 2 inclusive lower + : Value.IsMajorDefined ? new GameVersion(Value.Major + 1) + // Unbounded, I guess? + : GameVersion.Any; + + public GameVersion AsInclusiveUpper() + // Already inclusive? + => Inclusive ? Value + // 1.3.1 non-inclusive => 1.3.0 inclusive upper + : Value.IsPatchDefined && Value.Patch > 0 ? new GameVersion(Value.Major, Value.Minor, Value.Patch - 1) + // 1.3 non-inclusive => 1.2 inclusive upper + : Value.IsMinorDefined && Value.Minor > 0 ? new GameVersion(Value.Major, Value.Minor - 1) + // 2 non-inclusive => 1 inclusive lower + : Value.IsMajorDefined && Value.Major > 0 ? new GameVersion(Value.Major - 1) + // Unbounded, I guess? + : GameVersion.Any; + + public override string ToString() => _string; } public sealed partial class GameVersionBound : IEquatable @@ -63,15 +88,8 @@ public override int GetHashCode() } } - public static bool operator ==(GameVersionBound left, GameVersionBound right) - { - return Equals(left, right); - } - - public static bool operator !=(GameVersionBound left, GameVersionBound right) - { - return !Equals(left, right); - } + public static bool operator ==(GameVersionBound left, GameVersionBound right) => Equals(left, right); + public static bool operator !=(GameVersionBound left, GameVersionBound right) => !Equals(left, right); } public sealed partial class GameVersionBound diff --git a/Core/Versioning/GameVersionCriteria.cs b/Core/Versioning/GameVersionCriteria.cs index e33ead8c76..890964dfee 100644 --- a/Core/Versioning/GameVersionCriteria.cs +++ b/Core/Versioning/GameVersionCriteria.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; +using CKAN.Games; + namespace CKAN.Versioning { public class GameVersionCriteria : IEquatable @@ -26,49 +28,36 @@ public GameVersionCriteria(GameVersion v, List compatibleVersions) this._versions = this._versions.Distinct().ToList(); } - public IList Versions - { - get - { - return _versions.AsReadOnly(); - } - } + public IList Versions => _versions.AsReadOnly(); + + public GameVersionRange MinAndMax => Versions + .Skip(1) + .Select(v => v.ToVersionRange()) + .Aggregate(Versions.First().ToVersionRange(), + (range, v) => new GameVersionRange( + GameVersionBound.Lowest(range.Lower, v.Lower), + GameVersionBound.Highest(range.Upper, v.Upper))); public GameVersionCriteria Union(GameVersionCriteria other) - { - return new GameVersionCriteria( - null, - _versions.Union(other.Versions).ToList() - ); - } + => new GameVersionCriteria(null, _versions.Union(other.Versions).ToList()); public override bool Equals(object obj) - { - return Equals(obj as GameVersionCriteria); - } + => Equals(obj as GameVersionCriteria); // From IEquatable public bool Equals(GameVersionCriteria other) - { - return other == null - ? false - : !_versions.Except(other._versions).Any() - && !other._versions.Except(_versions).Any(); - } + => other == null ? false + : !_versions.Except(other._versions).Any() + && !other._versions.Except(_versions).Any(); public override int GetHashCode() - { - return _versions.Aggregate(19, (code, vers) => code * 31 + vers.GetHashCode()); - } + => _versions.Aggregate(19, (code, vers) => code * 31 + vers.GetHashCode()); public override String ToString() - { - List versionList = new List(); - foreach (GameVersion version in _versions) - { - versionList.Add(version.ToString()); - } - return string.Format(Properties.Resources.GameVersionCriteriaToString, string.Join( ", ", versionList)); - } + => string.Format(Properties.Resources.GameVersionCriteriaToString, + string.Join(", ", _versions.Select(v => v.ToString()))); + + public string ToSummaryString(IGame game) + => MinAndMax.ToSummaryString(game); } } diff --git a/Core/Versioning/GameVersionRange.cs b/Core/Versioning/GameVersionRange.cs index 74ce3d94ad..21feb44b14 100644 --- a/Core/Versioning/GameVersionRange.cs +++ b/Core/Versioning/GameVersionRange.cs @@ -12,7 +12,7 @@ public sealed partial class GameVersionRange new GameVersionRange(GameVersionBound.Unbounded, GameVersionBound.Unbounded); public GameVersionBound Lower { get; private set; } - public GameVersionBound Upper { get; private set; } + public GameVersionBound Upper { get; private set; } public GameVersionRange(GameVersionBound lower, GameVersionBound upper) { @@ -31,10 +31,7 @@ public GameVersionRange(GameVersionBound lower, GameVersionBound upper) public GameVersionRange(GameVersion lower, GameVersion upper) : this(lower?.ToVersionRange().Lower, upper?.ToVersionRange().Upper) { } - public override string ToString() - { - return _string; - } + public override string ToString() =>_string; public GameVersionRange IntersectWith(GameVersionRange other) { @@ -64,10 +61,8 @@ public bool IsSupersetOf(GameVersionRange other) } private static bool IsEmpty(GameVersionBound lower, GameVersionBound upper) - { - return upper.Value < lower.Value || + => upper.Value < lower.Value || (lower.Value == upper.Value && (!lower.Inclusive || !upper.Inclusive)); - } private static string DeriveString(GameVersionRange versionRange) { @@ -89,11 +84,9 @@ private static string DeriveString(GameVersionRange versionRange) } private static string SameVersionString(GameVersion v) - { - return v == null ? "???" - : v.IsAny ? "all versions" - : v.ToString(); - } + => v == null ? "???" + : v.IsAny ? Properties.Resources.CkanModuleAllVersions + : v.ToString(); /// /// Generate a string describing a range of KSP versions. @@ -105,15 +98,18 @@ private static string SameVersionString(GameVersion v) /// Human readable string describing the versions. /// public static string VersionSpan(IGame game, GameVersion minKsp, GameVersion maxKsp) - { - return minKsp == maxKsp ? $"{game.ShortName} {SameVersionString(minKsp)}" - : minKsp.IsAny + => minKsp == maxKsp + ? $"{game.ShortName} {SameVersionString(minKsp)}" + : minKsp.IsAny ? string.Format(Properties.Resources.GameVersionRangeMinOnly, game.ShortName, maxKsp) - : maxKsp.IsAny - ? string.Format(Properties.Resources.GameVersionRangeMaxOnly, game.ShortName, minKsp) - : $"{game.ShortName} {minKsp}–{maxKsp}"; - } - + : maxKsp.IsAny + ? string.Format(Properties.Resources.GameVersionRangeMaxOnly, game.ShortName, minKsp) + : $"{game.ShortName} {minKsp}–{maxKsp}"; + + public string ToSummaryString(IGame game) + => VersionSpan(game, + Lower.AsInclusiveLower().WithoutBuild, + Upper.AsInclusiveUpper().WithoutBuild); } public sealed partial class GameVersionRange : IEquatable @@ -140,14 +136,7 @@ public override int GetHashCode() } } - public static bool operator ==(GameVersionRange left, GameVersionRange right) - { - return Equals(left, right); - } - - public static bool operator !=(GameVersionRange left, GameVersionRange right) - { - return !Equals(left, right); - } + public static bool operator ==(GameVersionRange left, GameVersionRange right) => Equals(left, right); + public static bool operator !=(GameVersionRange left, GameVersionRange right) => !Equals(left, right); } } diff --git a/GUI/Controls/EditModpack.cs b/GUI/Controls/EditModpack.cs index fa5ca963b3..5738640eff 100644 --- a/GUI/Controls/EditModpack.cs +++ b/GUI/Controls/EditModpack.cs @@ -317,6 +317,18 @@ private void ExportModpackButton_Click(object sender, EventArgs e) } else if (TrySavePrompt(modpackExportOptions, out ExportOption selectedOption, out string filename)) { + if (module.depends.Count == 0) + { + module.depends = null; + } + if (module.recommends.Count == 0) + { + module.recommends = null; + } + if (module.suggests.Count == 0) + { + module.suggests = null; + } CkanModule.ToFile(ApplyVersionsCheckbox(module), filename); task?.SetResult(true); } diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index 8a4a567678..882b577956 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -70,7 +70,9 @@ private bool allowInstall(CkanModule module) return installable(installer, module, registry) || Main.Instance.YesNoDialog( - string.Format(Properties.Resources.AllModVersionsInstallPrompt, module.ToString()), + string.Format(Properties.Resources.AllModVersionsInstallPrompt, + module.ToString(), + currentInstance.VersionCriteria().ToSummaryString(currentInstance.game)), Properties.Resources.AllModVersionsInstallYes, Properties.Resources.AllModVersionsInstallNo); } diff --git a/GUI/Controls/Wait.cs b/GUI/Controls/Wait.cs index 6f9780c1dc..2d975275bc 100644 --- a/GUI/Controls/Wait.cs +++ b/GUI/Controls/Wait.cs @@ -85,6 +85,7 @@ public void SetProgress(string label, long remaining, long total) { AutoSize = true, Text = label, + Margin = new Padding(0, 8, 0, 0), }; progressLabels.Add(label, newLb); var newPb = new ProgressBar() diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 4d2c88fbe6..b1c3a6f645 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -563,6 +563,10 @@ private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e) { // We'll need to make some registry changes to do this. RegistryManager registry_manager = RegistryManager.Instance(CurrentInstance); + var crit = CurrentInstance.VersionCriteria(); + + var installed = registry_manager.registry.InstalledModules.Select(inst => inst.Module).ToList(); + var toInstall = new List(); foreach (string path in open_file_dialog.FileNames) { @@ -571,6 +575,20 @@ private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e) try { module = CkanModule.FromFile(path); + if (module.IsMetapackage && module.depends != null) + { + // Add metapackage dependencies to the changeset so we can skip compat checks for them + toInstall.AddRange(module.depends + .Where(rel => !rel.MatchesAny(installed, null, null)) + .Select(rel => + // If there's a compatible match, return it + // Metapackages aren't intending to prompt users to choose providing mods + rel.ExactMatch(registry_manager.registry, crit, installed, toInstall) + // Otherwise look for incompatible + ?? rel.ExactMatch(registry_manager.registry, null, installed, toInstall)) + .Where(mod => mod != null)); + } + toInstall.Add(module); } catch (Kraken kraken) { @@ -591,10 +609,24 @@ private void installFromckanToolStripMenuItem_Click(object sender, EventArgs e) currentUser.RaiseError(Properties.Resources.MainCantInstallDLC, module); continue; } - - InstallModuleDriver(registry_manager.registry, module); } - registry_manager.Save(true); + // Get all recursively incompatible module identifiers (quickly) + var allIncompat = registry_manager.registry.IncompatibleModules(crit) + .Select(mod => mod.identifier) + .ToHashSet(); + // Get incompatible mods we're installing + var myIncompat = toInstall.Where(mod => allIncompat.Contains(mod.identifier)).ToList(); + if (!myIncompat.Any() + // Confirm installation of incompatible like the Versions tab does + || Main.Instance.YesNoDialog( + string.Format(Properties.Resources.ModpackInstallIncompatiblePrompt, + string.Join(Environment.NewLine, myIncompat), + crit.ToSummaryString(CurrentInstance.game)), + Properties.Resources.AllModVersionsInstallYes, + Properties.Resources.AllModVersionsInstallNo)) + { + InstallModuleDriver(registry_manager.registry, toInstall); + } } } diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 309801b2ab..6e3566aab4 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -17,29 +17,30 @@ public partial class Main /// /// Reference to the registry /// Module to install - public void InstallModuleDriver(IRegistryQuerier registry, CkanModule module) + public void InstallModuleDriver(IRegistryQuerier registry, IEnumerable modules) { try { DisableMainWindow(); var userChangeSet = new List(); - InstalledModule installed = registry.InstalledModule(module.identifier); - if (installed != null) + foreach (var module in modules) { - // Already installed, remove it first - userChangeSet.Add(new ModChange(installed.Module, GUIModChangeType.Remove)); + InstalledModule installed = registry.InstalledModule(module.identifier); + if (installed != null) + { + // Already installed, remove it first + userChangeSet.Add(new ModChange(installed.Module, GUIModChangeType.Remove)); + } + // Install the selected mod + userChangeSet.Add(new ModChange(module, GUIModChangeType.Install)); } - // Install the selected mod - userChangeSet.Add(new ModChange(module, GUIModChangeType.Install)); if (userChangeSet.Count > 0) { // Resolve the provides relationships in the dependencies Wait.StartWaiting(InstallMods, PostInstallMods, true, new KeyValuePair, RelationshipResolverOptions>( userChangeSet, - RelationshipResolver.DependsOnlyOpts() - ) - ); + RelationshipResolver.DependsOnlyOpts())); } } catch diff --git a/GUI/Properties/Resources.Designer.cs b/GUI/Properties/Resources.Designer.cs index 5034c41f55..7697cd7e39 100644 --- a/GUI/Properties/Resources.Designer.cs +++ b/GUI/Properties/Resources.Designer.cs @@ -501,6 +501,9 @@ internal static string MainCorruptedRegistry { internal static string AllModVersionsInstallPrompt { get { return (string)(ResourceManager.GetObject("AllModVersionsInstallPrompt", resourceCulture)); } } + internal static string ModpackInstallIncompatiblePrompt { + get { return (string)(ResourceManager.GetObject("ModpackInstallIncompatiblePrompt", resourceCulture)); } + } internal static string AllModVersionsInstallYes { get { return (string)(ResourceManager.GetObject("AllModVersionsInstallYes", resourceCulture)); } } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index ea72d623ae..31e28d0460 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -214,9 +214,14 @@ Try to move {2} out of {3} and restart CKAN. Corrupted registry archived to {0}: {1} This means that CKAN forgot about all your installed mods, but they are still in GameData. You can reinstall them by importing the {2} file. - {0} is not supported on your current game version and may not work at all. If you have any problems with it, you should NOT ask its maintainers for help. + {0} is not supported on your current compatible game versions ({1}) and may not work at all. If you have any problems with it, you should NOT ask its maintainers for help. Do you really want to install it? + Some of the selected mods (or their dependencies) are not supported on your current compatible game versions ({1}) and may not work at all. If you have any problems with them, you should NOT ask their maintainers for help. + +{0} + +Are you sure you want to install them? Cancelling will abort the entire installation. Install Cancel Update selected by user to version {0}. diff --git a/Tests/Core/Versioning/KspVersionRangeTests.cs b/Tests/Core/Versioning/KspVersionRangeTests.cs index 57d7757cd9..538675ecf0 100644 --- a/Tests/Core/Versioning/KspVersionRangeTests.cs +++ b/Tests/Core/Versioning/KspVersionRangeTests.cs @@ -628,7 +628,7 @@ public void VersionSpan_AllVersions_CorrectString() // Act string s = GameVersionRange.VersionSpan(game, min, max); // Assert - Assert.AreEqual("KSP all versions", s); + Assert.AreEqual("KSP All versions", s); } [Test]