diff --git a/CHANGELOG.md b/CHANGELOG.md index a53aaeb5ba..519a1eaa54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file. - [Build] Use Core.Utilities.CopyDirectory in tests (#2670 by: DasSkelett; reviewed: HebaruSan) - [Core] Avoid redundant metadata downloads (#2682 by: HebaruSan, reviewed: DasSkelett, politas) - [Netkan] Releases option for Netkan (#2681 by: HebaruSan, reviewed: politas) +- [Multiple] Support replaced_by property (#2671 by: politas, HebaruSan; reviewed: DasSkelett, politas) ### Bugfixes diff --git a/CKAN.schema b/CKAN.schema index 1a468b205f..bd5c9b54a7 100644 --- a/CKAN.schema +++ b/CKAN.schema @@ -144,6 +144,25 @@ "items" : { "type" : "string" }, "uniqueItems" : true }, + "replaced_by" : { + "description" : "Optional pointer to mod that should be selected instead and treated as an update to this mod", + "type" : "object", + "properties" : { + "name" : { + "description" : "Identifier of the mod", + "$ref" : "#/definitions/identifier" + }, + "version" : { + "description" : "Optional version", + "$ref" : "#/definitions/version" + }, + "min_version" : { + "description" : "Optional minimum version", + "$ref" : "#/definitions/version" + } + }, + "required" : [ "name" ] + }, "resources" : { "description" : "Additional resources", "type" : "object", diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index 2d2cb71077..014be73a71 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -82,7 +82,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) // Parse the JSON file. try { - CkanModule m = LoadCkanFromFile(ksp, filename); + CkanModule m = MainClass.LoadCkanFromFile(ksp, filename); options.modules.Add($"{m.identifier}={m.version}"); } catch (Kraken kraken) @@ -284,21 +284,5 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) return Exit.OK; } - - internal static CkanModule LoadCkanFromFile(CKAN.KSP current_instance, string ckan_file) - { - CkanModule module = CkanModule.FromFile(ckan_file); - - // We'll need to make some registry changes to do this. - RegistryManager registry_manager = RegistryManager.Instance(current_instance); - - // Remove this version of the module in the registry, if it exists. - registry_manager.registry.RemoveAvailable(module); - - // Sneakily add our version in... - registry_manager.registry.AddAvailable(module); - - return module; - } } } diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 92b16a4e64..2a6117652a 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -51,7 +51,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) foreach (KeyValuePair mod in installed) { ModuleVersion current_version = mod.Value; - + string modInfo = string.Format("{0} {1}", mod.Key, mod.Value); string bullet = "*"; if (current_version is ProvidesModuleVersion) @@ -62,26 +62,52 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) else if (current_version is UnmanagedModuleVersion) { // Autodetected dll - bullet = "-"; + bullet = "A"; } else { try { // Check if upgrades are available, and show appropriately. + log.DebugFormat("Check if upgrades are available for {0}", mod.Key); CkanModule latest = registry.LatestAvailable(mod.Key, ksp.VersionCriteria()); - - log.InfoFormat("Latest {0} is {1}", mod.Key, latest); + CkanModule current = registry.GetInstalledVersion(mod.Key); if (latest == null) { // Not compatible! + log.InfoFormat("Latest {0} is not compatible", mod.Key); bullet = "X"; + if ( current == null ) log.DebugFormat( " {0} installed version not found in registry", mod.Key); + + //Check if mod is replaceable + if ( current.replaced_by != null ) + { + ModuleReplacement replacement = registry.GetReplacement(mod.Key, ksp.VersionCriteria()); + if ( replacement != null ) + { + //Replaceable! + bullet = ">"; + modInfo = string.Format("{0} > {1} {2}", modInfo, replacement.ReplaceWith.name, replacement.ReplaceWith.version); + } + } } else if (latest.version.IsEqualTo(current_version)) { // Up to date + log.InfoFormat("Latest {0} is {1}", mod.Key, latest.version); bullet = "-"; + //Check if mod is replaceable + if ( current.replaced_by != null ) + { + ModuleReplacement replacement = registry.GetReplacement(latest.identifier, ksp.VersionCriteria()); + if ( replacement != null ) + { + //Replaceable! + bullet = ">"; + modInfo = string.Format("{0} > {1} {2}", modInfo, replacement.ReplaceWith.name, replacement.ReplaceWith.version); + } + } } else if (latest.version.IsGreaterThan(mod.Value)) { @@ -96,7 +122,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) } } - user.RaiseMessage("{0} {1} {2}", bullet, mod.Key, mod.Value); + user.RaiseMessage("{0} {1}", bullet, modInfo); } } else @@ -108,7 +134,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) if (!(options.porcelain) && exportFileType == null) { - user.RaiseMessage("\r\nLegend: -: Up to date. X: Incompatible. ^: Upgradable. ?: Unknown. *: Broken. "); + user.RaiseMessage("\r\nLegend: -: Up to date. X: Incompatible. ^: Upgradable. >: Replaceable\r\n A: Autodetected. ?: Unknown. *: Broken. "); // Broken mods are in a state that CKAN doesn't understand, and therefore can't handle automatically } diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs new file mode 100644 index 0000000000..9d090f125b --- /dev/null +++ b/Cmdline/Action/Replace.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using log4net; +using CKAN.Versioning; + +namespace CKAN.CmdLine +{ + public class Replace : ICommand + { + private static readonly ILog log = LogManager.GetLogger(typeof(Replace)); + + public IUser User { get; set; } + + public Replace(CKAN.KSPManager mgr, IUser user) + { + manager = mgr; + User = user; + } + + private KSPManager manager; + + public int RunCommand(CKAN.KSP ksp, object raw_options) + { + ReplaceOptions options = (ReplaceOptions) raw_options; + + if (options.ckan_file != null) + { + options.modules.Add(MainClass.LoadCkanFromFile(ksp, options.ckan_file).identifier); + } + + if (options.modules.Count == 0 && ! options.replace_all) + { + // What? No mods specified? + User.RaiseMessage("Usage: ckan replace Mod [Mod2, ...]"); + User.RaiseMessage(" or ckan replace --all"); + return Exit.BADOPT; + } + + // Prepare options. Can these all be done in the new() somehow? + var replace_ops = new RelationshipResolverOptions + { + with_all_suggests = options.with_all_suggests, + with_suggests = options.with_suggests, + with_recommends = !options.no_recommends, + allow_incompatible = options.allow_incompatible + }; + + var registry = RegistryManager.Instance(ksp).registry; + var to_replace = new List(); + + if (options.replace_all) + { + log.Debug("Running Replace all"); + var installed = new Dictionary(registry.Installed()); + + foreach (KeyValuePair mod in installed) + { + ModuleVersion current_version = mod.Value; + + if ((current_version is ProvidesModuleVersion) || (current_version is UnmanagedModuleVersion)) + { + continue; + } + else + { + try + { + log.DebugFormat("Testing {0} {1} for possible replacement", mod.Key, mod.Value); + // Check if replacement is available + + ModuleReplacement replacement = registry.GetReplacement(mod.Key, ksp.VersionCriteria()); + if (replacement != null) + { + // Replaceable + log.InfoFormat("Replacement {0} {1} found for {2} {3}", + replacement.ReplaceWith.identifier, replacement.ReplaceWith.version, + replacement.ToReplace.identifier, replacement.ToReplace.version); + to_replace.Add(replacement); + } + } + catch (ModuleNotFoundKraken) + { + log.InfoFormat("{0} is installed, but it or its replacement is not in the registry", + mod.Key); + } + } + } + } + else + { + foreach (string mod in options.modules) + { + try + { + log.DebugFormat("Checking that {0} is installed", mod); + CkanModule modToReplace = registry.GetInstalledVersion(mod); + if (modToReplace != null) + { + log.DebugFormat("Testing {0} {1} for possible replacement", modToReplace.identifier, modToReplace.version); + try + { + // Check if replacement is available + ModuleReplacement replacement = registry.GetReplacement(modToReplace.identifier, ksp.VersionCriteria()); + if (replacement != null) + { + // Replaceable + log.InfoFormat("Replacement {0} {1} found for {2} {3}", + replacement.ReplaceWith.identifier, replacement.ReplaceWith.version, + replacement.ToReplace.identifier, replacement.ToReplace.version); + to_replace.Add(replacement); + } + if (modToReplace.replaced_by != null) + { + log.InfoFormat("Attempt to replace {0} failed, replacement {1} is not compatible", + mod, modToReplace.replaced_by.name); + } + else + { + log.InfoFormat("Mod {0} has no replacement defined for the current version {1}", + modToReplace.identifier, modToReplace.version); + } + } + catch (ModuleNotFoundKraken) + { + log.InfoFormat("{0} is installed, but its replacement {1} is not in the registry", + mod, modToReplace.replaced_by.name); + } + } + } + catch (ModuleNotFoundKraken kraken) + { + User.RaiseMessage("Module {0} not found", kraken.module); + } + } + } + if (to_replace.Count() != 0) + { + User.RaiseMessage("\r\nReplacing modules...\r\n"); + foreach (ModuleReplacement r in to_replace) + { + User.RaiseMessage("Replacement {0} {1} found for {2} {3}", + r.ReplaceWith.identifier, r.ReplaceWith.version, + r.ToReplace.identifier, r.ToReplace.version); + } + + bool ok = User.RaiseYesNoDialog("\r\nContinue?"); + + if (!ok) + { + User.RaiseMessage("Replacements canceled at user request."); + return Exit.ERROR; + } + + // TODO: These instances all need to go. + try + { + ModuleInstaller.GetInstance(ksp, manager.Cache, User).Replace(to_replace, replace_ops, new NetAsyncModulesDownloader(User)); + User.RaiseMessage("\r\nDone!\r\n"); + } + catch (DependencyNotSatisfiedKraken ex) + { + User.RaiseMessage("Dependencies not satisfied for replacement, {0} requires {1} {2} but it is not listed in the index, or not available for your version of KSP.", ex.parent, ex.module, ex.version); + } + } + else + { + User.RaiseMessage("No replacements found."); + return Exit.OK; + } + + return Exit.OK; + } + } +} diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 413d105cd7..9d063c7470 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -35,8 +35,8 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) UpgradeOptions options = (UpgradeOptions) raw_options; if (options.ckan_file != null) - { - options.modules.Add(LoadCkanFromFile(ksp, options.ckan_file).identifier); + { + options.modules.Add(MainClass.LoadCkanFromFile(ksp, options.ckan_file).identifier); } if (options.modules.Count == 0 && ! options.upgrade_all) @@ -152,21 +152,5 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) return Exit.OK; } - - internal static CkanModule LoadCkanFromFile(CKAN.KSP current_instance, string ckan_file) - { - CkanModule module = CkanModule.FromFile(ckan_file); - - // We'll need to make some registry changes to do this. - RegistryManager registry_manager = RegistryManager.Instance(current_instance); - - // Remove this version of the module in the registry, if it exists. - registry_manager.registry.RemoveAvailable(module); - - // Sneakily add our version in... - registry_manager.registry.AddAvailable(module); - - return module; - } } } diff --git a/Cmdline/CKAN-cmdline.csproj b/Cmdline/CKAN-cmdline.csproj index bb46b25234..e2813837f6 100644 --- a/Cmdline/CKAN-cmdline.csproj +++ b/Cmdline/CKAN-cmdline.csproj @@ -39,14 +39,14 @@ ..\_build\lib\nuget\CommandLineParser.1.9.71\lib\net45\CommandLine.dll - + + + ..\_build\lib\nuget\log4net.2.0.8\lib\net45-full\log4net.dll - + ..\_build\lib\nuget\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - @@ -69,6 +69,7 @@ + diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index 1f91600822..130a596bfa 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -203,6 +203,14 @@ private static int RunSimpleAction(Options cmdline, CommonOptions options, strin case "show": return (new Show(user)).RunCommand(GetGameInstance(manager), cmdline.options); + case "replace": + Scan(GetGameInstance(manager), user, cmdline.action); + return (new Replace(manager, user)).RunCommand(GetGameInstance(manager), (ReplaceOptions)cmdline.options); + + case "upgrade": + Scan(GetGameInstance(manager), user, cmdline.action); + return (new Upgrade(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); + case "search": return (new Search(user)).RunCommand(GetGameInstance(manager), options); @@ -210,10 +218,6 @@ private static int RunSimpleAction(Options cmdline, CommonOptions options, strin case "remove": return (new Remove(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); - case "upgrade": - Scan(GetGameInstance(manager), user, cmdline.action); - return (new Upgrade(manager, user)).RunCommand(GetGameInstance(manager), cmdline.options); - case "import": return (new Import(manager, user)).RunCommand(GetGameInstance(manager), options); @@ -232,6 +236,26 @@ private static int RunSimpleAction(Options cmdline, CommonOptions options, strin { return printMissingInstanceError(user); } + finally + { + RegistryManager.DisposeAll(); + } + } + + internal static CkanModule LoadCkanFromFile(CKAN.KSP current_instance, string ckan_file) + { + CkanModule module = CkanModule.FromFile(ckan_file); + + // We'll need to make some registry changes to do this. + RegistryManager registry_manager = RegistryManager.Instance(current_instance); + + // Remove this version of the module in the registry, if it exists. + registry_manager.registry.RemoveAvailable(module); + + // Sneakily add our version in... + registry_manager.registry.AddAvailable(module); + + return module; } private static int printMissingInstanceError(IUser user) diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index 1fee35cfa4..7c17e622bc 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -88,6 +88,9 @@ internal class Actions : VerbCommandOptions [VerbOption("repair", HelpText = "Attempt various automatic repairs")] public SubCommandOptions Repair { get; set; } + [VerbOption("replace", HelpText = "Replace list of replaceable mods")] + public ReplaceOptions Replace { get; set; } + [VerbOption("repo", HelpText = "Manage CKAN repositories")] public SubCommandOptions Repo { get; set; } @@ -418,7 +421,34 @@ internal class UpgradeOptions : InstanceSpecificOptions public List modules { get; set; } } - internal class ScanOptions : InstanceSpecificOptions { } + internal class ReplaceOptions : InstanceSpecificOptions + { + [Option('c', "ckanfile", HelpText = "Local CKAN file to process")] + public string ckan_file { get; set; } + + [Option("no-recommends", HelpText = "Do not install recommended modules")] + public bool no_recommends { get; set; } + + [Option("with-suggests", HelpText = "Install suggested modules")] + public bool with_suggests { get; set; } + + [Option("with-all-suggests", HelpText = "Install suggested modules all the way down")] + public bool with_all_suggests { get; set; } + + [Option("allow-incompatible", DefaultValue = false, HelpText = "Install modules that are not compatible with the current game version")] + public bool allow_incompatible { get; set; } + + [Option("all", HelpText = "Replace all available replaced modules")] + public bool replace_all { get; set; } + + // TODO: How do we provide helptext on this? + [ValueList(typeof (List))] + public List modules { get; set; } + } + + internal class ScanOptions : InstanceSpecificOptions + { + } internal class ListOptions : InstanceSpecificOptions { diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index 999cd38b76..3af4b7494a 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.ComponentModel; using System.Collections.Generic; using CKAN.ConsoleUI.Toolkit; @@ -31,6 +32,10 @@ public DependencyScreen(KSPManager mgr, ChangePlan cp, HashSet rej, bool )); generateList(plan.Install); + generateList(new HashSet( + ReplacementIdentifiers(plan.Replace) + .Select(id => registry.InstalledModule(id).Module) + )); dependencyList = new ConsoleListBox( 1, 4, -1, -2, @@ -147,6 +152,18 @@ private void generateList(HashSet inst) } } + private IEnumerable ReplacementIdentifiers(IEnumerable replaced_identifiers) + { + foreach (string replaced in replaced_identifiers) { + ModuleReplacement repl = registry.GetReplacement( + replaced, manager.CurrentInstance.VersionCriteria() + ); + if (repl != null) { + yield return repl.ReplaceWith.identifier; + } + } + } + private void AddDependencies(HashSet alreadyInstalling, CkanModule dependent, List source, bool installByDefault) { if (source != null) { diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index ecbc944a9d..7bc5bb4d3b 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -71,6 +71,10 @@ public override void Run(Action process = null) inst.InstallList(iList, resolvOpts, dl); plan.Install.Clear(); } + if (plan.Replace.Count > 0) { + inst.Replace(AllReplacements(plan.Replace), resolvOpts, dl, true); + } + trans.Complete(); // Don't let the installer re-use old screen references inst.User = null; @@ -141,6 +145,20 @@ private void OnModInstalled(CkanModule mod) RaiseMessage($"{Symbols.checkmark} Successfully installed {mod.name} {ModuleInstaller.StripEpoch(mod.version)}"); } + private IEnumerable AllReplacements(IEnumerable identifiers) + { + IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance).registry; + + foreach (string id in identifiers) { + ModuleReplacement repl = registry.GetReplacement( + id, manager.CurrentInstance.VersionCriteria() + ); + if (repl != null) { + yield return repl; + } + } + } + private static readonly RelationshipResolverOptions resolvOpts = new RelationshipResolverOptions() { with_all_suggests = false, with_suggests = false, diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index adf486a8df..ffc8c9425e 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -328,14 +328,45 @@ private int addVersionDisplay() if (latestIsInstalled) { - addVersionBox( - boxLeft, boxTop, boxRight, boxTop + boxH - 1, - () => $"Latest/Installed {instTime?.ToString("d") ?? "manually"}", - () => ConsoleTheme.Current.ActiveFrameFg, - true, - new List() {inst} + ModuleReplacement mr = registry.GetReplacement( + mod.identifier, + manager.CurrentInstance.VersionCriteria() ); - boxTop += boxH; + + if (mr != null) { + + // Show replaced_by + addVersionBox( + boxLeft, boxTop, boxRight, boxTop + boxH - 1, + () => $"Replaced by {mr.ReplaceWith.identifier}", + () => ConsoleTheme.Current.AlertFrameFg, + false, + new List() {mr.ReplaceWith} + ); + boxTop += boxH; + + addVersionBox( + boxLeft, boxTop, boxRight, boxTop + boxH - 1, + () => $"Installed {instTime?.ToString("d") ?? "manually"}", + () => ConsoleTheme.Current.ActiveFrameFg, + true, + new List() {inst} + ); + boxTop += boxH; + + } else { + + addVersionBox( + boxLeft, boxTop, boxRight, boxTop + boxH - 1, + () => $"Latest/Installed {instTime?.ToString("d") ?? "manually"}", + () => ConsoleTheme.Current.ActiveFrameFg, + true, + new List() {inst} + ); + boxTop += boxH; + + } + } else { diff --git a/ConsoleUI/ModListHelpDialog.cs b/ConsoleUI/ModListHelpDialog.cs index 7915dacbda..900386c3ed 100644 --- a/ConsoleUI/ModListHelpDialog.cs +++ b/ConsoleUI/ModListHelpDialog.cs @@ -14,7 +14,7 @@ public class ModListHelpDialog : ConsoleDialog { /// public ModListHelpDialog() : base() { - SetDimensions(9, 4, -9, -3); + SetDimensions(9, 3, -9, -3); int btnW = 10; int btnL = (Console.WindowWidth - btnW) / 2; @@ -32,6 +32,7 @@ public ModListHelpDialog() : base() symbolTb.AddLine($"{installed} Installed"); symbolTb.AddLine($"{upgradable} Upgradeable"); symbolTb.AddLine($"{autodetected} Manually installed"); + symbolTb.AddLine($"{replaceable} Replaceable"); symbolTb.AddLine($"! Unavailable"); symbolTb.AddLine(" "); symbolTb.AddLine("Basic Keys"); @@ -67,6 +68,7 @@ public ModListHelpDialog() : base() private static readonly string installed = Symbols.checkmark; private static readonly string upgradable = Symbols.greaterEquals; private static readonly string autodetected = Symbols.infinity; + private static readonly string replaceable = Symbols.doubleGreater; } } diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index cfe258090d..5882e0b9b6 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -171,6 +171,10 @@ public ModListScreen(KSPManager mgr, bool dbg) () => moduleList.Selection != null && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) ); + moduleList.AddTip("+", "Replace", + () => moduleList.Selection != null + && registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null + ); moduleList.AddBinding(Keys.Plus, (object sender) => { if (moduleList.Selection != null) { if (!registry.IsInstalled(moduleList.Selection.identifier, false)) { @@ -178,6 +182,8 @@ public ModListScreen(KSPManager mgr, bool dbg) } else if (registry.IsInstalled(moduleList.Selection.identifier, false) && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria())) { plan.ToggleUpgrade(moduleList.Selection); + } else if (registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null) { + plan.ToggleReplace(moduleList.Selection.identifier); } } return true; @@ -551,6 +557,8 @@ public static string StatusSymbol(InstallStatus st) case InstallStatus.Installing: return installing; case InstallStatus.NotInstalled: return notinstalled; case InstallStatus.AutoDetected: return autodetected; + case InstallStatus.Replaceable: return replaceable; + case InstallStatus.Replacing: return replacing; default: return ""; } } @@ -586,6 +594,8 @@ private long totalInstalledDownloadSize() private static readonly string upgrading = "^"; private static readonly string removing = "-"; private static readonly string autodetected = Symbols.infinity; + private static readonly string replaceable = Symbols.doubleGreater; + private static readonly string replacing = Symbols.plusMinus; } /// @@ -631,12 +641,22 @@ public void ToggleUpgrade(CkanModule mod) toggleContains(Upgrade, mod.identifier); } + /// + /// Add or remove a mod from the replace list + /// + /// The mod to Replace + public void ToggleReplace(string identifier) + { + Remove.Remove(identifier); + toggleContains(Replace, identifier); + } + /// /// Return true if we are planning to make any changes, false otherwise /// public bool NonEmpty() { - return Install.Count > 0 || Upgrade.Count > 0 || Remove.Count > 0; + return Install.Count > 0 || Upgrade.Count > 0 || Remove.Count > 0 || Replace.Count > 0; } /// @@ -674,6 +694,10 @@ public InstallStatus GetModStatus(KSPManager manager, IRegistryQuerier registry, } else { return InstallStatus.Upgradeable; } + } else if (Replace.Contains(identifier)) { + return InstallStatus.Replacing; + } else if (registry.GetReplacement(identifier, manager.CurrentInstance.VersionCriteria()) != null) { + return InstallStatus.Replaceable; } else if (!IsAnyAvailable(registry, identifier)) { return InstallStatus.Unavailable; } else { @@ -755,6 +779,11 @@ public static void toggleContains(HashSet list, CkanModule mod) /// Mods we're planning to remove /// public readonly HashSet Remove = new HashSet(); + + /// + /// Mods we're planning to replace with successor mods + /// + public readonly HashSet Replace = new HashSet(); } /// @@ -801,5 +830,16 @@ public enum InstallStatus { /// This mod was installed manually /// AutoDetected, + + /// + /// This mod is installed and can be replaced by a successor mod + /// + Replaceable, + + /// + /// This mod is installed and we are planning to replace it + /// + Replacing, + }; } diff --git a/ConsoleUI/Toolkit/Symbols.cs b/ConsoleUI/Toolkit/Symbols.cs index 0c9b395ede..bb877732f8 100644 --- a/ConsoleUI/Toolkit/Symbols.cs +++ b/ConsoleUI/Toolkit/Symbols.cs @@ -30,6 +30,15 @@ public static class Symbols { /// Infinity symbol /// public static readonly string infinity = cp437s(0xec); + /// + /// +- symbol + /// + public static readonly string plusMinus = cp437s(0xf1); + /// + /// >> symbol + /// + public static readonly string doubleGreater = cp437s(0xaf); + /// /// Hashed square box for drawing scrollbars diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 6208a6ea63..0142081cd4 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -1096,6 +1096,99 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa ); } + /// + /// 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. + /// Throws ModuleNotFoundKraken if a module is not installed. + /// + public void Replace(IEnumerable replacements, RelationshipResolverOptions options, IDownloader netAsyncDownloader, bool enforceConsistency = true) + { + log.Debug("Using Replace method"); + List modsToInstall = new List(); + var modsToRemove = new List(); + foreach (ModuleReplacement repl in replacements) + { + modsToInstall.Add(repl.ReplaceWith); + log.DebugFormat("We want to install {0} as a replacement for {1}", repl.ReplaceWith.identifier, repl.ToReplace.identifier); + } + // Start by making sure we've downloaded everything. + DownloadModules(modsToInstall, netAsyncDownloader); + + // Our replacement involves removing the currently installed mods, then + // adding everything that needs installing (which may involve new mods to + // satisfy dependencies). + + + // Let's discover what we need to do with each module! + foreach (ModuleReplacement repl in replacements) + { + string ident = repl.ToReplace.identifier; + InstalledModule installedMod = registry_manager.registry.InstalledModule(ident); + + if (installedMod == null) + { + log.DebugFormat("Wait, {0} is not actually installed?", installedMod.identifier); + //Maybe ModuleNotInstalled ? + if (registry_manager.registry.IsAutodetected(ident)) + { + throw new ModuleNotFoundKraken(ident, repl.ToReplace.version.ToString(), String.Format("Can't replace {0} as it was not installed by CKAN. \r\n Please remove manually before trying to install it.", ident)); + } + + throw new ModuleNotFoundKraken(ident, repl.ToReplace.version.ToString(), String.Format("Can't replace {0} as it is not installed. Please attempt to install {1} instead.", ident, repl.ReplaceWith.identifier)); + } + else + { + // Obviously, we need to remove the mod we are replacing + modsToRemove.Add(repl.ToReplace.identifier); + + log.DebugFormat("Ok, we are removing {0}", repl.ToReplace.identifier); + //Check whether our Replacement target is already installed + InstalledModule installed_replacement = registry_manager.registry.InstalledModule(repl.ReplaceWith.identifier); + + // If replacement is not installed, we've already added it to modsToInstall above + if (installed_replacement != null) + { + //Module already installed. We'll need to treat it as an upgrade. + log.DebugFormat("It turns out {0} is already installed, we'll upgrade it.", installed_replacement.identifier); + modsToRemove.Add(installed_replacement.identifier); + + CkanModule installed = installed_replacement.Module; + if (installed.version.IsEqualTo(repl.ReplaceWith.version)) + { + log.InfoFormat("{0} is already at the latest version, reinstalling to replace {1}", repl.ReplaceWith.identifier, repl.ToReplace.identifier); + } + else if (installed.version.IsGreaterThan(repl.ReplaceWith.version)) + { + log.WarnFormat("Downgrading {0} from {1} to {2} to replace {3}", repl.ReplaceWith.identifier, repl.ReplaceWith.version, repl.ReplaceWith.version, repl.ToReplace.identifier); + } + else + { + log.InfoFormat("Upgrading {0} to {1} to replace {2}", repl.ReplaceWith.identifier, repl.ReplaceWith.version, repl.ToReplace.identifier); + } + } + else + { + log.InfoFormat("Replacing {0} with {1} {2}", repl.ToReplace.identifier, repl.ReplaceWith.identifier, repl.ReplaceWith.version); + } + } + } + var resolver = new RelationshipResolver(modsToInstall, null, options, registry_manager.registry, ksp.VersionCriteria()); + try + { + var resolvedModsToInstall = resolver.ModList().ToList(); + AddRemove( + resolvedModsToInstall, + modsToRemove, + enforceConsistency + ); + } + catch (DependencyNotSatisfiedKraken kraken) + { + throw kraken; + } + + } + #endregion /// diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index f8826a4791..86804cf452 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -212,5 +212,78 @@ public static string CompatibleGameVersions(this IRegistryQuerier querier, CkanM ); return KspVersionRange.VersionSpan(minKsp, maxKsp); } + + /// + /// Is the mod installed and does it have a replaced_by relationship with a compatible version + /// Check latest information on installed version of mod "identifier" and if it has a "replaced_by" + /// value, check if there is a compatible version of the linked mod + /// Given a mod identifier, return a ModuleReplacement containing the relevant replacement + /// if compatibility matches. + /// + public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, string identifier, KspVersionCriteria version) + { + // We only care about the installed version + CkanModule installedVersion; + try + { + installedVersion = querier.GetInstalledVersion(identifier); + } + catch (ModuleNotFoundKraken) + { + return null; + } + return querier.GetReplacement(installedVersion, version); + } + + public static ModuleReplacement GetReplacement(this IRegistryQuerier querier, CkanModule installedVersion, KspVersionCriteria version) + { + // Mod is not installed, so we don't care about replacements + if (installedVersion == null) + return null; + // No replaced_by relationship + if (installedVersion.replaced_by == null) + return null; + + // Get the identifier from the replaced_by relationship, if it exists + ModuleRelationshipDescriptor replacedBy = installedVersion.replaced_by; + + // Now we need to see if there is a compatible version of the replacement + try + { + ModuleReplacement replacement = new ModuleReplacement(); + replacement.ToReplace = installedVersion; + if (installedVersion.replaced_by.version != null) + { + replacement.ReplaceWith = querier.GetModuleByVersion(installedVersion.replaced_by.name, installedVersion.replaced_by.version); + if (replacement.ReplaceWith != null) + { + if (replacement.ReplaceWith.IsCompatibleKSP(version)) + { + return replacement; + } + } + } + else + { + replacement.ReplaceWith = querier.LatestAvailable(installedVersion.replaced_by.name, version); + if (replacement.ReplaceWith != null) + { + if (installedVersion.replaced_by.min_version != null) + { + if (!replacement.ReplaceWith.version.IsLessThan(replacedBy.min_version)) + { + return replacement; + } + } + else return replacement; + } + } + return null; + } + catch (ModuleNotFoundKraken) + { + return null; + } + } } } diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index c9570cfa2c..ae597a3820 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -674,6 +674,20 @@ public override string Reason } } + public class Replacement : SelectionReason + { + public Replacement(CkanModule module) + { + if (module == null) throw new ArgumentNullException(); + Parent = module; + } + + public override string Reason + { + get { return " Replacing " + Parent.name + ".\r\n"; } + } + } + public sealed class Suggested : SelectionReason { public Suggested(CkanModule module) diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index ad36b2bfc6..3071cca452 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -262,6 +262,12 @@ public override string ToString() } } + public class ModuleReplacement + { + public CkanModule ToReplace; + public CkanModule ReplaceWith; + } + public class ResourcesDescriptor { [JsonProperty("repository", NullValueHandling = NullValueHandling.Ignore)] @@ -360,6 +366,9 @@ public class CkanModule : IEquatable [JsonConverter(typeof(JsonRelationshipConverter))] public List depends; + [JsonProperty("replaced_by", NullValueHandling = NullValueHandling.Ignore)] + public ModuleRelationshipDescriptor replaced_by; + [JsonProperty("download")] public Uri download; @@ -608,6 +617,7 @@ public static CkanModule FromIDandVersion(IRegistryQuerier registry, string mod, ); /// Generates a CKAN.Meta object given a filename + /// TODO: Catch and display errors public static CkanModule FromFile(string filename) { string json = File.ReadAllText(filename); @@ -664,7 +674,7 @@ internal static bool UniConflicts(CkanModule mod1, CkanModule mod2) /// public bool IsCompatibleKSP(KspVersionCriteria version) { - log.DebugFormat("Testing if {0} is compatible with KSP {1}", this, version); + log.DebugFormat("Testing if {0} is compatible with KSP {1}", this, version.ToString()); return _comparator.Compatible(version, this); diff --git a/Core/Versioning/KspVersionCriteria.cs b/Core/Versioning/KspVersionCriteria.cs index 0b06b85757..a4e433b3e1 100644 --- a/Core/Versioning/KspVersionCriteria.cs +++ b/Core/Versioning/KspVersionCriteria.cs @@ -44,7 +44,12 @@ public KspVersionCriteria Union(KspVersionCriteria other) public override String ToString() { - return "[Versions: " + _versions.ToString() + "]"; + List versionList = new List(); + foreach (KspVersion version in _versions) + { + versionList.Add(version.ToString()); + } + return "[Versions: " + String.Join( ", ", versionList) + "]"; } } } diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index c266ed428f..e82dcf4976 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -12,6 +12,8 @@ v4.5 512 false + ..\_build\out\$(AssemblyName)\$(Configuration)\bin\ + ..\_build\out\$(AssemblyName)\$(Configuration)\obj\ publish\ true Disk @@ -26,8 +28,6 @@ 1.0.0.0 false true - ..\_build\out\$(AssemblyName)\$(Configuration)\bin\ - ..\_build\out\$(AssemblyName)\$(Configuration)\obj\ AnyCPU @@ -357,6 +357,8 @@ PreserveNewest + + diff --git a/GUI/GUIMod.cs b/GUI/GUIMod.cs index abd3b84718..60a21362b3 100644 --- a/GUI/GUIMod.cs +++ b/GUI/GUIMod.cs @@ -14,6 +14,7 @@ public sealed class GUIMod public string Name { get; private set; } public bool IsInstalled { get; private set; } public bool HasUpdate { get; private set; } + public bool HasReplacement { get; private set; } public bool IsIncompatible { get; private set; } public bool IsAutodetected { get; private set; } public string Authors { get; private set; } @@ -35,6 +36,7 @@ public sealed class GUIMod public string Identifier { get; private set; } public bool IsInstallChecked { get; set; } public bool IsUpgradeChecked { get; private set; } + public bool IsReplaceChecked { get; private set; } public bool IsNew { get; set; } public bool IsCKAN { get; private set; } public string Abbrevation { get; private set; } @@ -101,6 +103,7 @@ public GUIMod(CkanModule mod, IRegistryQuerier registry, KspVersionCriteria curr Authors = mod.author == null ? "N/A" : String.Join(",", mod.author); HasUpdate = registry.HasUpdate(mod.identifier, current_ksp_version); + HasReplacement = registry.GetReplacement(mod, current_ksp_version) != null; DownloadSize = mod.download_size == 0 ? "N/A" : CkanModule.FmtSize(mod.download_size); IsIncompatible = IsIncompatible || !mod.IsCompatibleKSP(current_ksp_version); @@ -223,6 +226,10 @@ public CkanModule ToModule() { return new KeyValuePair(this, GUIModChangeType.Update); } + if (IsReplaceChecked) + { + return new KeyValuePair(this, GUIModChangeType.Replace); + } return null; } @@ -243,11 +250,16 @@ public void SetRequestedChange(GUIModChangeType change) case GUIModChangeType.Remove: IsInstallChecked = false; IsUpgradeChecked = false; + IsReplaceChecked = false; break; case GUIModChangeType.Update: IsInstallChecked = true; IsUpgradeChecked = true; break; + case GUIModChangeType.Replace: + IsInstallChecked = true; + IsReplaceChecked = true; + break; } } @@ -260,11 +272,15 @@ public void SetUpgradeChecked(DataGridViewRow row, bool? set_value_to = null) { //Contract.Requires(row.Cells[1] is DataGridViewCheckBoxCell); var update_cell = row.Cells[1] as DataGridViewCheckBoxCell; - var old_value = (bool) update_cell.Value; + if (update_cell != null) + { + var old_value = (bool) update_cell.Value; - bool value = set_value_to ?? old_value; - IsUpgradeChecked = value; - if (old_value != value) update_cell.Value = value; + bool value = set_value_to ?? old_value; + IsUpgradeChecked = value; + if (old_value != value) + update_cell.Value = value; + } } public void SetInstallChecked(DataGridViewRow row, bool? set_value_to = null) @@ -294,6 +310,20 @@ public void SetInstallChecked(DataGridViewRow row, bool? set_value_to = null) } } + public void SetReplaceChecked(DataGridViewRow row, bool? set_value_to = null) + { + var replace_cell = row.Cells[2] as DataGridViewCheckBoxCell; + if (replace_cell != null) + { + var old_value = (bool) replace_cell.Value; + + bool value = set_value_to ?? old_value; + IsReplaceChecked = value; + if (old_value != value) + replace_cell.Value = value; + } + } + private bool Equals(GUIMod other) { return Equals(Identifier, other.Identifier); diff --git a/GUI/Main.Designer.cs b/GUI/Main.Designer.cs index f8115b26c2..aabf7f22c3 100644 --- a/GUI/Main.Designer.cs +++ b/GUI/Main.Designer.cs @@ -60,6 +60,7 @@ private void InitializeComponent() this.FilterCompatibleButton = new System.Windows.Forms.ToolStripMenuItem(); this.FilterInstalledButton = new System.Windows.Forms.ToolStripMenuItem(); this.FilterInstalledUpdateButton = new System.Windows.Forms.ToolStripMenuItem(); + this.FilterReplaceableButton = new System.Windows.Forms.ToolStripMenuItem(); this.cachedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.FilterNewButton = new System.Windows.Forms.ToolStripMenuItem(); this.FilterNotInstalledButton = new System.Windows.Forms.ToolStripMenuItem(); @@ -72,6 +73,7 @@ private void InitializeComponent() this.InstallAllCheckbox = new System.Windows.Forms.CheckBox(); this.Installed = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.UpdateCol = new System.Windows.Forms.DataGridViewCheckBoxColumn(); + this.ReplaceCol = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.ModName = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.Author = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.InstalledVersion = new System.Windows.Forms.DataGridViewTextBoxColumn(); @@ -225,7 +227,6 @@ private void InitializeComponent() // this.toolStripSeparator2.Name = "toolStripSeparator2"; this.toolStripSeparator2.Size = new System.Drawing.Size(278, 6); - // // // importDownloadsToolStripMenuItem // @@ -238,14 +239,12 @@ private void InitializeComponent() // this.toolStripSeparator3.Name = "toolStripSeparator3"; this.toolStripSeparator3.Size = new System.Drawing.Size(278, 6); - // // // toolStripSeparator7 // this.toolStripSeparator7.Name = "toolStripSeparator7"; this.toolStripSeparator7.Size = new System.Drawing.Size(278, 6); // - // // importDownloadsToolStripMenuItem // this.auditRecommendationsMenuItem.Name = "auditRecommendationsMenuItem"; @@ -322,7 +321,6 @@ private void InitializeComponent() this.aboutToolStripMenuItem.Size = new System.Drawing.Size(230, 30); this.aboutToolStripMenuItem.Text = "About"; this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click); - // // // statusStrip1 // @@ -400,6 +398,7 @@ private void InitializeComponent() this.FilterCompatibleButton, this.FilterInstalledButton, this.FilterInstalledUpdateButton, + this.FilterReplaceableButton, this.cachedToolStripMenuItem, this.FilterNewButton, this.FilterNotInstalledButton, @@ -431,7 +430,14 @@ private void InitializeComponent() this.FilterInstalledUpdateButton.Size = new System.Drawing.Size(307, 30); this.FilterInstalledUpdateButton.Text = "Installed (update available)"; this.FilterInstalledUpdateButton.Click += new System.EventHandler(this.FilterInstalledUpdateButton_Click); - // + // + // FilterReplaceableButton + // + this.FilterReplaceableButton.Name = "FilterReplaceableButton"; + this.FilterReplaceableButton.Size = new System.Drawing.Size(307, 30); + this.FilterReplaceableButton.Text = "Replaceable"; + this.FilterReplaceableButton.Click += new System.EventHandler(this.FilterReplaceableButton_Click); + // // cachedToolStripMenuItem // this.cachedToolStripMenuItem.Name = "cachedToolStripMenuItem"; @@ -500,6 +506,7 @@ private void InitializeComponent() // this.splitContainer1.Panel1.Controls.Add(this.MainTabControl); this.splitContainer1.Panel1MinSize = 200; + // // splitContainer1.Panel2 // this.splitContainer1.Panel2.Controls.Add(this.ModInfoTabControl); @@ -526,6 +533,7 @@ private void InitializeComponent() this.ModList.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.Installed, this.UpdateCol, + this.ReplaceCol, this.ModName, this.Author, this.InstalledVersion, @@ -565,7 +573,14 @@ private void InitializeComponent() this.UpdateCol.Name = "UpdateCol"; this.UpdateCol.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.UpdateCol.Width = 46; - // + // + // ReplaceCol + // + this.ReplaceCol.HeaderText = "Replace"; + this.ReplaceCol.Name = "ReplaceCol"; + this.ReplaceCol.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; + this.ReplaceCol.Width = 46; + // // ModName // this.ModName.HeaderText = "Name"; @@ -724,7 +739,6 @@ private void InitializeComponent() this.ManageModsTabPage.Size = new System.Drawing.Size(1536, 948); this.ManageModsTabPage.TabIndex = 0; this.ManageModsTabPage.Text = "Manage mods"; - // // // InstallAllCheckbox // @@ -1320,6 +1334,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem FilterCompatibleButton; private System.Windows.Forms.ToolStripMenuItem FilterInstalledButton; private System.Windows.Forms.ToolStripMenuItem FilterInstalledUpdateButton; + private System.Windows.Forms.ToolStripMenuItem FilterReplaceableButton; private System.Windows.Forms.ToolStripMenuItem cachedToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem FilterNewButton; private System.Windows.Forms.ToolStripMenuItem FilterNotInstalledButton; @@ -1332,6 +1347,7 @@ private void InitializeComponent() private System.Windows.Forms.CheckBox InstallAllCheckbox; private System.Windows.Forms.DataGridViewCheckBoxColumn Installed; private System.Windows.Forms.DataGridViewCheckBoxColumn UpdateCol; + private System.Windows.Forms.DataGridViewCheckBoxColumn ReplaceCol; private System.Windows.Forms.DataGridViewTextBoxColumn ModName; private System.Windows.Forms.DataGridViewTextBoxColumn Author; private System.Windows.Forms.DataGridViewTextBoxColumn InstalledVersion; diff --git a/GUI/Main.cs b/GUI/Main.cs index af2f961abc..b178b72197 100644 --- a/GUI/Main.cs +++ b/GUI/Main.cs @@ -17,26 +17,6 @@ namespace CKAN { - public enum GUIModFilter - { - Compatible = 0, - Installed = 1, - InstalledUpdateAvailable = 2, - NewInRepository = 3, - NotInstalled = 4, - Incompatible = 5, - All = 6, - Cached = 7 - } - - public enum GUIModChangeType - { - None = 0, - Install = 1, - Remove = 2, - Update = 3 - } - public partial class Main : Form { private static readonly ILog log = LogManager.GetLogger(typeof(Main)); @@ -192,7 +172,8 @@ public Main(string[] cmdlineArgs, KSPManager mgr, GUIUser user, bool showConsole settingsToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer(); helpToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer(); FilterToolButton.DropDown.Renderer = new FlatToolStripRenderer(); - this.minimizedContextMenuStrip.Renderer = new FlatToolStripRenderer(); + minimizedContextMenuStrip.Renderer = new FlatToolStripRenderer(); + ModListContextMenuStrip.Renderer = new FlatToolStripRenderer(); } // Initialize all user interaction dialogs. @@ -744,6 +725,11 @@ private void FilterInstalledUpdateButton_Click(object sender, EventArgs e) Filter(GUIModFilter.InstalledUpdateAvailable); } + private void FilterReplaceableButton_Click(object sender, EventArgs e) + { + Filter(GUIModFilter.Replaceable); + } + private void cachedToolStripMenuItem_Click(object sender, EventArgs e) { Filter(GUIModFilter.Cached); @@ -773,22 +759,18 @@ private void Filter(GUIModFilter filter) { mainModList.ModFilter = filter; - if (filter == GUIModFilter.All) - FilterToolButton.Text = "Filter (All)"; - else if (filter == GUIModFilter.Incompatible) - FilterToolButton.Text = "Filter (Incompatible)"; - else if (filter == GUIModFilter.Installed) - FilterToolButton.Text = "Filter (Installed)"; - else if (filter == GUIModFilter.InstalledUpdateAvailable) - FilterToolButton.Text = "Filter (Upgradeable)"; - else if (filter == GUIModFilter.Cached) - FilterToolButton.Text = "Filter (Cached)"; - else if (filter == GUIModFilter.NewInRepository) - FilterToolButton.Text = "Filter (New)"; - else if (filter == GUIModFilter.NotInstalled) - FilterToolButton.Text = "Filter (Not installed)"; - else - FilterToolButton.Text = "Filter (Compatible)"; + switch (filter) + { + case GUIModFilter.All: FilterToolButton.Text = "Filter (All)"; break; + case GUIModFilter.Incompatible: FilterToolButton.Text = "Filter (Incompatible)"; break; + case GUIModFilter.Installed: FilterToolButton.Text = "Filter (Installed)"; break; + case GUIModFilter.InstalledUpdateAvailable: FilterToolButton.Text = "Filter (Upgradeable)"; break; + case GUIModFilter.Replaceable: FilterToolButton.Text = "Filter (Replaceable)"; break; + case GUIModFilter.Cached: FilterToolButton.Text = "Filter (Cached)"; break; + case GUIModFilter.NewInRepository: FilterToolButton.Text = "Filter (New)"; break; + case GUIModFilter.NotInstalled: FilterToolButton.Text = "Filter (Not installed)"; break; + default: FilterToolButton.Text = "Filter (Compatible)"; break; + } } private GUIMod GetSelectedModule() diff --git a/GUI/MainChangeset.cs b/GUI/MainChangeset.cs index 50718b2da4..7245e97132 100644 --- a/GUI/MainChangeset.cs +++ b/GUI/MainChangeset.cs @@ -23,7 +23,7 @@ public void UpdateChangesDialog(List changeset, BackgroundWorker inst } // We're going to split our change-set into two parts: updated/removed mods, - // and everything else (which right now is installing mods, but we may have + // and everything else (which right now is replacing and installing mods, but we may have // other types in the future). changeSet = new List(); @@ -86,7 +86,10 @@ private void ClearChangeSet() if (mod.IsInstallChecked != mod.IsInstalled) { mod.SetInstallChecked(row, mod.IsInstalled); + } + mod.SetUpgradeChecked(row, false); + mod.SetReplaceChecked(row, false); } } diff --git a/GUI/MainInstall.cs b/GUI/MainInstall.cs index 7e0ad10981..a1fb291def 100644 --- a/GUI/MainInstall.cs +++ b/GUI/MainInstall.cs @@ -79,7 +79,8 @@ await mainModList.ComputeChangeSetFromModList( } } - private void InstallMods(object sender, DoWorkEventArgs e) // this probably needs to be refactored + // this probably needs to be refactored + private void InstallMods(object sender, DoWorkEventArgs e) { installCanceled = false; ClearLog(); @@ -112,6 +113,14 @@ private void InstallMods(object sender, DoWorkEventArgs e) // this probably need case GUIModChangeType.Install: toInstall.Add(change.Mod.ToModule()); break; + case GUIModChangeType.Replace: + ModuleReplacement repl = registry.GetReplacement(change.Mod.ToModule(), CurrentInstance.VersionCriteria()); + if (repl != null) + { + toUninstall.Add(repl.ToReplace.identifier); + toInstall.Add(repl.ReplaceWith); + } + break; } } diff --git a/GUI/MainModInfo.Designer.cs b/GUI/MainModInfo.Designer.cs index c3993c4490..4a1c7a5482 100644 --- a/GUI/MainModInfo.Designer.cs +++ b/GUI/MainModInfo.Designer.cs @@ -38,6 +38,8 @@ private void InitializeComponent() this.MetaDataLowerLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); this.IdentifierLabel = new System.Windows.Forms.Label(); this.MetadataIdentifierTextBox = new TransparentTextBox(); + this.ReplacementLabel = new System.Windows.Forms.Label(); + this.ReplacementTextBox = new TransparentTextBox(); this.KSPCompatibilityLabel = new System.Windows.Forms.Label(); this.ReleaseLabel = new System.Windows.Forms.Label(); this.GitHubLabel = new System.Windows.Forms.Label(); @@ -202,6 +204,7 @@ private void InitializeComponent() this.MetaDataLowerLayoutPanel.Controls.Add(this.ReleaseLabel, 0, 5); this.MetaDataLowerLayoutPanel.Controls.Add(this.KSPCompatibilityLabel, 0, 6); this.MetaDataLowerLayoutPanel.Controls.Add(this.IdentifierLabel, 0, 7); + this.MetaDataLowerLayoutPanel.Controls.Add(this.ReplacementLabel, 0, 8); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleVersionTextBox, 1, 0); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleLicenseTextBox, 1, 1); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleAuthorTextBox, 1, 2); @@ -210,6 +213,7 @@ private void InitializeComponent() this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleReleaseStatusTextBox, 1, 5); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataModuleKSPCompatibilityTextBox, 1, 6); this.MetaDataLowerLayoutPanel.Controls.Add(this.MetadataIdentifierTextBox, 1, 7); + this.MetaDataLowerLayoutPanel.Controls.Add(this.ReplacementTextBox, 1, 8); this.MetaDataLowerLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; this.MetaDataLowerLayoutPanel.Location = new System.Drawing.Point(0, 0); this.MetaDataLowerLayoutPanel.Name = "MetaDataLowerLayoutPanel"; @@ -236,7 +240,7 @@ private void InitializeComponent() this.IdentifierLabel.Name = "IdentifierLabel"; this.IdentifierLabel.Size = new System.Drawing.Size(84, 20); this.IdentifierLabel.TabIndex = 28; - this.IdentifierLabel.Text = "Identifier"; + this.IdentifierLabel.Text = "Identifier:"; // // MetadataIdentifierTextBox // @@ -252,6 +256,31 @@ private void InitializeComponent() this.MetadataIdentifierTextBox.ForeColor = System.Drawing.SystemColors.ControlText; this.MetadataIdentifierTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; // + // ReplacementLabel + // + this.ReplacementLabel.AutoSize = true; + this.ReplacementLabel.Dock = System.Windows.Forms.DockStyle.Fill; + this.ReplacementLabel.ForeColor = System.Drawing.SystemColors.GrayText; + this.ReplacementLabel.Location = new System.Drawing.Point(3, 240); + this.ReplacementLabel.Name = "ReplacementLabel"; + this.ReplacementLabel.Size = new System.Drawing.Size(84, 20); + this.ReplacementLabel.TabIndex = 28; + this.ReplacementLabel.Text = "Replaced by:"; + // + // ReplacementTextBox + // + this.ReplacementTextBox.AutoSize = true; + this.ReplacementTextBox.Dock = System.Windows.Forms.DockStyle.Fill; + this.ReplacementTextBox.Location = new System.Drawing.Point(93, 240); + this.ReplacementTextBox.Name = "ReplacementTextBox"; + this.ReplacementTextBox.Size = new System.Drawing.Size(250, 20); + this.ReplacementTextBox.TabIndex = 27; + this.ReplacementTextBox.Text = "-"; + this.ReplacementTextBox.ReadOnly = true; + this.ReplacementTextBox.BackColor = MetadataTabPage.BackColor; + this.ReplacementTextBox.ForeColor = System.Drawing.SystemColors.ControlText; + this.ReplacementTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; + // // KSPCompatibilityLabel // this.KSPCompatibilityLabel.AutoSize = true; @@ -283,7 +312,7 @@ private void InitializeComponent() this.GitHubLabel.Name = "GitHubLabel"; this.GitHubLabel.Size = new System.Drawing.Size(84, 30); this.GitHubLabel.TabIndex = 10; - this.GitHubLabel.Text = "Source Code:"; + this.GitHubLabel.Text = "Source code:"; // // HomePageLabel // @@ -294,7 +323,7 @@ private void InitializeComponent() this.HomePageLabel.Name = "HomePageLabel"; this.HomePageLabel.Size = new System.Drawing.Size(84, 30); this.HomePageLabel.TabIndex = 7; - this.HomePageLabel.Text = "Homepage:"; + this.HomePageLabel.Text = "Home page:"; // // AuthorLabel // @@ -638,6 +667,8 @@ private void InitializeComponent() private System.Windows.Forms.TableLayoutPanel MetaDataLowerLayoutPanel; private System.Windows.Forms.Label IdentifierLabel; private TransparentTextBox MetadataIdentifierTextBox; + private System.Windows.Forms.Label ReplacementLabel; + private TransparentTextBox ReplacementTextBox; private System.Windows.Forms.Label KSPCompatibilityLabel; private System.Windows.Forms.Label ReleaseLabel; private System.Windows.Forms.Label GitHubLabel; diff --git a/GUI/MainModInfo.cs b/GUI/MainModInfo.cs index 1f9dd120dc..48bc5095ec 100644 --- a/GUI/MainModInfo.cs +++ b/GUI/MainModInfo.cs @@ -140,26 +140,11 @@ private void UpdateModInfo(GUIMod gui_module) Util.Invoke(MetadataIdentifierTextBox, () => MetadataIdentifierTextBox.Text = module.identifier); // If we have a homepage provided, use that; otherwise use the spacedock page, curse page or the github repo so that users have somewhere to get more info than just the abstract. - Util.Invoke(MetadataModuleHomePageLinkLabel, - () => MetadataModuleHomePageLinkLabel.Text = gui_module.Homepage.ToString()); - - if (module.resources != null && module.resources.repository != null) - { - Util.Invoke(MetadataModuleGitHubLinkLabel, - () => MetadataModuleGitHubLinkLabel.Text = module.resources.repository.ToString()); - } - else - { - Util.Invoke(MetadataModuleGitHubLinkLabel, - () => MetadataModuleGitHubLinkLabel.Text = "N/A"); - } - - if (module.release_status != null) - { - Util.Invoke(MetadataModuleReleaseStatusTextBox, () => MetadataModuleReleaseStatusTextBox.Text = module.release_status.ToString()); - } - + Util.Invoke(MetadataModuleHomePageLinkLabel, () => MetadataModuleHomePageLinkLabel.Text = gui_module.Homepage.ToString()); + Util.Invoke(MetadataModuleGitHubLinkLabel,() => MetadataModuleGitHubLinkLabel.Text = module.resources?.repository?.ToString() ?? "N/A"); + Util.Invoke(MetadataModuleReleaseStatusTextBox, () => MetadataModuleReleaseStatusTextBox.Text = module.release_status?.ToString() ?? "N/A"); Util.Invoke(MetadataModuleKSPCompatibilityTextBox, () => MetadataModuleKSPCompatibilityTextBox.Text = gui_module.KSPCompatibilityLong); + Util.Invoke(ReplacementTextBox, () => ReplacementTextBox.Text = gui_module.ToModule()?.replaced_by?.ToString() ?? "N/A"); } private void BeforeExpand(object sender, TreeViewCancelEventArgs args) diff --git a/GUI/MainModList.cs b/GUI/MainModList.cs index 2a22d24b12..bbb117b22e 100644 --- a/GUI/MainModList.cs +++ b/GUI/MainModList.cs @@ -12,6 +12,19 @@ namespace CKAN { + public enum GUIModFilter + { + Compatible = 0, + Installed = 1, + InstalledUpdateAvailable = 2, + NewInRepository = 3, + NotInstalled = 4, + Incompatible = 5, + All = 6, + Cached = 7, + Replaceable = 8 + } + public partial class Main { private IEnumerable _SortRowsByColumn(IEnumerable rows) @@ -19,10 +32,10 @@ private IEnumerable _SortRowsByColumn(IEnumerable (r.Tag as GUIMod)?.DownloadCount ?? 0); + case 0: case 1: case 2: return Sort(rows, CheckboxSorter); + case 8: return Sort(rows, DownloadSizeSorter); + case 9: return Sort(rows, InstallDateSorter); + case 10: return Sort(rows, r => (r.Tag as GUIMod)?.DownloadCount ?? 0); } return Sort(rows, DefaultSorter); } @@ -242,15 +255,17 @@ private void _UpdateModsList(bool repo_updated, IEnumerable mc) mainModList.CountModsByFilter(GUIModFilter.Installed)); FilterToolButton.DropDownItems[2].Text = String.Format("Upgradeable ({0})", mainModList.CountModsByFilter(GUIModFilter.InstalledUpdateAvailable)); - FilterToolButton.DropDownItems[3].Text = String.Format("Cached ({0})", + FilterToolButton.DropDownItems[3].Text = String.Format("Replaceable ({0})", + mainModList.CountModsByFilter(GUIModFilter.Replaceable)); + FilterToolButton.DropDownItems[4].Text = String.Format("Cached ({0})", mainModList.CountModsByFilter(GUIModFilter.Cached)); - FilterToolButton.DropDownItems[4].Text = String.Format("Newly compatible ({0})", + FilterToolButton.DropDownItems[5].Text = String.Format("Newly compatible ({0})", mainModList.CountModsByFilter(GUIModFilter.NewInRepository)); - FilterToolButton.DropDownItems[5].Text = String.Format("Not installed ({0})", + FilterToolButton.DropDownItems[6].Text = String.Format("Not installed ({0})", mainModList.CountModsByFilter(GUIModFilter.NotInstalled)); - FilterToolButton.DropDownItems[6].Text = String.Format("Incompatible ({0})", + FilterToolButton.DropDownItems[7].Text = String.Format("Incompatible ({0})", mainModList.CountModsByFilter(GUIModFilter.Incompatible)); - FilterToolButton.DropDownItems[7].Text = String.Format("All ({0})", + FilterToolButton.DropDownItems[8].Text = String.Format("All ({0})", mainModList.CountModsByFilter(GUIModFilter.All)); UpdateAllToolButton.Enabled = has_any_updates; @@ -357,11 +372,11 @@ private void ModList_KeyDown(object sender, KeyEventArgs e) case Keys.Space: // If they've focused one of the checkbox columns, don't intercept - if (ModList.CurrentCell.ColumnIndex > 1) + if (ModList.CurrentCell.ColumnIndex > 2) { DataGridViewRow row = ModList.CurrentRow; // Toggle Update column if enabled, otherwise Install - for (int colIndex = 1; colIndex >= 0; --colIndex) + for (int colIndex = 2; colIndex >= 0; --colIndex) { if (row?.Cells[colIndex] is DataGridViewCheckBoxCell) { @@ -460,7 +475,7 @@ private async void ModList_CellValueChanged(object sender, DataGridViewCellEvent if (!string.IsNullOrEmpty(cmd)) Process.Start(cmd); } - else if (column_index < 2) + else if (column_index <= 2) { GUIMod gui_mod = row?.Tag as GUIMod; if (gui_mod != null) @@ -475,6 +490,9 @@ private async void ModList_CellValueChanged(object sender, DataGridViewCellEvent case 1: gui_mod.SetUpgradeChecked(row); break; + case 2: + gui_mod.SetReplaceChecked(row); + break; } await UpdateChangeSetAndConflicts( RegistryManager.Instance(CurrentInstance).registry @@ -541,6 +559,10 @@ protected override void SetSelectedRowCore(int rowIndex, bool selected) return; base.SetSelectedRowCore(rowIndex, selected); } + + //ImageList for Update/Changes Column + public System.Windows.Forms.ImageList ModChangesImageList { get; set; } + } public class MainModList @@ -648,6 +670,14 @@ public async Task> ComputeChangeSetFromModList( case GUIModChangeType.Remove: modules_to_remove.Add(change.Mod); break; + case GUIModChangeType.Replace: + ModuleReplacement repl = registry.GetReplacement(change.Mod.ToModule(), version); + if (repl != null) + { + modules_to_remove.Add(repl.ToReplace); + modules_to_install.Add(repl.ReplaceWith); + } + break; default: throw new ArgumentOutOfRangeException(); } @@ -715,34 +745,19 @@ public bool IsVisible(GUIMod mod) var nameMatchesFilter = IsNameInNameFilter(mod); var authorMatchesFilter = IsAuthorInauthorFilter(mod); var abstractMatchesFilter = IsAbstractInDescriptionFilter(mod); - var modMatchesType = IsModInFilter(mod); + var modMatchesType = IsModInFilter(ModFilter, mod); var isVisible = nameMatchesFilter && modMatchesType && authorMatchesFilter && abstractMatchesFilter; return isVisible; } - public int CountModsByFilter(GUIModFilter filter) { - switch (filter) + if (filter == GUIModFilter.All) { - case GUIModFilter.Compatible: - return Modules.Count(m => !m.IsIncompatible); - case GUIModFilter.Installed: - return Modules.Count(m => m.IsInstalled); - case GUIModFilter.InstalledUpdateAvailable: - return Modules.Count(m => m.HasUpdate); - case GUIModFilter.Cached: - return Modules.Count(m => m.IsCached); - case GUIModFilter.NewInRepository: - return Modules.Count(m => m.IsNew); - case GUIModFilter.NotInstalled: - return Modules.Count(m => !m.IsInstalled); - case GUIModFilter.Incompatible: - return Modules.Count(m => m.IsIncompatible); - case GUIModFilter.All: - return Modules.Count(); - } - throw new Kraken("Unknown filter type in CountModsByFilter"); + // Don't check each one + return Modules.Count; + } + return Modules.Count(m => IsModInFilter(filter, m)); } /// @@ -797,6 +812,18 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp Value = "-" }; + var replacing = IsModInFilter(GUIModFilter.Replaceable, mod) + ? (DataGridViewCell) new DataGridViewCheckBoxCell() + { + Value = myChange == null ? false + : myChange.ChangeType == GUIModChangeType.Replace ? true + : false + } + : new DataGridViewTextBoxCell() + { + Value = "-" + }; + var name = new DataGridViewTextBoxCell() {Value = mod.Name}; var author = new DataGridViewTextBoxCell() {Value = mod.Authors}; @@ -827,7 +854,7 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp var installDate = new DataGridViewTextBoxCell() { Value = mod.InstallDate }; var desc = new DataGridViewTextBoxCell() { Value = mod.Abstract }; - item.Cells.AddRange(selecting, updating, name, author, installVersion, latestVersion, compat, size, installDate, downloadCount, desc); + item.Cells.AddRange(selecting, updating, replacing, name, author, installVersion, latestVersion, compat, size, installDate, downloadCount, desc); selecting.ReadOnly = selecting is DataGridViewTextBoxCell; updating.ReadOnly = updating is DataGridViewTextBoxCell; @@ -835,6 +862,17 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp return item; } + /// + /// Returns a version string shorn of any leading epoch as delimited by a single colon + /// + public string StripEpoch(string version) + { + // If our version number starts with a string of digits, followed by + // a colon, and then has no more colons, we're probably safe to assume + // the first string of digits is an epoch + return Regex.IsMatch(version, @"^[0-9][0-9]*:[^:]+$") ? Regex.Replace(version, @"^([^:]+):([^:]+)$", @"$2") : version; + } + private bool IsNameInNameFilter(GUIMod mod) { return mod.Name.IndexOf(ModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1 @@ -852,32 +890,23 @@ private bool IsAbstractInDescriptionFilter(GUIMod mod) return mod.Abstract.IndexOf(ModDescriptionFilter, StringComparison.InvariantCultureIgnoreCase) != -1; } - - private bool IsModInFilter(GUIMod m) + private static bool IsModInFilter(GUIModFilter filter, GUIMod m) { - switch (ModFilter) + switch (filter) { - case GUIModFilter.Compatible: - return !m.IsIncompatible; - case GUIModFilter.Installed: - return m.IsInstalled; - case GUIModFilter.InstalledUpdateAvailable: - return m.IsInstalled && m.HasUpdate; - case GUIModFilter.Cached: - return m.IsCached; - case GUIModFilter.NewInRepository: - return m.IsNew; - case GUIModFilter.NotInstalled: - return !m.IsInstalled; - case GUIModFilter.Incompatible: - return m.IsIncompatible; - case GUIModFilter.All: - return true; + case GUIModFilter.Compatible: return !m.IsIncompatible; + case GUIModFilter.Installed: return m.IsInstalled; + case GUIModFilter.InstalledUpdateAvailable: return m.IsInstalled && m.HasUpdate; + case GUIModFilter.Cached: return m.IsCached; + case GUIModFilter.NewInRepository: return m.IsNew; + case GUIModFilter.NotInstalled: return !m.IsInstalled; + case GUIModFilter.Incompatible: return m.IsIncompatible; + case GUIModFilter.Replaceable: return m.IsInstalled && m.HasReplacement; + case GUIModFilter.All: return true; + default: throw new Kraken($"Unknown filter type {filter} in IsModInFilter"); } - throw new Kraken("Unknown filter type in IsModInFilter"); } - public static Dictionary ComputeConflictsFromModList(IRegistryQuerier registry, IEnumerable change_set, KspVersionCriteria ksp_version) { @@ -905,6 +934,14 @@ public static Dictionary ComputeConflictsFromModList(IRegistryQu break; case GUIModChangeType.Update: break; + case GUIModChangeType.Replace: + ModuleReplacement repl = registry.GetReplacement(change.Mod.ToModule(), ksp_version); + if (repl != null) + { + modules_to_remove.Add(repl.ToReplace.identifier); + modules_to_install.Add(repl.ReplaceWith.identifier); + } + break; default: throw new ArgumentOutOfRangeException(); } @@ -934,12 +971,13 @@ public static Dictionary ComputeConflictsFromModList(IRegistryQu public HashSet ComputeUserChangeSet() { - return new HashSet(Modules. - Where(mod => mod.IsInstallable()). - Select(mod => mod.GetRequestedChange()). - Where(change => change.HasValue). - Select(change => change.Value). - Select(change => new ModChange(change.Key, change.Value, null)) + return new HashSet( + Modules + .Where(mod => mod.IsInstallable()) + .Select(mod => mod.GetRequestedChange()) + .Where(change => change.HasValue) + .Select(change => change.Value) + .Select(change => new ModChange(change.Key, change.Value, null)) ); } } diff --git a/GUI/ModChange.cs b/GUI/ModChange.cs index e0704dbdc1..6bbe22aa56 100644 --- a/GUI/ModChange.cs +++ b/GUI/ModChange.cs @@ -1,21 +1,30 @@ namespace CKAN { + public enum GUIModChangeType + { + None = 0, + Install = 1, + Remove = 2, + Update = 3, + Replace = 4 + } + /// /// Everything the GUI needs to know about a change, including /// the mod itself, the change we're making, and the reason why. /// public class ModChange { - public GUIMod Mod { get; private set; } + public GUIMod Mod { get; private set; } public GUIModChangeType ChangeType { get; private set; } - public SelectionReason Reason { get; private set; } + public SelectionReason Reason { get; private set; } public ModChange(GUIMod mod, GUIModChangeType changeType, SelectionReason reason) { - Mod = mod; + Mod = mod; ChangeType = changeType; - Reason = reason; + Reason = reason; if (Reason == null) { diff --git a/GUI/Properties/Resources.Designer.cs b/GUI/Properties/Resources.Designer.cs index ccc6a3458f..9443fbc87f 100644 --- a/GUI/Properties/Resources.Designer.cs +++ b/GUI/Properties/Resources.Designer.cs @@ -60,6 +60,16 @@ internal Resources() { } } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap alert { + get { + object obj = ResourceManager.GetObject("alert", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -70,6 +80,26 @@ internal static System.Drawing.Bitmap apply { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap arrow_forward { + get { + object obj = ResourceManager.GetObject("arrow_forward", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap arrow_upward { + get { + object obj = ResourceManager.GetObject("arrow_upward", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -121,9 +151,9 @@ internal static System.Drawing.Bitmap forward { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap ksp { + internal static System.Drawing.Bitmap info { get { - object obj = ResourceManager.GetObject("ksp", resourceCulture); + object obj = ResourceManager.GetObject("info", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -131,9 +161,9 @@ internal static System.Drawing.Bitmap ksp { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap refresh { + internal static System.Drawing.Bitmap ksp { get { - object obj = ResourceManager.GetObject("refresh", resourceCulture); + object obj = ResourceManager.GetObject("ksp", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -141,9 +171,9 @@ internal static System.Drawing.Bitmap refresh { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap search { + internal static System.Drawing.Bitmap refresh { get { - object obj = ResourceManager.GetObject("search", resourceCulture); + object obj = ResourceManager.GetObject("refresh", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -151,9 +181,9 @@ internal static System.Drawing.Bitmap search { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap settings { + internal static System.Drawing.Bitmap search { get { - object obj = ResourceManager.GetObject("settings", resourceCulture); + object obj = ResourceManager.GetObject("search", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -161,9 +191,9 @@ internal static System.Drawing.Bitmap settings { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap textClear { + internal static System.Drawing.Bitmap settings { get { - object obj = ResourceManager.GetObject("textClear", resourceCulture); + object obj = ResourceManager.GetObject("settings", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -171,9 +201,9 @@ internal static System.Drawing.Bitmap textClear { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap update { + internal static System.Drawing.Bitmap smile { get { - object obj = ResourceManager.GetObject("update", resourceCulture); + object obj = ResourceManager.GetObject("smile", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -191,19 +221,9 @@ internal static System.Drawing.Bitmap star { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap thumbup { - get { - object obj = ResourceManager.GetObject("thumbup", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap info { + internal static System.Drawing.Bitmap textClear { get { - object obj = ResourceManager.GetObject("info", resourceCulture); + object obj = ResourceManager.GetObject("textClear", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -211,9 +231,9 @@ internal static System.Drawing.Bitmap info { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap smile { + internal static System.Drawing.Bitmap thumbup { get { - object obj = ResourceManager.GetObject("smile", resourceCulture); + object obj = ResourceManager.GetObject("thumbup", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } @@ -221,9 +241,9 @@ internal static System.Drawing.Bitmap smile { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap alert { + internal static System.Drawing.Bitmap update { get { - object obj = ResourceManager.GetObject("alert", resourceCulture); + object obj = ResourceManager.GetObject("update", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index f6dfb95885..4d12c26674 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -1,17 +1,17 @@ - @@ -169,4 +169,10 @@ ..\Resources\alert.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - + + ..\Resources\arrow_forward.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\arrow_upward.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/GUI/Resources/arrow_forward.png b/GUI/Resources/arrow_forward.png new file mode 100644 index 0000000000..47148c2ccd Binary files /dev/null and b/GUI/Resources/arrow_forward.png differ diff --git a/GUI/Resources/arrow_upward.png b/GUI/Resources/arrow_upward.png new file mode 100644 index 0000000000..d30a33a463 Binary files /dev/null and b/GUI/Resources/arrow_upward.png differ diff --git a/Spec.md b/Spec.md index 587fc028e1..c814884435 100644 --- a/Spec.md +++ b/Spec.md @@ -500,7 +500,23 @@ only. A list of mods which *conflict* with this mod. The current mod *will not* be installed if any of these mods are already on the system. -##### resources +##### replaced-by + +(**v1.26**) This is a way to mark a specific mod identifier as being +obsoleted and tell the client what it has been *replaced by*. It contains a +single mod that should be selected for installation if a replace command is +performed on this mod, while this mod is uninstalled. If this mod identifier +is brought back to life, an epoch change should be applied. A *replaced_by* +relationship should be added to the final release of the mod being replaced. +The listed mod should include a "provides" relationship either to this mod, +or one of this mod's listed "provides". + +replaced_by differs from other relationships in two ways: + +- It is *not* an array. Only a single mod can be defined as the replacement. +- Only "version" and "min_version" are permitted as options. + +#### resources The `resources` field describes additional information that a user or program may wish to know about the mod, but which are not required diff --git a/Tests/Core/ModuleInstaller.cs b/Tests/Core/ModuleInstaller.cs index 247b57b1e8..63cfaa45dd 100644 --- a/Tests/Core/ModuleInstaller.cs +++ b/Tests/Core/ModuleInstaller.cs @@ -717,6 +717,24 @@ public void AllowInstallsToScenarios() } } + [Test] + public void SuccessfulReplacement() + { + //Need to set up an installed DogeCoinFlag-101replaced mod that can validly be replaced by DogeTokenFlag-101 + + // Assert that DogeCoinFlag has been removed and DogeTokenFlag has been installed + Assert.IsTrue(true); + } + + [Test] + public void UnsuccessfulReplacement() + { + //Need to set up an installed DogeCoinFlag-101-replaced mod in a KSP version too low for DogeTokenFlag-101 + + // Assert that DogeCoinFlag has not been removed and DogeTokenFlag has not been installed + Assert.IsTrue(true); + } + private static void TestDogeCoinStanza(ModuleInstallDescriptor stanza) { diff --git a/Tests/Core/Versioning/KspVersionBoundTests.cs b/Tests/Core/Versioning/KspVersionBoundTests.cs index 1152d81b3f..9cb8855858 100644 --- a/Tests/Core/Versioning/KspVersionBoundTests.cs +++ b/Tests/Core/Versioning/KspVersionBoundTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using CKAN.Versioning; using NUnit.Framework; diff --git a/Tests/Core/Versioning/KspVersionJsonConverterTests.cs b/Tests/Core/Versioning/KspVersionJsonConverterTests.cs index 79267f0085..f32cf0ff7b 100644 --- a/Tests/Core/Versioning/KspVersionJsonConverterTests.cs +++ b/Tests/Core/Versioning/KspVersionJsonConverterTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using CKAN.Versioning; using Newtonsoft.Json; using NUnit.Framework; diff --git a/Tests/Core/Versioning/KspVersionTests.cs b/Tests/Core/Versioning/KspVersionTests.cs index d3291723f6..df921ff72b 100644 --- a/Tests/Core/Versioning/KspVersionTests.cs +++ b/Tests/Core/Versioning/KspVersionTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using CKAN.Versioning; using NUnit.Framework; diff --git a/Tests/Data/DogeTokenFlag-1.01.zip b/Tests/Data/DogeTokenFlag-1.01.zip new file mode 100644 index 0000000000..bd83ebd2df Binary files /dev/null and b/Tests/Data/DogeTokenFlag-1.01.zip differ diff --git a/Tests/Data/TestData.cs b/Tests/Data/TestData.cs index 5f84ded212..eb9f05b5fc 100644 --- a/Tests/Data/TestData.cs +++ b/Tests/Data/TestData.cs @@ -208,6 +208,79 @@ public static string DogeCoinFlag_101() "; } + /// + /// Replaced_by DogeCoinFlag 1.01 info. This doesn't contain any bugs. + /// + public static string DogeCoinFlag_101_replaced() + { + return @" + { + ""spec_version"": ""v1.24"", + ""identifier"": ""DogeCoinFlag"", + ""install"": [ + { + ""file"": ""DogeCoinFlag-1.01/GameData/DogeCoinFlag"", + ""install_to"": ""GameData"", + ""filter"" : [ ""Thumbs.db"", ""README.md"" ], + ""filter_regexp"" : ""\\.bak$"" + } + ], + ""replaced_by"": { + ""name"": ""DogeTokenFlag"", + ""min_version"": ""1.01"" + ), + ""resources"": { + ""kerbalstuff"": { + ""url"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag"" + }, + ""homepage"": ""https://www.reddit.com/r/dogecoin/comments/1tdlgg/i_made_a_more_accurate_dogecoin_and_a_ksp_flag/"" + }, + ""name"": ""Dogecoin Flag"", + ""license"": ""CC-BY"", + ""abstract"": ""Such flag. Very currency. To the mun! Wow!"", + ""author"": ""pjf"", + ""version"": ""1.01"", + ""download"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag/download/1.01"", + ""comment"": ""Generated by ks2ckan"", + ""download_size"": 53647, + ""ksp_version"": ""0.24"" + } + "; + } + + /// + /// DogeTokenFlag 1.01 info. This is our replacement target. + /// + public static string DogeTokenFlag_101() + { + return @" + { + ""spec_version"": 1, + ""identifier"": ""DogeTokenFlag"", + ""install"": [ + { + ""file"": ""DogeTokenFlag-1.01/GameData/DogeTokenFlag"", + ""install_to"": ""GameData"", + ""filter"" : [ ""Thumbs.db"", ""README.md"" ], + ""filter_regexp"" : ""\\.bak$"" + } + ], + ""resources"": { + ""homepage"": ""https://www.reddit.com/r/dogecoin/comments/1tdlgg/i_made_a_more_accurate_dogecoin_and_a_ksp_flag/"" + }, + ""name"": ""Dogetoken Flag"", + ""license"": ""CC-BY"", + ""abstract"": ""Such flag. Very token. To the mun! Wow!"", + ""author"": ""politas"", + ""version"": ""1.01"", + ""download"": ""https://kerbalstuff.com/mod/269/Dogetoken%20Flag/download/1.01"", + ""comment"": ""Generated by hand"", + ""download_size"": 53647, + ""ksp_version"": ""0.25"" + } + "; + } + public static CkanModule DogeCoinFlag_101_module() { return CkanModule.FromJson(DogeCoinFlag_101()); diff --git a/Tests/NetKAN/Services/FileServiceTests.cs b/Tests/NetKAN/Services/FileServiceTests.cs index 06c3ac339d..58b6fbcf5b 100644 --- a/Tests/NetKAN/Services/FileServiceTests.cs +++ b/Tests/NetKAN/Services/FileServiceTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using CKAN.NetKAN.Services; using NUnit.Framework; using Tests.Data; diff --git a/Tests/NetKAN/Services/ModuleServiceTests.cs b/Tests/NetKAN/Services/ModuleServiceTests.cs index 9dd857557e..9c3d62c8c5 100644 --- a/Tests/NetKAN/Services/ModuleServiceTests.cs +++ b/Tests/NetKAN/Services/ModuleServiceTests.cs @@ -1,4 +1,4 @@ -using CKAN; +using CKAN; using CKAN.NetKAN.Services; using CKAN.Versioning; using Newtonsoft.Json.Linq; diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index cb412c52a4..27cc57a6cf 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -36,9 +36,6 @@ 7 - - ..\_build\lib\nuget\Castle.Core.4.2.1\lib\net45\Castle.Core.dll - ..\lib\curlsharp-v0.5.1-2-gd2d5699\CurlSharp.dll @@ -46,23 +43,28 @@ ..\_build\lib\nuget\ICSharpCode.SharpZipLib.Patched.0.86.5.1\lib\net20\ICSharpCode.SharpZipLib.dll True - + ..\_build\lib\nuget\log4net.2.0.8\lib\net45-full\log4net.dll ..\_build\lib\nuget\Moq.4.7.145\lib\net45\Moq.dll + False ..\_build\lib\nuget\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll ..\_build\lib\nuget\NUnit.3.9.0\lib\net45\nunit.framework.dll + False + + ..\_build\lib\nuget\Castle.Core.4.2.1\lib\net45\Castle.Core.dll + @@ -137,23 +139,12 @@ - - - - - - - - - - - @@ -187,6 +178,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {3B9AEA22-FA3B-4E43-9283-EABDD81CF271}